You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.21 04:28:12