Spring Boot OAuth2:用户修改密码时刷新令牌过期方案咨询
解决方案:JWT模式下密码变更失效Refresh Token + 安全优化
先梳理下你的核心诉求:
- 无令牌存储(JWT模式)下,实现修改密码时自动失效所有旧Refresh Token
- 优化长有效期Refresh Token的安全风险,改为45天强制客户端更新
一、修改密码失效Refresh Token的无状态方案
JWT本身是无状态、不可篡改的,没法主动撤销已签发的Token,所以我们可以通过**添加「密码校验标识」**的方式实现被动失效:
1. 给用户表增加密码版本字段
在你的用户实体中新增一个password_version整数字段(初始值设为1),每次用户修改密码时,将该字段自增:
// 用户修改密码的服务方法示例 public void changePassword(Long userId, String newPassword) { User user = userRepository.findById(userId).orElseThrow(() -> new RuntimeException("用户不存在")); user.setPassword(passwordEncoder.encode(newPassword)); user.setPasswordVersion(user.getPasswordVersion() + 1); // 版本号自增 userRepository.save(user); }
2. 把密码版本嵌入JWT Claims中
修改JwtAccessTokenConverter,在生成Token时将密码版本号加入到Claims里,确保每次签发的Refresh Token都携带当前的密码版本:
@Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { final JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); converter.setSigningKey(env.getProperty("jwt.secret")); // 添加自定义Token增强逻辑,注入密码版本号 converter.setAccessTokenConverter(new DefaultAccessTokenConverter() { @Override public Map<String, ?> convertAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) { Map<String, Object> claims = (Map<String, Object>) super.convertAccessToken(token, authentication); // 从自定义UserDetails中获取密码版本(需要你的CustomUserDetails实现包含该字段) CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal(); claims.put("pwd_version", userDetails.getPasswordVersion()); return claims; } }); return converter; }
3. 验证Refresh Token时校验版本号
在Refresh Token的校验环节,对比Token中的pwd_version和用户数据库中的当前版本,不一致则拒绝刷新:
@Component public class RefreshTokenValidationFilter extends OncePerRequestFilter { @Autowired private JwtAccessTokenConverter jwtAccessTokenConverter; @Autowired private UserRepository userRepository; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 只针对refresh_token类型的令牌刷新请求校验 if ("/oauth/token".equals(request.getRequestURI()) && "refresh_token".equals(request.getParameter("grant_type"))) { String refreshToken = request.getParameter("refresh_token"); if (refreshToken != null) { try { // 解析Refresh Token的Claims Map<String, Object> claims = jwtAccessTokenConverter.decode(refreshToken); String username = (String) claims.get("user_name"); Integer tokenPwdVersion = (Integer) claims.get("pwd_version"); User user = userRepository.findByUsername(username).orElseThrow(); if (!user.getPasswordVersion().equals(tokenPwdVersion)) { // 密码已变更,拒绝刷新请求 response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Refresh token invalid due to password change"); return; } } catch (Exception e) { response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid refresh token"); return; } } } filterChain.doFilter(request, response); } }
记得把这个过滤器注册到Spring Security的过滤链中,确保校验逻辑生效。
二、优化Refresh Token有效期为45天
你可以通过配置DefaultTokenServices来统一设置令牌有效期,在AuthorizationServer配置中添加:
@Configuration @EnableAuthorizationServer protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { // ... 其他已有的Autowired依赖 @Bean public DefaultTokenServices tokenServices() { DefaultTokenServices services = new DefaultTokenServices(); services.setSupportRefreshToken(true); services.setRefreshTokenValiditySeconds(45 * 24 * 3600); // 设置为45天 services.setAccessTokenValiditySeconds(30 * 24 * 3600); // 保留原有的30天access token有效期 services.setTokenEnhancer(jwtAccessTokenConverter); return services; } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints .authenticationManager(authenticationManager) .userDetailsService(userDetailsService) .accessTokenConverter(jwtAccessTokenConverter) .tokenServices(tokenServices()); // 绑定自定义的Token服务配置 } // ... 其他原有配置 }
如果你是通过JDBC存储客户端信息,也可以直接修改数据库oauth_client_details表的refresh_token_validity字段,设置值为45*86400。
三、关于之前JdbcTokenStore的「Invalid refresh token」问题
之前出现这个错误大概率是因为AuthorizationServer和ResourceServer的TokenStore配置不一致,或者令牌的序列化/反序列化出现异常。如果后续想换回有状态存储,需要确保:
- 两端使用同一个
TokenStore实例 - 数据库
oauth_refresh_token表结构符合Spring Security OAuth的默认要求 - 避免手动修改数据库中的令牌记录
四、额外安全建议
- 客户端存储Refresh Token时,Web端务必用
HttpOnly+Secure的Cookie,移动端用系统密钥库(Keychain/Keystore)存储 - 可以考虑给Refresh Token添加用户IP绑定(写入Claims),进一步降低令牌泄露后的风险
- 定期清理客户端本地过期的Refresh Token,避免无效请求
内容的提问来源于stack exchange,提问作者SheppardDigital
相关产品推荐
相关产品推荐

