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

Spring Boot OAuth2:用户修改密码时刷新令牌过期方案咨询

解决方案:JWT模式下密码变更失效Refresh Token + 安全优化

先梳理下你的核心诉求:

  1. 无令牌存储(JWT模式)下,实现修改密码时自动失效所有旧Refresh Token
  2. 优化长有效期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配置不一致,或者令牌的序列化/反序列化出现异常。如果后续想换回有状态存储,需要确保:

  1. 两端使用同一个TokenStore实例
  2. 数据库oauth_refresh_token表结构符合Spring Security OAuth的默认要求
  3. 避免手动修改数据库中的令牌记录

四、额外安全建议

  • 客户端存储Refresh Token时,Web端务必用HttpOnly+Secure的Cookie,移动端用系统密钥库(Keychain/Keystore)存储
  • 可以考虑给Refresh Token添加用户IP绑定(写入Claims),进一步降低令牌泄露后的风险
  • 定期清理客户端本地过期的Refresh Token,避免无效请求

内容的提问来源于stack exchange,提问作者SheppardDigital

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.15 07:46:36