spring cloud中如何安全使用ThreadLocal
问题
在我们产品中,我们使用了Spring Cloud+OAuth2+JWT的架构,并将微服务网关和系统管理微服务(prong-system-api)都配置为了OAuth2的资源服务器(resource server)。我们在资源服务器的解析JWT的access token时将相关的用户信息存入了ThreadLocal:
public class CustomerAccessTokenConverter extends DefaultAccessTokenConverter {
@Override
public OAuth2AccessToken extractAccessToken(String value, Map<String, ?> map) {
OAuth2AccessToken token = super.extractAccessToken(value, map);
// 从access token中解析用户信息
IJWTUser user = xxx;
// 将用户保存到threadlocal中,在其他地方就可以使用了
BaseContextHandler.setJwtUser(user);
return token;
}
}
这时候,问题来了。我们发现在用户张三登录后,李四访问网关一个不需要用户认证的api时(这时资源服务器并不会解析token并存入ThreadLocal),网关从ThreadLocal取到的当前用户居然是张三
!而且出现的频率是随机的。
这是什么原因呢?
问题原因
spring boot内嵌了tomcat web服务器,而一般的web服务器对于每个http请求,会开设一个线程用于处理请求,为了提高响应速度,web服务器一般都会配置启用一个线程池,所以线程池中的线程,都会存在复用的可能。
这时,如果我们使用ThreadLocal来在线程内共享数据时,当线程处理结束后,没有从ThreadLocal剔除数据时,可能存在数据被窜用的可能,更严重的导致内存泄露(见:http://my.oschina.net/ainilife/blog/261297)。
分析和解决
我们观察资源服务器启动时的debug日志,发现spring创建了两个过滤器处理链:
Creating filter chain: org.springframework.boot.actuate.autoconfigure.ManagementWebSecurityAutoConfiguration$LazyEndpointPathRequestMatcher@5c648e38, [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@6bc8d8bd, org.springframework.security.web.context.SecurityContextPersistenceFilter@47bbf44d,
org.springframework.security.web.header.HeaderWriterFilter@5cf6ba1c,
org.springframework.web.filter.CorsFilter@3ef7f332,
org.springframework.security.web.authentication.logout.LogoutFilter@949f0d,
org.springframework.security.web.authentication.www.BasicAuthenticationFilter@7e4c0bc7, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@2a9f7572, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@4202bfe8, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@395c21ee,
org.springframework.security.web.session.SessionManagementFilter@6afb240d,
org.springframework.security.web.access.ExceptionTranslationFilter@51d76ad3,
org.springframework.security.web.access.intercept.FilterSecurityInterceptor@498f1f63]
2017-12-08 22:26:12.689 INFO 60318 --- [ main] o.s.s.web.DefaultSecurityFilterChain :
Creating filter chain: org.springframework.security.web.util.matcher.AnyRequestMatcher@1,
[org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@25c98637, org.springframework.security.web.context.SecurityContextPersistenceFilter@784c74e,
org.springframework.security.web.header.HeaderWriterFilter@3052395d,
org.springframework.security.web.authentication.logout.LogoutFilter@11a0c708, org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationProcessingFilter@623bdc46,
org.springframework.security.web.savedrequest.RequestCacheAwareFilter@5fee3c9c, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@7e577eed, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@108fd5d5,
org.springframework.security.web.session.SessionManagementFilter@a2ca681,
org.springframework.security.web.access.ExceptionTranslationFilter@14b9817b,
org.springframework.security.web.access.intercept.FilterSecurityInterceptor@377cc0f8]
从中我们找到排在最前面的过滤器为WebAsyncManagerIntegrationFilter
在这个过滤器的前面插入我们自定义的过滤器,相关代码:
@Configuration
@EnableConfigurationProperties(SecuritySettings.class)
public class DefaultResourceServerConfiguration extends ResourceServerConfigurerAdapter {
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Autowired
private SecuritySettings settings;
@Override
public void configure(final HttpSecurity http) throws Exception {
if (StringUtils.isNotEmpty(settings.getPermitAll())) {
// 增加自定义过滤器,放在所有过滤器的前面
http.addFilterBefore(new ThreadLocalFilter(), WebAsyncManagerIntegrationFilter.class);
...
}
@Override
public void configure(ResourceServerSecurityConfigurer config) {
jwtAccessTokenConverter.setAccessTokenConverter(new CustomerAccessTokenConverter());
}
}
自定义的过滤器:
public class ThreadLocalFilter extends GenericFilterBean {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
try {
chain.doFilter(request, response);
} finally {
// 删除线程本地变量
BaseContextHandler.remove();
}
}
}
测试后问题解决。
参考:
ThreadLocal在应用中,因服务器线程复用导致问题
当ThreadLocal碰上线程池
Writing a Custom Filter in Spring Security
对Java 过滤器、拦截器、监听器在Spring MVC中应用场景的探究
Spring Cloud内置的Zuul过滤器详解
Spring Cloud OAuth2 认证流程