一个使用线程局部存储(ThreadLocal)技术导致用户会话信息泄露案例的剖析
我们的系统是一个B/S架构的WEB系统,采用的是类似struts的基于action的WEB框架,近期系统上线后碰到了一个用户会话信息泄露的问题,虽然问题最终于半天后得到了解决,但对此问题的剖析有利于我们更深地理解与多线程并发相关的线程局部存储(ThreadLocal)技术,故特撰此文与大家共飨。
线程局部存储(ThreadLocal)技术是多线程技术中用于解决并发问题的一个最轻量级且使用起来最简单的技术。其原理是将一块内存与线程关联,每个线程访问的的变量都存在于本线程的局部存储区中,因此多个线程间访问相同的变量名时不会产生并发问题。对应就到java中,类ThreadLocal就是JVM用来实现线程局部存储的。
我们磁到的问题是这样的,正常情况下用户登录后系统首页会显示用户的账户等信息,但某用户登录后发现其首页显示的却是其他人的信息。当其再次刷新首页后,页面信息显示却又恢复了正常。发生这样的问题后,我们在测试环境下进行了测试,分别使用两个用户在不同的浏览器中登录系统,并同时刷新首页,此时问题被复现了,而且发生此问题的机率还比较高,粗略估计每10笔就有一两笔发生。另外测试中还发现一个现象比较值得注意,就是此问题并非是并发情况下才会发生,当一个用户未发送任何交易时,另一用户多次刷新页面后还有可能会显示前一用户的信息。
经验告诉我们,如果一个问题有时发生有时不发生,而且发生的机率不是很高,那么该问题很有可能与多线程并发有关。问题发生后我们首先想到是否是程序中的交易处理类未考虑多线程并发呢?最后的结果表明这个问题确实与多线程并发有关,但却并非是交易类未考虑并发而导致的,事实上系统中的所有交易类都是线程安全的(类似Webwork中的Action类),根本不需考虑多线程并发的问题。为了更好地让大家思考这个问题,下面先描述一下系统中交易的基本处理流程,如下图:
- ----> EncodingFilter, UserSessionFilter ---> MainServlet ---> Transaction ---> JSP
----> EncodingFilter, UserSessionFilter ---> MainServlet ---> Transaction ---> JSP
用户发起的交易首先经过一组过滤器进行交易的通用处理,其中包括字符集转换过滤器、用户会话处理过滤器等,其中用户会话过滤器实现了基于线程局部存储技术的用户会话访问(后面会详细描述)。过滤器处理后所有交易全部交由一个主控的Sevlet根据交易名进行交易的转发。具体交易处理类调用相关领域对象实现交易处理,并为JSP页面准备展示所需数据。
在上述过程中,因为交易类需要频繁访问用户会话信息,比如获取当前用户的权限信息、获取当前用户的帐户信息等,为了减少参数传递,系统中的RequestContext类实现了获取上述对象的便捷方法。以下是该类的部分方法:
- public class RequestContext {
- private static ThreadLocal context = new ThreadLocal() {
- protected synchronized Object initialValue() {
- return new RequestContext();
- }
- };
-
- public static RequestContext get() {
- return (RequestContext) context.get();
- }
-
- public User getUser();
- public UserSession getSession();
- public String getIP();
- ...
- }
public class RequestContext {
private static ThreadLocal context = new ThreadLocal() {
protected synchronized Object initialValue() {
return new RequestContext();
}
};
public static RequestContext get() {
return (RequestContext) context.get();
}
public User getUser();
public UserSession getSession();
public String getIP();
...
}
各交易类使用如下方式获取所需信息:
- User user = RequestContext.get().getUser();
- UserSession session = RequestContext.get().getIP();
- String ip = RequestContext.get().getIP();
- ...
User user = RequestContext.get().getUser();
UserSession session = RequestContext.get().getIP();
String ip = RequestContext.get().getIP();
...
系统通过用户会话过滤器拦截所有经由主控Servlet处理的交易,在交易处理前将用户信息注入到一个RequestContext的实例中,然后将该实例与当前线程绑定,这样随后的交易类就可以便利地访问用户会话信息了,相关代码如下:
- public class UserSessionFilter implements Filter {
- ...
- public void doFilter(ServletRequest request, ServletResponse response,
- FilterChain chain) throws IOException, ServletException {
- ...
- UserSession session = getSession();
- User user = getUserFromSession(session);
- RequestContext.get().clear();
- RequestContext.get().setSession(session);
- RequestContext.get().setUser(user);
- ...
- chain.doFilter(request, response);
- ...
- }
- ...
- }
public class UserSessionFilter implements Filter {
...
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
...
UserSession session = getSession();
User user = getUserFromSession(session);
RequestContext.get().clear();
RequestContext.get().setSession(session);
RequestContext.get().setUser(user);
...
chain.doFilter(request, response);
...
}
...
}
以上对系统的交易处理流程作了一个大致的介绍,那么回到文章开头的问题中,是什么导致了用户会话信息的泄露呢?也就是说是什么导致了另一个用户可以访问其它用户的RequestContext中的数据呢?
答案相当简单,就是因为这个首页交易只是一个纯粹的JSP页面,该交易并未经过用户会话过滤器的处理。有人可能会问,既然该页面并未经过滤器处理,那么该JSP页面对应的处理线程的RequestContext中就不应该有任何用户信息,这样JSP页面上就应该不显示任何内容才对,为什么页面上反而会显示出其它人的用户信息呢?要解答这个问题就要先了解应用服务器是如何使用线程技术处理用户请求的。应用服务器收到一个用户请求后,总是分配一个独立的线程对该请求进行处理,考虑到频繁创建和销毁线程的开销太大,一般应用服务器都会有一个高效的线程池系统来回收已完成处理的请求线程,也就是说当某个请求被处理完后,相应线程并不会被销毁,而是被返回到线程池中以再次响应其它请求。这样一说,大家是不是就明白问题原因所在了呢?
是的,当某个用户提交访问页面的请求时,应用服务器会从线程池中取得一个空闲线程以处理该请求,如果此时分配的线程是曾经响应过其它用户请求的线程时,该线程的局部存储中就还保留有其它用户的用户信息,因为系统中所有交易都经由会话过滤器处理过,所以当执行流程转到交易类时,线程的局部存储中已经有了正确的用户信息,此时并不会产生任何问题。而一旦所访问的交易没有经过会话过滤器处理时,页面上就出现仍然存留于线程局部存储中的其它用户的信息了。
一旦问题的原因分析清楚了,要解决就很容易。
通过以上的剖析,你是否对线程局部存储(ThreadLocal)、线程池等技术有了更深的理解呢?欢迎大家多谈谈自己的看法。