Spring Authorization Server中的常见类1:OAuth2AccessTokenAuthenticationToken

92 阅读5分钟

授权码模式访问oauth2/token端点的流程

我们在使用授权码模式通过code换取accessToken的时候,通常会调用oauth2/tokenendpoint,http请求如下:

POST /oauth2/token HTTP/1.1
Host: localhost:9000
Content-Type: application/x-www-form-urlencoded
Content-Length: 271

grant_type=authorization_code&code=jnAhK1dAGV42GQNK4hhxF14-xx61c6PplDSz1ziW63V8XWq42A3Nzi1ZYSv9UZTATkSFk_WXAF0OchWZo1Eb9xFupOJ0FFXnDzr3RsfcZqsoE91B5U2awlvnA3dRfNRV&redirect_uri=http://127.0.0.1:8080/login/oauth2/code/oidc-client&client_id=oidc-client&client_secret=secret

HTTP Request最终会进入到授权服务器的OAuth2TokenEndpointFilter过滤器中,在授权码的模式中,过滤器需要对授权码进行认证。

tips: 我们都知道在spring security中,所有的认证都要转换成authentication对象,这是一种认证令牌,经过 authenticationManager的认证之后,返回一个完全认证的令牌。

所以OAuth2TokenEndpointFilter使用AuthenticationConverterHTTP request转换成Authentication

tips: AuthenticationConverter的接口签名如下:

Authentication convert(HttpServletRequest request);

接受一个HttpServletRequest作为参数,提取里面的信息,转换成待认证的Authentication令牌

OAuth2AuthorizationCodeAuthenticationConverter

由于我们使用的是授权码模式,所以AuthenticationConverter的实现类使用的是OAuth2AuthorizationCodeAuthenticationConverter 看名称就知道啦:oauth2授权码认证的converter。

tips:OAuth2TokenEndpointFilter,内部持有一个“委托式认证转换器”,其内部持有一组“策略”,每个策略负责处理一种 grant_type

this.authenticationConverter = new DelegatingAuthenticationConverter(
      Arrays.asList(
            new OAuth2AuthorizationCodeAuthenticationConverter(),
            new OAuth2RefreshTokenAuthenticationConverter(),
            new OAuth2ClientCredentialsAuthenticationConverter(),
            new OAuth2DeviceCodeAuthenticationConverter(),
            new OAuth2TokenExchangeAuthenticationConverter())
);

OAuth2AuthorizationCodeAuthenticationToken

OAuth2AuthorizationCodeAuthenticationConverter 提取请求参数,转换成OAuth2AuthorizationCodeAuthenticationToken,下图中我提取了关键逻辑,主要是看最后的返回值,返回一个OAuth2AuthorizationCodeAuthenticationToken

@Override
public Authentication convert(HttpServletRequest request) {
   MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getFormParameters(request);

   // grant_type (REQUIRED)
   String grantType = parameters.getFirst(OAuth2ParameterNames.GRANT_TYPE);
   if (!AuthorizationGrantType.AUTHORIZATION_CODE.getValue().equals(grantType)) {
      return null;
   }

   Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();

   // code (REQUIRED)
   String code = parameters.getFirst(OAuth2ParameterNames.CODE);
   
   // redirect_uri (REQUIRED)
   // Required only if the "redirect_uri" parameter was included in the authorization
   // request
   String redirectUri = parameters.getFirst(OAuth2ParameterNames.REDIRECT_URI);
  

   Map<String, Object> additionalParameters = new HashMap<>();
   parameters.forEach((key, value) -> {
      if (!key.equals(OAuth2ParameterNames.GRANT_TYPE) && !key.equals(OAuth2ParameterNames.CLIENT_ID)
            && !key.equals(OAuth2ParameterNames.CODE) && !key.equals(OAuth2ParameterNames.REDIRECT_URI)) {
         additionalParameters.put(key, (value.size() == 1) ? value.get(0) : value.toArray(new String[0]));
      }
   });

   return new OAuth2AuthorizationCodeAuthenticationToken(code, clientPrincipal, redirectUri, additionalParameters);
}

