問題
在我們產(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)證流程