Spring Resource Server响应时校验过期JWT返回401问题求助
我之前在维护基于Spring Boot 1.5和Spring Security OAuth的项目时,完全遇到过一模一样的问题!当时排查了好久才找到根源和解决办法,跟你分享下:
问题根源
Spring Security OAuth的ResourceServerFilter默认会在整个请求生命周期(包括请求进入和响应返回两个阶段)检查认证状态。当你的请求处理耗时超过JWT的30秒TTL时,响应阶段SecurityContext里的认证信息已经过期失效,框架就会触发401拦截,把原本的200响应替换掉。这个设计确实不符合业务逻辑——毕竟请求发起时JWT是有效的,就应该允许正常返回结果。
解决方案(适配Spring Boot 1.5.x版本)
方案1:缓存请求初期的有效认证信息
自定义ResourceServerTokenServices,在请求进入时校验JWT并缓存认证结果,响应阶段直接复用缓存的有效认证信息,不再重新校验JWT:
import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.security.oauth2.provider.token.DefaultTokenServices; import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; public class CachedAuthTokenServices extends DefaultTokenServices implements ResourceServerTokenServices { private final ConcurrentHashMap<String, OAuth2Authentication> authCache = new ConcurrentHashMap<>(); private final long jwtTtlSeconds; public CachedAuthTokenServices(long jwtTtlSeconds) { this.jwtTtlSeconds = jwtTtlSeconds; } @Override public OAuth2Authentication loadAuthentication(String accessToken) { // 优先读取缓存的认证信息 OAuth2Authentication cachedAuth = authCache.get(accessToken); if (cachedAuth != null) { return cachedAuth; } // 首次校验JWT,获取有效认证信息 OAuth2Authentication validAuth = super.loadAuthentication(accessToken); // 存入缓存,并设置自动过期清理(和JWT TTL一致) authCache.put(accessToken, validAuth); new Thread(() -> { try { TimeUnit.SECONDS.sleep(jwtTtlSeconds); authCache.remove(accessToken); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }).start(); return validAuth; } }
然后在资源服务器配置中替换默认的TokenServices:
@Configuration @EnableResourceServer public class ResourceServerConfig extends ResourceServerConfigurerAdapter { @Value("${jwt.ttl.seconds:30}") private long jwtTtlSeconds; @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { CachedAuthTokenServices tokenServices = new CachedAuthTokenServices(jwtTtlSeconds); // 绑定你的JWT解析配置(比如JwtAccessTokenConverter) tokenServices.setTokenStore(jwtTokenStore()); resources.tokenServices(tokenServices); } // 以下是你的原有JWT配置,根据实际情况调整 @Bean public TokenStore jwtTokenStore() { return new JwtTokenStore(jwtAccessTokenConverter()); } @Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); converter.setSigningKey("your-jwt-signing-key"); // 替换成你的密钥/公钥 return converter; } }
方案2:让认证校验只在请求入口执行
自定义过滤器包装类,拦截默认的安全过滤器,只在请求进入阶段执行认证校验,响应阶段直接跳过:
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; public class RequestOnlyAuthFilter extends OncePerRequestFilter { private final FilterSecurityInterceptor delegateFilter; public RequestOnlyAuthFilter(FilterSecurityInterceptor delegateFilter) { this.delegateFilter = delegateFilter; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 只在请求首次进入时执行认证,转发/包含场景(响应阶段)直接跳过 if (!request.getDispatcherType().isForward() && !request.getDispatcherType().isInclude()) { delegateFilter.doFilter(request, response, filterChain); } else { filterChain.doFilter(request, response); } } }
在资源服务器配置中替换原有过滤器:
@Configuration @EnableResourceServer public class ResourceServerConfig extends ResourceServerConfigurerAdapter { @Autowired private FilterSecurityInterceptor filterSecurityInterceptor; @Override public void configure(HttpSecurity http) throws Exception { // 用自定义过滤器替换默认的安全拦截器 http.addFilterBefore(new RequestOnlyAuthFilter(filterSecurityInterceptor), FilterSecurityInterceptor.class); // 你的其他权限配置 http.authorizeRequests() .anyRequest().authenticated(); } }
注意事项
- 方案1更稳妥,因为它保留了JWT的有效期校验逻辑,只是避免了重复校验;
- 方案2需要注意Spring Security 4.x(对应Spring Boot 1.5)的过滤器顺序,测试时要确保不会影响其他安全逻辑;
- 如果你的项目中有异步请求处理,需要额外处理SecurityContext的传递,避免缓存失效。
内容的提问来源于stack exchange,提问作者Arnould
相关产品推荐
相关产品推荐