OAuth2AuthorizationCodeAuthenticationProvider

OAuth2AuthorizationCodeAuthenticationProvider用来对OAuth2AuthorizationCodeAuthenticationToken进行认证

tips:OAuth2TokenEndpointFilter 持有AuthenticationManager对令牌进行认证,但实际根据令牌类型的不同,需要使用不同的AuthenticationProvider来进行认证。OAuth2AuthorizationCodeAuthenticationProvider就是负责对 OAuth2AuthorizationCodeAuthenticationToken进行认证,认证之后返回OAuth2AccessTokenAuthenticationToken,代表认证的结果。

OAuth2AccessTokenAuthenticationToken

终于讲到今天的主角了OAuth2AccessTokenAuthenticationToken,经过OAuth2AuthorizationCodeAuthenticationProvider 认证之后返回的结果就是它:

return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken,
      additionalParameters);

构造器:

public OAuth2AccessTokenAuthenticationToken(RegisteredClient registeredClient, Authentication clientPrincipal,
      OAuth2AccessToken accessToken, @Nullable OAuth2RefreshToken refreshToken,
      Map<String, Object> additionalParameters) {
   super(Collections.emptyList());
   Assert.notNull(registeredClient, "registeredClient cannot be null");
   Assert.notNull(clientPrincipal, "clientPrincipal cannot be null");
   Assert.notNull(accessToken, "accessToken cannot be null");
   Assert.notNull(additionalParameters, "additionalParameters cannot be null");
   this.registeredClient = registeredClient; // 表示在授权服务器中注册的客户端应用(Client Application)
   this.clientPrincipal = clientPrincipal;
   this.accessToken = accessToken;// 实际颁发给客户端的访问令牌(Access Token)。
   this.refreshToken = refreshToken; // 刷新令牌(Refresh Token),用于在访问令牌过期后获取新的访问令牌
   this.additionalParameters = additionalParameters;
}

image.png

它扩展了 AbstractAuthenticationToken,表明它是一个用于在认证流程中传递认证信息的 认证令牌(Authentication Token) 。也就是经过认证之后,颁发的令牌和刷新令牌已经保存在这个类中了。

AuthenticationSuccessHandler

OAuth2TokenEndpointFilter拿到OAuth2AccessTokenAuthenticationToken 之后,AuthenticationSuccessHandler就派上用场了,见名之意:认证成功处理器。

public interface AuthenticationSuccessHandler {
   /**
    * Called when a user has been successfully authenticated.
    * @param request the request which caused the successful authentication
    * @param response the response
    * @param authentication the <tt>Authentication</tt> object which was created during
    * the authentication process.
    */
   void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
         Authentication authentication) throws IOException, ServletException;

}

tips: AuthenticationSuccessHandler是一个接口,里面提供了一个方法onAuthenticationSuccess,当认证成功之后,调用这个方法进行响应。OAuth2TokenEndpointFilter默认使用的是OAuth2AccessTokenResponseAuthenticationSuccessHandler实现来 用来将accesToken返回给用户。

OAuth2TokenEndpointFilter部分代码

private AuthenticationSuccessHandler authenticationSuccessHandler = new OAuth2AccessTokenResponseAuthenticationSuccessHandler();


OAuth2AccessTokenAuthenticationToken accessTokenAuthentication = (OAuth2AccessTokenAuthenticationToken) this.authenticationManager
   .authenticate(authorizationGrantAuthentication);
this.authenticationSuccessHandler.onAuthenticationSuccess(request, response, accessTokenAuthentication);

OAuth2AccessTokenResponseAuthenticationSuccessHandler 默认的成功处理器

OAuth2AccessTokenResponseAuthenticationSuccessHandler都怎么处理的呢?

  1. 将OAuth2AccessTokenAuthenticationToken包装成OAuth2AccessTokenResponse

  2. 利用Converter<OAuth2AccessTokenResponse, Map<String, Object>>OAuth2AccessTokenResponse转成map结构。

  3. 利用MappingJackson2HttpMessageConverter将map转换成HTTP响应

