`
jxb8901
  • 浏览: 166966 次
  • 性别: 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是不合适的。

相关推荐

    ThreadLocal 内存泄露的实例分析1

    在描述的案例中,`LeakingServlet` 是一个使用了 `ThreadLocal` 的 Servlet。`ThreadLocal` 是 Java 中用于在单个线程内存储线程局部变量的类,每个线程都有自己的副本,不会互相干扰。`MyThreadLocal` 是 `...

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

    Java线程编程中的ThreadLocal类是一个非常重要的工具,它在多线程环境下提供了一种线程局部变量的机制。ThreadLocal并非是简单的变量,而是一种能够确保每个线程都拥有独立副本的变量容器。理解ThreadLocal的工作...

    ThreadLocal的几种误区

    1. 存储当前会话用户信息:例如,可以在一个线程中使用ThreadLocal保存用户的登录状态,确保每个线程只处理一个用户的信息。 2. 存放上下文变量:如WebWork的ActionContext,它可以保存请求处理过程中的各种上下文...

    ThreadLocal

    ThreadLocal是Java编程语言中的一个类,用于在多线程环境中提供线程局部变量。它是一种特殊类型的变量,每个线程都有自己的副本,互不影响,从而实现线程间数据隔离。ThreadLocal通常被用来解决线程共享数据时可能...

    ThreadLocal应用示例及理解

    **线程局部变量(ThreadLocal)是Java编程中一个非常重要的工具类,它在多线程环境下提供了线程安全的数据存储。ThreadLocal并不是一个变量,而是一个类,它为每个线程都创建了一个独立的变量副本,使得每个线程都...

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

    ThreadLocal是Java中的一种线程局部存储机制,用于在线程中传递数据。但是,在Hystrix线程隔离模式下,ThreadLocal数据可能会丢失,因为Hystrix将请求放入Hystrix的线程池中去执行,这时候某个请求就有A线程变成B...

    共享线程和局部存储技术

    大多数编程语言都提供了支持线程局部存储的特性,例如C++中的`thread_local`关键字,Java中的`ThreadLocal`类等。 线程局部存储的实现通常依赖于操作系统的线程模型。在某些系统中,线程局部存储可以是通过特定的...

    ThreadLocal相关

    2. 会话信息:ThreadLocal 可以用于存储会话信息,如用户信息、订单信息等。 3. 线程上下文:ThreadLocal 可以用于存储线程上下文信息,如当前用户、当前订单等。 ThreadLocal 的实现 ThreadLocal 的实现是基于 ...

    java中ThreadLocal类的使用

    Java中的`ThreadLocal`类是一个非常实用的工具,它提供了线程局部变量的功能。线程局部变量意味着每个线程都拥有自己独立的变量副本,互不干扰,这在多线程编程中尤其有用,可以避免数据共享带来的同步问题。下面...

    ThreadLocal简单Demo

    因此,如果一个`ThreadLocal`变量在线程结束后仍然被其他线程引用,可能会导致内存泄漏。 **使用示例** 下面是一个简单的`ThreadLocal`使用示例: ```java public class ThreadLocalDemo { public static void ...

    Java中ThreadLocal的设计与使用

    这使得ThreadLocal适合于存储线程私有数据,如数据库连接、用户会话信息等。 ### ThreadLocal的最佳实践 1. **避免过度使用**:虽然ThreadLocal方便,但过度依赖可能导致设计复杂,不易维护。应优先考虑其他同步...

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

    `ThreadLocal`类内部维护了一个`ThreadLocalMap`结构,该结构存储了线程与线程局部变量之间的映射关系。每当一个新的线程创建并首次访问某个`ThreadLocal`实例时,都会在该线程的`ThreadLocalMap`中添加一个新的键值...

    Java ThreadLocal详解_动力节点Java学院整理

    2. 会话管理:在多线程环境中,使用ThreadLocal可以在每个线程中创建一个会话副本,这样可以避免多个线程之间的会话竞争问题。 3. 缓存管理:在多线程环境中,使用ThreadLocal可以在每个线程中创建一个缓存副本,...

    使用ThreadLocal管理“session”数据

    而ThreadLocal提供了一个线程局部的存储空间,每个线程都有自己的ThreadLocal变量副本,互不影响。因此,使用ThreadLocal可以在多线程环境下保证session数据的安全性,避免了同步锁带来的性能开销。 3. **如何使用...

    ThreadLocal原理及在多层架构中的应用

    **线程局部变量(ThreadLocal)是Java编程中一个非常重要的概念,主要用于在多线程环境中为每个线程提供独立的变量副本。ThreadLocal不是一种数据结构,而是一种解决线程间共享数据的方式,它提供了线程安全的局部...

    正确理解ThreadLocal.pdf

    `ThreadLocal`是Java平台提供的一种线程局部变量的解决方案,它为每一个使用该变量的线程都提供了独立的变量副本,使得每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。这不同于普通的静态...

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

    ThreadLocal 是 Java 中用于处理线程局部变量的一个重要工具,它的设计目的是为了解决多线程环境下线程间数据隔离的问题。以下是对标题和描述中所述知识点的详细说明: 1. **与 Synchronized 的区别** - `...

    JDK的ThreadLocal理解(一)使用和测试

    通过创建ThreadLocal实例,我们可以为每个线程提供一个独立的变量副本,这些副本在各个线程之间互不影响,从而实现线程局部变量的功能。本文将深入探讨ThreadLocal的使用、原理以及一些实际应用中的测试案例。 ### ...

Global site tag (gtag.js) - Google Analytics