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

刷新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等唯一标识,每次接口权限校验时,从缓存/数据库实时获取最新权限。

实现示例:

  1. 调整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
    }
}
  1. 自定义权限校验逻辑,结合缓存获取最新权限:
@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() }
    }
}
  1. 接口上使用@PreAuthorize做权限校验:
@GetMapping("/admin")
@PreAuthorize("hasPermission(null, 'ADMIN')")
fun adminEndpoint(): String {
    return "Admin content"
}

优点:

  • 权限变更实时生效,无延迟
  • JWT体积更小,传输效率更高

缺点:

  • 每次权限校验都要访问缓存/数据库,需做好缓存优化(比如权限变更时主动清空对应缓存)

方案3:引入权限版本号,校验时比对版本

在用户表新增permission_version字段,每次权限变更时版本号自增。JWT中写入该版本号,后端校验令牌时,对比数据库中的最新版本号:如果JWT版本号落后,则拒绝请求,引导用户用Refresh Token重新获取令牌。

实现步骤:

  1. 用户实体添加版本字段:
@Entity
class User(
    // 其他字段
    @Column(nullable = false)
    var permissionVersion: Int = 1
)
  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)
        // 清空对应缓存(如果使用了缓存)
    }
}
  1. 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)
    }
}
  1. 自定义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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.30 08:35:36