@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
      Authentication authentication) throws IOException, ServletException {
   

   OAuth2AccessToken accessToken = accessTokenAuthentication.getAccessToken();
   OAuth2RefreshToken refreshToken = accessTokenAuthentication.getRefreshToken();
   Map<String, Object> additionalParameters = accessTokenAuthentication.getAdditionalParameters();

   OAuth2AccessTokenResponse.Builder builder = OAuth2AccessTokenResponse.withToken(accessToken.getTokenValue())
      .tokenType(accessToken.getTokenType())
      .scopes(accessToken.getScopes());
   if (accessToken.getIssuedAt() != null && accessToken.getExpiresAt() != null) {
      builder.expiresIn(ChronoUnit.SECONDS.between(accessToken.getIssuedAt(), accessToken.getExpiresAt()));
   }
   if (refreshToken != null) {
      builder.refreshToken(refreshToken.getTokenValue());
   }
   if (!CollectionUtils.isEmpty(additionalParameters)) {
      builder.additionalParameters(additionalParameters);
   }
   OAuth2AccessTokenResponse accessTokenResponse = builder.build();
   ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
   this.accessTokenResponseConverter.write(accessTokenResponse, null, httpResponse);
}

我去掉了一些代码发现整个流程使用了构建者模式,最后build出一个OAuth2AccessTokenResponse实例

OAuth2AccessTokenResponse的关键属性 image.png

最后一步利用HttpMessageConverter将响应返回

private final HttpMessageConverter<OAuth2AccessTokenResponse> accessTokenResponseConverter = new OAuth2AccessTokenResponseHttpMessageConverter();

tips: HttpMessageConverter 是 Spring 框架中一个用于在 HTTP 请求/响应 和 Java 对象之间进行转换的组件

它负责:

  • 把 HTTP 请求体(如 JSON)  转成 Java 对象(反序列化) ✅ read
  • 把 Java 对象 转成 HTTP 响应体(如 JSON) (序列化) ✅ write

在写出响应之前转换成map: image.png

加一层map我们可以手动控制每一个字段的输出

这种设计模式叫什么?

这其实是典型的: ✅ DTO + Converter 模式(或 适配器模式

  • OAuth2AccessTokenResponse:业务模型(Java 对象)
  • Map<String, Object>:协议模型(符合 RFC 的键值对)
  • Converter:适配层,负责两者之间的转换

类似于你在做 API 开发时,不会直接返回数据库实体类,而是转成一个 ApiResponseDTO

Map<String, Object> 在这里是“协议中间表示层”,它不是多余的,而是为了确保生成的 JSON 严格符合 OAuth2 标准,而不是“看起来像”的 JSON。

正确的 OAuth2 响应应该是:

{
  "access_token": "abc123",
  "expires_in": 3600,
  "refresh_token": "def456",
  "token_type": "Bearer",
  "scope": "read write"
}

所以这个converter的作用就是使其结果符合RFC 6749 的规范!

image.png

OAuth2ParameterNames类中定义了很多oauth2的标准参数和字段: image.png

要不要统一响应格式

{
  "code": 200,
  "msg": "success",
  "data": { ... }
}

可能有的公司的业务API会有统一的返回结构,那么我们要不要自定义呢?

我的建议是:不要

因为这是“协议”不是“API”,OAuth2 是一个 开放标准(Open Standard)

所有标准 SDK 都是按 RFC 6749 写死的逻辑,不会去猜你的 data 字段在哪。

你的授权服务器必须返回:


HTTP/1.1 200 OK
Content-Type: application/json

{
  "access_token": "abc123",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "xyz789",
  "scope": "read write"
}

这样所有客户端 SDK 才能正确读取:

json.get("access_token") → "abc123"  // ✅ 成功

tips:虽然不建议,如果想要自定义,可以通过实现自己的AuthenticationSuccessHandler,来统一封装返回结果