
本文介绍在 spring security oauth2 客户端中,当授权服务器不支持刷新令牌时,如何检测并主动淘汰已过期的 access token,通过自定义过滤器触发用户重新登录,确保资源访问的安全性与有效性。
本文介绍在 spring security oauth2 客户端中,当授权服务器不支持刷新令牌时,如何检测并主动淘汰已过期的 access token,通过自定义过滤器触发用户重新登录,确保资源访问的安全性与有效性。
在基于 Spring Security 的 OAuth2 客户端应用中,OAuth2AuthorizedClientService 会将获取到的 OAuth2AccessToken 缓存至服务端(如 InMemoryOAuth2AuthorizedClientService 或 JdbcOAuth2AuthorizedClientService),默认不会自动校验其时效性。一旦 Access Token 过期(例如 1 小时后),后续对受保护资源的调用将因 401 Unauthorized 失败——而此时 client.getAccessToken() 仍返回已失效的旧令牌,导致业务逻辑持续出错。
为解决该问题,不能依赖客户端缓存的令牌生命周期管理,而需在请求入口层主动拦截并验证令牌状态。以下是一种轻量、可控且符合 Spring Security 设计理念的实现方式:
✅ 推荐方案:自定义 TokenValidationFilter
该过滤器应在安全链中早于资源访问逻辑执行(例如置于 SecurityWebFilterChain 的前置位置),用于检查当前认证上下文中的 Access Token 是否已过期,并在过期时引导用户重新发起 OAuth2 授权流程。
⚠️ 注意:Spring Security 5.7+ 已弃用
OAuth2ClientContext(属于旧版spring-security-oauth2-client早期 API),现代项目应使用OAuth2AuthorizedClientService+Authentication主体进行令牌管理。因此,原始答案中基于OAuth2ClientContext的实现不适用于 Spring Security 5.7+(即 Boot 2.7+ / 3.x),需升级适配。
✅ 正确实现(适配 Spring Security 5.7+)
@Component
@Order(Ordered.HIGHEST_PRECEDENCE + 10)
public class StaleTokenRedirectFilter implements Filter {
private final OAuth2AuthorizedClientService authorizedClientService;
private final ClientRegistrationRepository clientRegistrationRepository;
public StaleTokenRedirectFilter(OAuth2AuthorizedClientService authorizedClientService,
ClientRegistrationRepository clientRegistrationRepository) {
this.authorizedClientService = authorizedClientService;
this.clientRegistrationRepository = clientRegistrationRepository;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth == null || !auth.isAuthenticated()) {
chain.doFilter(request, response);
return;
}
// 获取当前 OAuth2 注册名(通常与 login 页面配置的 client-id 对应)
String registrationId = resolveRegistrationId(httpRequest);
if (registrationId == null) {
chain.doFilter(request, response);
return;
}
OAuth2AuthorizedClient authorizedClient =
authorizedClientService.loadAuthorizedClient(registrationId, auth.getName());
if (authorizedClient != null && isAccessTokenExpired(authorizedClient.getAccessToken())) {
// 清除过期令牌(可选:提升安全性)
authorizedClientService.removeAuthorizedClient(registrationId, auth.getName());
// 重定向至 OAuth2 登录入口(触发新 authorization code flow)
String loginUrl = "/oauth2/authorization/" + registrationId;
httpResponse.sendRedirect(httpRequest.getContextPath() + loginUrl);
return;
}
chain.doFilter(request, response);
}
private boolean isAccessTokenExpired(OAuth2AccessToken token) {
return token != null && token.getExpiresAt() != null && token.getExpiresAt().isBefore(Instant.now());
}
private String resolveRegistrationId(HttpServletRequest request) {
// 方式一:从当前请求路径或 session 推断(如 /api/user → 默认 client)
// 方式二:推荐 —— 在登录成功后将 registrationId 存入 SecurityContext 或 Session
// 此处简化:假设单一 OAuth2 客户端,取第一个注册项
return clientRegistrationRepository
.getRegistrations()
.values()
.stream()
.findFirst()
.map(ClientRegistration::getRegistrationId)
.orElse(null);
}
}? 配置说明
- 确保该过滤器被注册进
SecurityFilterChain(Spring Boot 自动扫描@Component过滤器,但需注意@Order优先级); - 若应用支持多身份提供商(如 Google + GitHub),建议在登录页或
AuthenticationSuccessHandler中显式将registrationId绑定到SecurityContext或HttpSession,避免resolveRegistrationId()猜测失败; - 可配合
HttpSessionEventPublisher监听会话销毁,进一步清理OAuth2AuthorizedClient缓存。
? 补充建议:增强健壮性
-
前端兜底:API 响应返回
401时,前端统一跳转/oauth2/authorization/{id}; -
静默续签(如支持):若未来 IDP 支持 PKCE + Refresh Token,可改用
RefreshTokenOAuth2AuthorizedClientProvider; - 日志审计:在过滤器中记录令牌过期事件,便于监控用户活跃度与令牌轮换频率。
通过上述方案,你无需重启应用即可实现 Access Token 的“按需淘汰”与用户无感重认证,既规避了长期持有失效令牌的风险,又完全兼容无 Refresh Token 的 OIDC 提供商(如部分政务、金融类平台)。

















