如何在基于OAuth的Spring Boot应用中设置闲置会话超时
嘿,这个需求太常见了——固定的Token过期时间要么不够灵活(太短导致用户频繁登录,太长又有安全风险),闲置超时才是更贴合用户体验的方案。下面分两种最常用的OAuth2场景给你拆解实现方式:
1. 基于存储的令牌(Redis/JDBC 存储令牌)
如果你的OAuth2是把Access Token和Refresh Token存在后端存储(比如Redis)里,而不是用无状态的JWT,那实现闲置超时会更直接,因为我们可以跟踪每个令牌的最后活跃时间。
实现步骤:
步骤1:记录令牌的活跃时间
在令牌生成时,把令牌的最后活跃时间存入存储;同时在每次用户用令牌发起请求时,更新这个时间。我们可以用一个拦截器来做这件事:@Component public class TokenActivityInterceptor implements HandlerInterceptor { @Autowired private StringRedisTemplate redisTemplate; @Value("${security.oauth2.idle-timeout:1800}") // 30分钟,单位秒 private long idleTimeoutSeconds; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = extractTokenFromHeader(request); if (token != null) { String tokenKey = "oauth:active:" + token; // 更新最后活跃时间 redisTemplate.opsForHash().put(tokenKey, "lastActive", String.valueOf(System.currentTimeMillis())); // 重置令牌的闲置超时时间 redisTemplate.expire(tokenKey, idleTimeoutSeconds, TimeUnit.SECONDS); } return true; } private String extractTokenFromHeader(HttpServletRequest request) { String authHeader = request.getHeader("Authorization"); if (StringUtils.hasText(authHeader) && authHeader.startsWith("Bearer ")) { return authHeader.substring(7); } return null; } }别忘了把拦截器注册到Spring MVC里:
@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Autowired private TokenActivityInterceptor activityInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(activityInterceptor) .addPathPatterns("/api/**"); // 只拦截你的业务接口 } }步骤2:校验令牌时检查闲置时间
自定义TokenServices,在读取令牌的时候,判断当前时间和最后活跃时间的间隔是否超过闲置阈值,如果超过就作废令牌:@Service public class CustomTokenServices extends DefaultTokenServices { @Autowired private StringRedisTemplate redisTemplate; @Value("${security.oauth2.idle-timeout:1800}") private long idleTimeoutSeconds; @Override public OAuth2AccessToken readAccessToken(String tokenValue) { OAuth2AccessToken token = super.readAccessToken(tokenValue); if (token != null && !token.isExpired()) { String tokenKey = "oauth:active:" + tokenValue; String lastActiveStr = (String) redisTemplate.opsForHash().get(tokenKey, "lastActive"); if (lastActiveStr != null) { long lastActive = Long.parseLong(lastActiveStr); long idleDuration = (System.currentTimeMillis() - lastActive) / 1000; if (idleDuration > idleTimeoutSeconds) { // 闲置超时,作废令牌 revokeToken(tokenValue); throw new InvalidTokenException("Token has been idle for too long"); } } } return token; } }
2. 无状态JWT令牌场景
JWT本身是无状态的,令牌里的过期时间是硬编码的,没法直接修改。这种情况下我们需要结合Refresh Token和后端存储来实现闲置超时:
核心思路:
- 把Access Token的有效期设短一点(比如15分钟),Refresh Token设长一点,但把Refresh Token存在后端(Redis)并跟踪它的最后活跃时间。
- 用户每次操作时,前端自动用Refresh Token刷新Access Token(可以在Access Token快过期时触发),后端刷新时更新Refresh Token的活跃时间。
- 如果用户长时间不操作,Refresh Token的闲置时间超过阈值,后端就拒绝刷新请求,强制用户重新登录。
关键代码实现:
自定义TokenServices来处理Refresh Token的闲置校验:
@Service public class CustomJwtTokenServices extends DefaultTokenServices { @Autowired private StringRedisTemplate redisTemplate; @Value("${security.oauth2.refresh-idle-timeout:3600}") // 1小时闲置超时 private long refreshIdleTimeoutSeconds; @Override public OAuth2AccessToken refreshAccessToken(String refreshTokenValue, TokenRequest tokenRequest) throws AuthenticationException { // 先检查Refresh Token的闲置状态 String refreshKey = "oauth:refresh:active:" + refreshTokenValue; String lastActiveStr = (String) redisTemplate.opsForHash().get(refreshKey, "lastActive"); if (lastActiveStr != null) { long lastActive = Long.parseLong(lastActiveStr); long idleDuration = (System.currentTimeMillis() - lastActive) / 1000; if (idleDuration > refreshIdleTimeoutSeconds) { revokeToken(refreshTokenValue); throw new InvalidTokenException("Refresh token has been idle for too long"); } } // 更新Refresh Token的活跃时间并重置过期时间 redisTemplate.opsForHash().put(refreshKey, "lastActive", String.valueOf(System.currentTimeMillis())); redisTemplate.expire(refreshKey, refreshIdleTimeoutSeconds + 3600, TimeUnit.SECONDS); // 总有效期比闲置超时久一点 return super.refreshAccessToken(refreshTokenValue, tokenRequest); } }
额外注意事项
- 闲置超时和固定过期时间要配合:比如闲置超时设30分钟,固定过期时间设2小时,这样即使用户一直操作,2小时后还是需要重新登录,兼顾安全和体验。
- 前端要做好异常处理:收到401响应时,自动跳转到登录页。
- 分布式系统下,一定要用共享存储(比如Redis)来存令牌的活跃状态,不然不同节点的状态会不一致。
内容的提问来源于stack exchange,提问作者Ramesh Naidu
相关产品推荐
相关产品推荐

