美文网首页spring cloud 微服务
spring cloud中如何安全使用ThreadLocal

spring cloud中如何安全使用ThreadLocal

作者: 大浪滔滔 | 来源:发表于2017-12-09 00:49 被阅读424次

    问题

    在我们产品中,我们使用了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 认证流程

    相关文章

      网友评论

        本文标题:spring cloud中如何安全使用ThreadLocal

        本文链接:https://www.haomeiwen.com/subject/swtuixtx.html