上一篇文章发出后,有小伙伴在评论区说没法刷新令牌,这在最早一个版本中的确是个问题,修复后一直没时间来更新文章,直到现在才抽空补上,实在罪过,给各位同学鞠躬了~

OAuth2AuthorizationService接口

首先说一下这个接口,它用于对oauth的授权信息进行管理。在oauth2的框架中,它起到了一个十分重要的作用,即框架完成认证后,需要将相应的认证信息(包括但不限于客户端id、授权方式、授权范围、授权码、accessToken相关、oicdToken相关、refreshToken相关等)存储起来,以便在后面使用。

它有两个实现类:
InMemoryOAuth2AuthorizationServiceJdbcOAuth2AuthorizationService,分别对应将信息存储到内存中或存储到数据库中。在开发过程中要结合项目实际情况选择用哪个实现类。

为什么开头就要提到这个接口呢?因为当我们刷新令牌的时候,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。实际上这里应该放进去一个包含UserDetailsAuthentication类型的对象,正确的方式是:

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也要保持固定。

更多推荐