spring cloud中如何安全使用ThreadLocal

問題

在我們產(chǎn)品中,我們使用了Spring Cloud+OAuth2+JWT的架構(gòu),并將微服務(wù)網(wǎng)關(guān)和系統(tǒng)管理微服務(wù)(prong-system-api)都配置為了OAuth2的資源服務(wù)器(resource server)。我們?cè)谫Y源服務(wù)器的解析JWT的access token時(shí)將相關(guān)的用戶信息存入了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;
    }

}

這時(shí)候,問題來了。我們發(fā)現(xiàn)在用戶張三登錄后,李四訪問網(wǎng)關(guān)一個(gè)不需要用戶認(rèn)證的api時(shí)(這時(shí)資源服務(wù)器并不會(huì)解析token并存入ThreadLocal),網(wǎng)關(guān)從ThreadLocal取到的當(dāng)前用戶居然是張三!而且出現(xiàn)的頻率是隨機(jī)的。

這是什么原因呢?

問題原因

spring boot內(nèi)嵌了tomcat web服務(wù)器,而一般的web服務(wù)器對(duì)于每個(gè)http請(qǐng)求,會(huì)開設(shè)一個(gè)線程用于處理請(qǐng)求,為了提高響應(yīng)速度,web服務(wù)器一般都會(huì)配置啟用一個(gè)線程池,所以線程池中的線程,都會(huì)存在復(fù)用的可能。

這時(shí),如果我們使用ThreadLocal來在線程內(nèi)共享數(shù)據(jù)時(shí),當(dāng)線程處理結(jié)束后,沒有從ThreadLocal剔除數(shù)據(jù)時(shí),可能存在數(shù)據(jù)被竄用的可能,更嚴(yán)重的導(dǎo)致內(nèi)存泄露(見:http://my.oschina.net/ainilife/blog/261297)。

分析和解決

我們觀察資源服務(wù)器啟動(dòng)時(shí)的debug日志,發(fā)現(xiàn)spring創(chuàng)建了兩個(gè)過濾器處理鏈:

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

在這個(gè)過濾器的前面插入我們自定義的過濾器,相關(guān)代碼:

@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();
        }
    }
    
}

測(cè)試后問題解決。

參考:
ThreadLocal在應(yīng)用中,因服務(wù)器線程復(fù)用導(dǎo)致問題
當(dāng)ThreadLocal碰上線程池
Writing a Custom Filter in Spring Security
對(duì)Java 過濾器、攔截器、監(jiān)聽器在Spring MVC中應(yīng)用場(chǎng)景的探究
Spring Cloud內(nèi)置的Zuul過濾器詳解
Spring Cloud OAuth2 認(rèn)證流程

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容