刷新JWT令牌时如何妥善处理用户权限更新问题?
解决JWT权限更新不及时的优雅方案
问题回顾
你当前通过OAuth2TokenCustomizer将用户权限写入JWT的authorities claim,但刷新令牌时,Authentication对象携带的是旧权限,导致新权限无法同步。直接吊销令牌影响体验,每次刷新查数据库又不够优雅,以下是几种更合理的处理方式:
方案1:缩短Access Token有效期 + 自动刷新
核心思路是让Access Token的有效期足够短(比如5-15分钟),这样权限变更后,用户最多等待令牌过期,就能通过Refresh Token自动获取包含最新权限的新令牌,无需手动重新登录。
Spring Security配置示例:
@Bean fun jwtDecoder(jwkSource: JwkSource<SecurityContext>): JwtDecoder { val jwtDecoder = JwtDecoders.fromIssuerLocation("http://your-auth-server") as NimbusJwtDecoder jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer("http://your-auth-server")) return jwtDecoder } @Bean fun tokenSettings(): TokenSettings { return TokenSettings.builder() .accessTokenTimeToLive(Duration.ofMinutes(10)) // 设置短有效期 .refreshTokenTimeToLive(Duration.ofDays(7)) .build() }
优点:
- 无需修改现有令牌生成逻辑,仅调整配置即可
- 用户无感知,自动同步最新权限
- 降低令牌被盗用的安全风险
缺点:
- 权限变更存在最多10分钟的延迟,适合对实时性要求不是极高的场景
方案2:权限校验实时查询,JWT仅存用户标识
放弃在JWT中存储authorities,只保留userId等唯一标识,每次接口权限校验时,从缓存/数据库实时获取最新权限。
实现示例:
- 调整JWT自定义器,仅写入用户ID:
@Bean fun jwtCustomizer(): OAuth2TokenCustomizer<JwtEncodingContext> { return OAuth2TokenCustomizer { context -> val principal = context.principal val user = principal.principal as UserDetailsImpl context.claims.claim("userId", user.userId) // 不再写入authorities } }
- 自定义权限校验逻辑,结合缓存获取最新权限:
@Component class CustomPermissionEvaluator : PermissionEvaluator { @Autowired lateinit var userDetailsRepository: UserDetailsRepository @Autowired lateinit var cacheManager: CacheManager override fun hasPermission(authentication: Authentication?, targetDomainObject: Any?, permission: Any?): Boolean { val userId = (authentication?.principal as Jwt).claims["userId"] as String // 优先从缓存取权限,缓存失效时查询数据库 val userCache = cacheManager.getCache("userPermissions") ?: throw RuntimeException("权限缓存未配置") var authorities = userCache.get(userId, MutableList::class.java) as? MutableList<GrantedAuthority> if (authorities == null) { val user = userDetailsRepository.findById(userId).orElseThrow() authorities = user.authorities.toMutableList() userCache.put(userId, authorities) } return authorities.any { it.authority == permission.toString() } } }
- 接口上使用
@PreAuthorize做权限校验:
@GetMapping("/admin") @PreAuthorize("hasPermission(null, 'ADMIN')") fun adminEndpoint(): String { return "Admin content" }
优点:
- 权限变更实时生效,无延迟
- JWT体积更小,传输效率更高
缺点:
- 每次权限校验都要访问缓存/数据库,需做好缓存优化(比如权限变更时主动清空对应缓存)
方案3:引入权限版本号,校验时比对版本
在用户表新增permission_version字段,每次权限变更时版本号自增。JWT中写入该版本号,后端校验令牌时,对比数据库中的最新版本号:如果JWT版本号落后,则拒绝请求,引导用户用Refresh Token重新获取令牌。
实现步骤:
- 用户实体添加版本字段:
@Entity class User( // 其他字段 @Column(nullable = false) var permissionVersion: Int = 1 )
- 权限变更时更新版本号:
@Service class UserService { fun updateUserPermissions(userId: String, newAuthorities: List<GrantedAuthority>) { val user = userDetailsRepository.findById(userId).orElseThrow() user.authorities = newAuthorities user.permissionVersion += 1 // 版本自增 userDetailsRepository.save(user) // 清空对应缓存(如果使用了缓存) } }
- JWT自定义器写入版本号:
@Bean fun jwtCustomizer(): OAuth2TokenCustomizer<JwtEncodingContext> { return OAuth2TokenCustomizer { context -> val principal = context.principal val user = principal.principal as UserDetailsImpl context.claims.claim("userId", user.userId) context.claims.claim("permVersion", user.permissionVersion) context.claims.claim("authorities", user.authorities) } }
- 自定义JWT校验逻辑,比对版本:
@Bean fun jwtDecoder(jwkSource: JwkSource<SecurityContext>, userDetailsRepository: UserDetailsRepository): JwtDecoder { val jwtDecoder = JwtDecoders.fromIssuerLocation("http://your-auth-server") as NimbusJwtDecoder jwtDecoder.setJwtValidator(object : OAuth2TokenValidator<Jwt> { override fun validate(token: Jwt): OAuth2TokenValidatorResult { val userId = token.claims["userId"] as String val tokenVersion = token.claims["permVersion"] as Int val user = userDetailsRepository.findById(userId).orElseThrow() return if (user.permissionVersion > tokenVersion) { OAuth2TokenValidatorResult.failure(OAuth2Error("INVALID_TOKEN", "权限已更新,请重新获取令牌", null)) } else { OAuth2TokenValidatorResult.success() } } }) return jwtDecoder }
优点:
- 权限变更后,旧令牌立即失效,实时性强
- 无需频繁查库,仅在校验时比对版本
- 用户只需用Refresh Token重新获取令牌,无需重新登录
缺点:
- 需要修改用户实体和权限变更逻辑
- 增加了令牌校验的复杂度
内容的提问来源于stack exchange,提问作者Patrick
相关产品推荐
相关产品推荐

