Springboot3+OAuth2.1实现密码模式认证续:补全刷新令牌逻辑
上一篇文章发出后,有小伙伴在评论区说没法刷新令牌,这在最早一个版本中的确是个问题,修复后一直没时间来更新文章,直到现在才抽空补上,实在罪过,给各位同学鞠躬了~
OAuth2AuthorizationService接口
首先说一下这个接口,它用于对oauth的授权信息进行管理。在oauth2的框架中,它起到了一个十分重要的作用,即框架完成认证后,需要将相应的认证信息(包括但不限于客户端id、授权方式、授权范围、授权码、accessToken相关、oicdToken相关、refreshToken相关等)存储起来,以便在后面使用。
它有两个实现类:InMemoryOAuth2AuthorizationService和JdbcOAuth2AuthorizationService,分别对应将信息存储到内存中或存储到数据库中。在开发过程中要结合项目实际情况选择用哪个实现类。
为什么开头就要提到这个接口呢?因为当我们刷新令牌的时候,oauth需要去找是否存储过这个refreshToken,只有匹配上的时候才认为你传入的refreshToken是有效的。
CustomProviderTokenGenerator工具类修复
上篇文章中,我们自己手搓了一个CustomProviderTokenGenerator工具类,用于在框架以外的地方根据我们自己的需求去生成token。后来发现这个类有几个很严重的bug,主要还是源于当时对oauth2.1框架的机制了解不深,这里依次讲一下,最后放出一份完整的代码。
1. attribute设置错误
// 2. 创建 OAuth2Authorization(持久化授权记录)
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient)
.principalName(userDetails.getUsername())
.authorizationGrantType(token.getAuthorizationGrantType())
.authorizedScopes(scopes)
.attribute(Principal.class.getName(), userDetails);
以前的代码开头有这么一段儿,将一个UserDetails对象存入了OAuth2Authorization的attribute中,其key是Principal.class.getName()。
在刷新令牌的过程中,框架会取这个attribute。由于UserDetails不是它期望的类型,因此此处会抛出ClassCastException。实际上这里应该放进去一个包含UserDetails的Authentication类型的对象,正确的方式是:
Authentication userAuthentication = new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());
// 创建 OAuth2Authorization(持久化授权记录)
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient)
.principalName(userDetails.getUsername())
.authorizationGrantType(token.getAuthorizationGrantType())
.authorizedScopes(scopes)
.attribute(Principal.class.getName(), userAuthentication);
2. 生成的accessToken和refreshToken没交给OAuth2AuthorizationService管理
说白了,生成完token就直接扔给前端了。由于这套token由oauth按正常流程签发,其秘钥也符合要求,因此直接使用是没问题的。但refresh的时候,oauth不知道你传过来的refreshToken是否合法,因此直接扔出来个401 status。
因此,正确的姿势应该是生成完token保存一下:
authorization = OAuth2Authorization.from(authorization)
.accessToken(accessToken)
.refreshToken(refreshToken).build();
authorizationService.save(authorization);
3. 完整代码
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.core.OAuth2Token;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
import java.security.Principal;
import java.time.Instant;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class CustomProviderTokenGenerator {
public static OAuth2AccessTokenAuthenticationToken generate(RegisteredClient registeredClient, UserDetails userDetails,
CustomAuthorizationGrantAuthenticationToken token,
OAuth2AuthorizationService authorizationService,
OAuth2TokenGenerator<OAuth2Token> tokenGenerator) {
// 构建授权范围(scopes)
Set<String> scopes = registeredClient.getScopes(); // 或从请求参数解析
if (scopes.isEmpty()) {
scopes = Collections.singleton("read"); // 默认 scope
}
Authentication userAuthentication = new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());
// 创建 OAuth2Authorization(持久化授权记录)
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient)
.principalName(userDetails.getUsername())
.authorizationGrantType(token.getAuthorizationGrantType())
.authorizedScopes(scopes)
.attribute(Principal.class.getName(), userAuthentication);
OAuth2Authorization authorization = authorizationBuilder.build();
authorizationService.save(authorization);
OAuth2TokenContext accessTokenContext = DefaultOAuth2TokenContext.builder()
.registeredClient(registeredClient)
.principal(userAuthentication)
.authorization(authorization)
.authorizationGrantType(token.getAuthorizationGrantType())
.authorizationServerContext(AuthorizationServerContextHolder.getContext())
.tokenType(OAuth2TokenType.ACCESS_TOKEN)
.build();
OAuth2TokenContext refreshTokenContext = DefaultOAuth2TokenContext.builder()
.registeredClient(registeredClient)
.principal(userAuthentication)
.authorization(authorization)
.authorizationGrantType(token.getAuthorizationGrantType())
.authorizationServerContext(AuthorizationServerContextHolder.getContext())
.tokenType(OAuth2TokenType.REFRESH_TOKEN)
.build();
OAuth2Token generatedToken = tokenGenerator.generate(accessTokenContext);
if (generatedToken == null) {
throw new IllegalStateException("Failed to generate token");
}
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(registeredClient.getTokenSettings().getAccessTokenTimeToLive());
OAuth2AccessToken accessToken = new OAuth2AccessToken(
OAuth2AccessToken.TokenType.BEARER,
generatedToken.getTokenValue(), // ← 从 Jwt 或 OpaqueToken 中取值
issuedAt,
expiresAt,
accessTokenContext.getAuthorizedScopes() // 或 registeredClient.getScopes()
);
OAuth2RefreshToken refreshToken = (OAuth2RefreshToken) tokenGenerator.generate(refreshTokenContext);
if (refreshToken == null) {
throw new IllegalStateException("Failed to generate refresh token");
}
authorization = OAuth2Authorization.from(authorization)
.accessToken(accessToken)
.refreshToken(refreshToken).build();
authorizationService.save(authorization);
// 构建 AccessToken 响应所需的 attributes
Map<String, Object> accessTokenAttributes = new HashMap<>();
accessTokenAttributes.put(OAuth2ParameterNames.SCOPE, scopes);
accessTokenAttributes.put(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId());
// 返回框架能识别的标准 token
return new OAuth2AccessTokenAuthenticationToken(
registeredClient,
userAuthentication,
accessToken,
refreshToken,
accessTokenAttributes
);
}
}
认证服务器重启,业务端就需要重新登录的问题
这个跟刷新令牌问题不相关,而是最近有人问这个事儿。
实际上是什么呢,在认证服务器端要配置 JwtEncoder,配置它的过程中要先配置JWKSource,而配置JWKSource的过程中有一个关键的动作,就是要配置JWT的RSAKey。其实说到这有的小伙伴已经猜到了,如果构建RSAKey的过程中,JWT-KEY-ID在每次重启时生成随机串,那么业务端传过来的token就必然校验不通过了,这里要设置为固定值,可以每隔一段时间重置为一个新的值。
另外,RSAKey的publicKey和privateKey也要保持固定。
更多推荐

所有评论(0)