目标
客户端调用刷新令牌接口时,不再沿用首次登录时的旧用户数据,而是根据 Refresh Token 中关联的用户 ID 实时查询数据库,获取最新的角色、权限等信息,并生成新的 access_token、refresh_token 和 id_token ,同时保持 OIDC 会话的一致性(如 auth_time、sub 等字段)。
为什么默认刷新逻辑不够用
Spring Authorization Server 内置的 OAuth2RefreshTokenAuthenticationProvider 在刷新时:
- 直接复用旧的
OAuth2Authorization中保存的Principal对象(即首次登录时的Authentication); - 用户权限、角色变更后,刷新得到的令牌仍然包含旧信息,必须重新登录才能生效。
这无法满足"权限实时生效"的安全要求。
自定义刷新提供者核心逻辑
新建 CustomRefreshTokenAuthenticationProvider 并实现 AuthenticationProvider,替换默认实现。
关键步骤
- 校验客户端和 Refresh Token 的有效性。
- 从数据库查出旧的
OAuth2Authorization。 - 从旧授权中提取
userId(通过Principal属性)。 - 调用
SysUserDetailsService.loadUserByUserId(userId)加载最新用户信息。 - 用新的
SysUserDetails构建新的UsernamePasswordAuthenticationToken。 - 构建
DefaultOAuth2TokenContext,特别注意 ID Token 上下文必须传入旧的OAuth2Authorization,否则JwtGenerator会因为无法读取旧的 ID Token 而抛出NullPointerException。 - 生成新的
access_token、refresh_token、id_token。 - 持久化新的授权记录,并建议废除旧的 Refresh Token。
代码骨架(仅保留刷新关键部分)
bash
@Override
public Authentication authenticate(Authentication authentication) {
OAuth2RefreshTokenAuthenticationToken refreshAuth = (OAuth2RefreshTokenAuthenticationToken) authentication;
// 1. 客户端校验
OAuth2ClientAuthenticationToken clientPrincipal = ...;
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
// 2. 查找旧授权
OAuth2Authorization oldAuth = authorizationService.findByToken(
refreshAuth.getRefreshToken(), OAuth2TokenType.REFRESH_TOKEN);
if (oldAuth == null) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
}
// 3. 提取 userId 并加载最新用户信息
Principal oldPrincipal = oldAuth.getAttribute(Principal.class.getName());
Authentication newPrincipal = loadLatestPrincipal(oldPrincipal);
// 4. 构建 token 上下文
Set<String> scopes = oldAuth.getAuthorizedScopes();
DefaultOAuth2TokenContext.Builder ctxBuilder = DefaultOAuth2TokenContext.builder()
.registeredClient(registeredClient)
.principal(newPrincipal)
.authorizationServerContext(AuthorizationServerContextHolder.getContext())
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.authorizationGrant(refreshAuth)
.authorizedScopes(scopes);
// 5. 生成新令牌
OAuth2AccessToken newAccessToken = createAccessToken(ctxBuilder);
OAuth2RefreshToken newRefreshToken = createRefreshToken(ctxBuilder);
// 6. 构建新授权并添加 ID Token
OAuth2Authorization.Builder newAuthBuilder = OAuth2Authorization.withRegisteredClient(registeredClient)
.principalName(newPrincipal.getName())
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.authorizedScopes(scopes)
.attribute(Principal.class.getName(), newPrincipal)
.token(newAccessToken)
.refreshToken(newRefreshToken);
addIdToken(newAuthBuilder, ctxBuilder, scopes, oldAuth); // 必须传入 oldAuth
OAuth2Authorization newAuth = newAuthBuilder.build();
authorizationService.save(newAuth);
// 7. 废弃旧 Refresh Token(可选但推荐)
// authorizationService.remove(oldAuth);
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, newAccessToken, newRefreshToken);
}
关键点:ID Token 生成必须传入旧授权
bash
private void addIdToken(OAuth2Authorization.Builder authBuilder,
DefaultOAuth2TokenContext.Builder ctxBuilder,
Set<String> scopes,
OAuth2Authorization oldAuth) {
if (scopes.contains(OidcScopes.OPENID)) {
OAuth2TokenContext idTokenCtx = ctxBuilder
.tokenType(new OAuth2TokenType(OidcParameterNames.ID_TOKEN))
.authorization(oldAuth) // 必须设置,否则 JwtGenerator NPE
.build();
OAuth2Token generated = tokenGenerator.generate(idTokenCtx);
OidcIdToken oidcIdToken = toOidcIdToken(generated); // 处理 Jwt 转换
if (oidcIdToken != null) {
authBuilder.token(oidcIdToken, (meta) ->
meta.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, oidcIdToken.getClaims()));
}
}
}
原因 :JwtGenerator 在生成 ID Token 时会调用 context.getAuthorization().getToken(OidcIdToken.class) 来获取旧的 ID Token(以保持 auth_time 等字段一致)。如果不传入旧授权,则此处为 null,引发 NullPointerException。
配置替换默认提供者
在 AuthorizationServerConfig 中修改 tokenEndpoint 的认证提供者列表:
bash
tokenEndpoint.authenticationProviders(providers -> {
providers.removeIf(p -> p instanceof OAuth2RefreshTokenAuthenticationProvider);
providers.add(new CustomRefreshTokenAuthenticationProvider(
authorizationService, tokenGenerator, sysUserDetailsService));
// ... 其他自定义提供者
});
前提依赖
- 首次登录时 ID Token 已正确保存 到
OAuth2Authorization表中(否则刷新时无法获取旧的 ID Token,仍会 NPE)。 - Jackson 反序列化需允许
Long等自定义类型(若遇到allowlist异常,需添加SecurityJackson2Modules.allowListClass(Long.class))。
验证方法
- 使用自定义 grant_type(如
phone_password)登录,获取refresh_token。 - 修改数据库中该用户的角色/权限(不重新登录)。
- 用
refresh_token调用/oauth2/token,scope 包含openid。 - 解码新
access_token的 JWT,确认其中包含最新的权限信息。 - 确认旧
refresh_token已失效(若实现了失效逻辑)。
总结
通过自定义 RefreshTokenAuthenticationProvider 替换默认实现,并在生成 ID Token 时强制传入旧的 OAuth2Authorization,彻底解决了:
- 刷新后用户权限不更新
- 刷新时
JwtGenerator的空指针异常
使得 OAuth2/OIDC 刷新流程在自定义授权模式下能够稳定、安全地运行,真正实现"权限实时生效"。