`
jxb8901
  • 浏览: 164963 次
  • 性别: Icon_minigender_1
  • 来自: shenzhen
社区版块
存档分类
最新评论

一个使用线程局部存储(ThreadLocal)技术导致用户会话信息泄露案例的剖析

阅读更多

一个使用线程局部存储(ThreadLocal)技术导致用户会话信息泄露案例的剖析

我们的系统是一个B/S架构的WEB系统,采用的是类似struts的基于action的WEB框架,近期系统上线后碰到了一个用户会话信息泄露的问题,虽然问题最终于半天后得到了解决,但对此问题的剖析有利于我们更深地理解与多线程并发相关的线程局部存储(ThreadLocal)技术,故特撰此文与大家共飨。

线程局部存储(ThreadLocal)技术是多线程技术中用于解决并发问题的一个最轻量级且使用起来最简单的技术。其原理是将一块内存与线程关联,每个线程访问的的变量都存在于本线程的局部存储区中,因此多个线程间访问相同的变量名时不会产生并发问题。对应就到java中,类ThreadLocal就是JVM用来实现线程局部存储的。

我们磁到的问题是这样的,正常情况下用户登录后系统首页会显示用户的账户等信息,但某用户登录后发现其首页显示的却是其他人的信息。当其再次刷新首页后,页面信息显示却又恢复了正常。发生这样的问题后,我们在测试环境下进行了测试,分别使用两个用户在不同的浏览器中登录系统,并同时刷新首页,此时问题被复现了,而且发生此问题的机率还比较高,粗略估计每10笔就有一两笔发生。另外测试中还发现一个现象比较值得注意,就是此问题并非是并发情况下才会发生,当一个用户未发送任何交易时,另一用户多次刷新页面后还有可能会显示前一用户的信息。

经验告诉我们,如果一个问题有时发生有时不发生,而且发生的机率不是很高,那么该问题很有可能与多线程并发有关。问题发生后我们首先想到是否是程序中的交易处理类未考虑多线程并发呢?最后的结果表明这个问题确实与多线程并发有关,但却并非是交易类未考虑并发而导致的,事实上系统中的所有交易类都是线程安全的(类似Webwork中的Action类),根本不需考虑多线程并发的问题。为了更好地让大家思考这个问题,下面先描述一下系统中交易的基本处理流程,如下图:
----> 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();
	...
}

各交易类使用如下方式获取所需信息:
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);
		...
	}
	...
}

以上对系统的交易处理流程作了一个大致的介绍,那么回到文章开头的问题中,是什么导致了用户会话信息的泄露呢?也就是说是什么导致了另一个用户可以访问其它用户的RequestContext中的数据呢?

答案相当简单,就是因为这个首页交易只是一个纯粹的JSP页面,该交易并未经过用户会话过滤器的处理。有人可能会问,既然该页面并未经过滤器处理,那么该JSP页面对应的处理线程的RequestContext中就不应该有任何用户信息,这样JSP页面上就应该不显示任何内容才对,为什么页面上反而会显示出其它人的用户信息呢?要解答这个问题就要先了解应用服务器是如何使用线程技术处理用户请求的。应用服务器收到一个用户请求后,总是分配一个独立的线程对该请求进行处理,考虑到频繁创建和销毁线程的开销太大,一般应用服务器都会有一个高效的线程池系统来回收已完成处理的请求线程,也就是说当某个请求被处理完后,相应线程并不会被销毁,而是被返回到线程池中以再次响应其它请求。这样一说,大家是不是就明白问题原因所在了呢?

是的,当某个用户提交访问页面的请求时,应用服务器会从线程池中取得一个空闲线程以处理该请求,如果此时分配的线程是曾经响应过其它用户请求的线程时,该线程的局部存储中就还保留有其它用户的用户信息,因为系统中所有交易都经由会话过滤器处理过,所以当执行流程转到交易类时,线程的局部存储中已经有了正确的用户信息,此时并不会产生任何问题。而一旦所访问的交易没有经过会话过滤器处理时,页面上就出现仍然存留于线程局部存储中的其它用户的信息了。

一旦问题的原因分析清楚了,要解决就很容易。


通过以上的剖析,你是否对线程局部存储(ThreadLocal)、线程池等技术有了更深的理解呢?欢迎大家多谈谈自己的看法。



分享到:
评论
14 楼 vincent_fan 2008-04-18  
chbest 写道
一般来说一个好的框架是需要拦截所有的...
里面通过配置正则表达式来去匹配不同类型的servlet以减少web配置和程序的可控性.
不知道对不对,呵呵

我们一般是这么做的,拦截所有;使用struts时所有页面的访问都是通过action进行转发,拦截/*.do,其实也是拦截所有。
13 楼 javazhujf 2008-04-18  
最好的选择应该是RequestListener,它里面的两个方法可以在创建和结束Request时执行,而且一个Request内的处理都是在同一线程内(除非你自己创建个线程),创建Request时初始化线程数据,结束Request时清空线程数据。
12 楼 liangguanhui 2008-04-08  
其实问题产生的原因不是很简单吗?就是Filter里面没有清楚ThreadLocal的东西。
11 楼 chbest 2008-04-08  
一般来说一个好的框架是需要拦截所有的...
里面通过配置正则表达式来去匹配不同类型的servlet以减少web配置和程序的可控性.
不知道对不对,呵呵
10 楼 pufan 2008-01-16  
jxb8901 写道

之所以不敢确定,主要是对filter的拦截机制了解得还不是非常透彻,现在还不太清楚filter对forword和include的页面是如何做拦截的,不过这与目前讨论的问题已经没有太大关系了。





应该不做拦截的,否则岂不是可能会出现n次过滤,但印象中websphere好像是对forward再做一次过滤,相当于一次新请求。

没有websphere的环境,谁有做一次试验变知。
9 楼 dennis_zane 2008-01-16  
楼上的第一种写法是正确的,试试就知道,没必要去拦截所有请求
8 楼 jxb8901 2008-01-16  
ahuaxuan 写道

这个跟线程池无关,还是应用代码写得有问题

我认为这个这个bug是对做权限的人对threadlocal的不甚了解导致的,因为在一次请求结束的时候他应该把线程中的用户数据清空的。如果有了这个操作那么什么问题都没有了。

ahuaxuan同学说的非常准确,我在分析这个问题的过程中也一直存有两个疑问:
1、UserSessionFilter能否在请求处理完成后清除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);
		...
		try {
			chain.doFilter(request, response);
		}
		finally {
			RequestContext.get().clear(); // 能否改为在这里清除?
		}
		...
	}
	...
}


2、UserSessionFilter能否改为拦截所有请求,而不仅限于MainServlet?
原来UserSessionFilter的配置是这样的:
	<filter-mapping>
		<filter-name>UserSessionFilter</filter-name>
		<servlet-name>MainServlet</servlet-name> <!-- 这里能否去掉?改为拦截"/*"? -->
		<!-- 
		<url-pattern>/*</url-pattern>
		-->
	</filter-mapping>


上面两个两种修改理论上也应该可以成立,但现在不敢确定是否会引起其它问题,故目前的修改方案是,所有的JSP页面不允许直接访问,必须通过MainServlet来访问。

之所以不敢确定,主要是对filter的拦截机制了解得还不是非常透彻,现在还不太清楚filter对forword和include的页面是如何做拦截的,不过这与目前讨论的问题已经没有太大关系了。


7 楼 ahuaxuan 2008-01-16  
jomper 写道
ahuaxuan 写道

我认为这个这个bug是对做权限的人对threadlocal的不甚了解导致的,因为在一次请求结束的时候他应该把线程中的用户数据清空的。如果有了这个操作那么什么问题都没有了。acegi中就是这么做的


一次请求是指的,一次request发生?
那么request发生的次数似乎是会非常频繁,那么每次都从session里取出来 并存为线程副本,为何不从session里直接取呢?

似乎在一个线程引起的一个生命周期相对较长的会话里,使用ThreadLocal来保存线程副本才是比较合适的,短期会话里线程副本刚刚被创建,就马上被迫消亡,并没有起到在同一个线程里长期使用,避免并发和重复创建复杂对象作用。


第一:放在线程里的好处是不需要参数传递,否则你就需要四处传递你的session了,
第二:如果我的信息不在session里,比如说在memcached里,难道每次我要取session的时候都去读一下memcached吗,当然是放在线程中最合适了
6 楼 jomper 2008-01-16  
ahuaxuan 写道

我认为这个这个bug是对做权限的人对threadlocal的不甚了解导致的,因为在一次请求结束的时候他应该把线程中的用户数据清空的。如果有了这个操作那么什么问题都没有了。acegi中就是这么做的


一次请求是指的,一次request发生?
那么request发生的次数似乎是会非常频繁,那么每次都从session里取出来 并存为线程副本,为何不从session里直接取呢?

似乎在一个线程引起的一个生命周期相对较长的会话里,使用ThreadLocal来保存线程副本才是比较合适的,短期会话里线程副本刚刚被创建,就马上被迫消亡,并没有起到在同一个线程里长期使用,避免并发和重复创建复杂对象作用。
5 楼 jomper 2008-01-16  
fch0402 写道
难道线程被放回线程池的时候不清除之前的线程局部变量?晕。。。。应该算线程池的一个BUG吧

线程池是web容器的,那个pool怎么可能知道被你取出的thread 会用来做什么,更不可能去清楚那个不知哪里来的ThreadLocal里的线程副本.web容器的线程池是无辜的.
4 楼 sunrie 2008-01-16  
同意楼上的,明显是代码没写好嘛,我也是这么使用的啊,但只要filter记得清除了,根本不会产生相应的问题嘛
3 楼 ahuaxuan 2008-01-16  
jomper 写道
的确 因为容器是用线程池来管理请求线程。所以用threadlocal来管理user session是不合适的。

不是这样的,threadlocal来管理用户是非常合适的,可以说是一个典型的正确用法。
fch0402 写道
难道线程被放回线程池的时候不清除之前的线程局部变量?晕。。。。应该算线程池的一个BUG吧

这个跟线程池无关,还是应用代码写得有问题

我认为这个这个bug是对做权限的人对threadlocal的不甚了解导致的,因为在一次请求结束的时候他应该把线程中的用户数据清空的。如果有了这个操作那么什么问题都没有了。acegi中就是这么做的:
finally {
            // This is the only place in this class where SecurityContextHolder.getContext() is called
            SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();

            // Crucial removal of SecurityContextHolder contents - do this before anything else.
            SecurityContextHolder.clearContext();

            request.removeAttribute(FILTER_APPLIED);
·········
}
2 楼 fch0402 2008-01-16  
难道线程被放回线程池的时候不清除之前的线程局部变量?晕。。。。应该算线程池的一个BUG吧
1 楼 jomper 2008-01-16  
的确 因为容器是用线程池来管理请求线程。所以用threadlocal来管理user session是不合适的。

相关推荐

    简单分析Java线程编程中ThreadLocal类的使用共

    简单分析Java线程编程中ThreadLocal类的使用共4页.pdf.zip

    ThreadLocal 内存泄露的实例分析1

    问题背景在 Tomcat 中,下面的代码都在 webapp 内,会导致 WebappClassLoaderWebappClassLoader 泄漏,无法被回收。

    详解Spring Cloud中Hystrix 线程隔离导致ThreadLocal数据丢失

    主要介绍了详解Spring Cloud中Hystrix 线程隔离导致ThreadLocal数据丢失,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

    04、导致JVM内存泄露的ThreadLocal详解-ev

    04、导致JVM内存泄露的ThreadLocal详解_ev04、导致JVM内存泄露的ThreadLocal详解_ev04、导致JVM内存泄露的ThreadLocal详解_ev04、导致JVM内存泄露的ThreadLocal详解_ev04、导致JVM内存泄露的ThreadLocal详解_ev04、...

    java ThreadLocal多线程专属的变量源码

    java ThreadLocal多线程专属的变量源码java ThreadLocal多线程专属的变量源码java ThreadLocal多线程专属的变量源码java ThreadLocal多线程专属的变量源码java ThreadLocal多线程专属的变量源码java ThreadLocal多...

    2、导致JVM内存泄露的ThreadLocal详解

    导致JVM内存泄露的ThreadLocal详解 为什么要有ThreadLocal ThreadLocal的使用 实现解析 引发的内存泄漏分析 错误使用ThreadLocal导致 线程不安全分析

    线程ThreadLocal机制实现例子

    本例以序列号生成的程序为例,展示ThreadLocal的使用

    ThreadLocal

    ThreadLocal入门教程。 讲解了线程安全和ThreadLocal的使用的基本知识。

    Java多线程编程中ThreadLocal类的用法及深入

    其实,它就是一个容器,用于存放线程的局部变量,我认为应该叫做 ThreadLocalVariable(线程局部变量)才对,真不理解为什么当初 Sun 公司的工程师这样命名。 早在 JDK 1.2 的时代,java.lang.ThreadLocal 就诞生了...

    线程锁+ThreadLocal.docx

    多线程可以同时运行多个任务但是当多个线程同时访问共享数据时,可能导致数据不同步,甚至错误!线程锁主要用来给方法、代码块加锁

    ThreadLocal应用示例及理解

    ThreadLocal应用示例及理解,这个写了相关的示例,可以参考一下。

    使用Java ThreadLocal.docx

    我们可以看到,通过这段代码实例化了一个ThreadLocal对象。我们只需要实例化对象一次,并且也不需要知道它是被哪个线程实例化。虽然所有的线程都能访问到这个ThreadLocal实例,但是每个线程却只能访问到自己通过调用...

    JavaEE DBUtil结合ThreadLocal的一个案例

    JavaEE DBUtil结合ThreadLocal的一个案例

    Hibernate用ThreadLocal模式(线程局部变量模式)管理Session

    今天小编就为大家分享一篇关于Hibernate用ThreadLocal模式(线程局部变量模式)管理Session,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧

    ThreadLocal详解及说明

    关于线程变量ThreadLocal的介绍以及说明. 关于线程变量ThreadLocal的介绍以及说明. 关于线程变量ThreadLocal的介绍以及说明. 关于线程变量ThreadLocal的介绍以及说明. 关于线程变量ThreadLocal的介绍以及说明. ...

    java线程本地变量ThreadLocal详解

    ThreadLocal则为每一个线程提供了一个变量副本,从而隔离了多个线程访问数据的冲突,ThreadLocal提供了线程安全的对象封装,下面我们就来详细了解一下吧

    理解threadlocal

    ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序,ThreadLocal并不是一个Thread,而是Thread的局部变量

    Java中的线程同步与ThreadLocal无锁化线程封闭实现

    主要介绍了Java中的线程同步与ThreadLocal无锁化线程封闭实现,Synchronized关键字与ThreadLocal变量的使用是Java中线程控制的基础,需要的朋友可以参考下

    8个案例详解教会你ThreadLocal.docx

    通常情况下,我们创建的成员变量都是线程不安全的。因为他可能被多个线程同时修改,此变量对于多...而使用ThreadLocal创建的变量只能被当前线程访问,其他线程无法访问和修改。也就是说:将线程公有化变成线程私有化。

Global site tag (gtag.js) - Google Analytics