oauth2中OAuth2TokenEndpointFilter的处理流程

92 阅读5分钟

OAuth2TokenEndpointFilter是什么啊?

它是 Spring Authorization Server(授权服务器)中,专门负责处理 /oauth2/token 端点请求的核心过滤器。 授权服务器签发token说的就是它的功能。

说人话就是:

拦截发送到 OAuth 2.0 令牌端点(通常是 /oauth2/token)的 HTTP 请求,请求经过认证之后,给客户端返回token

下图展示了过滤器的核心过滤流程和其依赖的组件。

image.png

从上图我们可以清晰的看到,OAuth2TokenEndpointFilter 依赖如下组件:

  1. tokenEndpointMatcher
  2. authenticationConverter
  3. authenticationManager
  4. authenticationSuccessHandler
  5. authenticationFailureHandler

上面这些组件都是支持开发者进行custom定制的,可以通OAuth2TokenEndpointConfigurer进行配置的。我们暂不去讨论定制化,就讨论框架默认情况下所做的处理。

tokenEndpointMatcher

this.tokenEndpointMatcher = new AntPathRequestMatcher(tokenEndpointUri, HttpMethod.POST.name());
if (!this.tokenEndpointMatcher.matches(request)) {
   filterChain.doFilter(request, response);
   return;
}

用来统一请求入口,表明当前过滤器只处理/oauth2/token 的请求,其他请求一律放行。

authenticationConverter

将原始的 HTTP 请求(特别是发送到 /oauth2/token 端点的请求)转换成一个特定类型的 Authentication 对象以便后续的 AuthenticationManager 能够处理。

在 Spring Security 的认证流程中,核心是 AuthenticationManager,它接收一个 Authentication 类型的对象,对其进行认证,并返回一个已认证的 Authentication 对象。

比如下面的请求:

curl --location --request POST 'http://localhost:9000/oauth2/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--header 'Cookie: JSESSIONID=4150EED52BD5B1AE1FA0BC913D697258' \
--data-urlencode 'grant_type=client_credentials' \
--data-urlencode 'client_id=oidc-client' \
--data-urlencode 'client_secret=secret'

AuthenticationManager 并不能直接处理这种原始的 HTTP 请求。它需要一个结构化的、类型安全的“认证请求对象”。

这时,authenticationConverter 就登场了!

它在流程中的位置

  1. OAuth2TokenEndpointFilter 拦截请求:这个 Filter 拦截了所有发往 /oauth2/token 的请求。

  2. 调用 converter:Filter 不会自己解析请求,而是调用你配置的 authenticationConverter

  3. converter 解析请求并生成 Authentication 对象

    • 它检查请求中的 grant_type 参数。
    • 根据 grant_type 的值(如 authorization_coderefresh_tokenclient_credentials 等),选择合适的子 converter
    • 例如,如果 grant_type=authorization_code,则 OAuth2AuthorizationCodeAuthenticationConverter 会被调用。
    • 这个子 converter 会从请求中提取 coderedirect_uriclient_idclient_secret 等参数,并将它们封装成一个 OAuth2AuthorizationCodeAuthenticationToken 对象。
  4. 将 Authentication 对象交给 AuthenticationManagerOAuth2TokenEndpointFilter 拿到这个 Authentication 对象后,将其提交给 AuthenticationManager 进行真正的认证和令牌颁发。

委托模式

image.png

  • DelegatingAuthenticationConverter:这是一个“委托式”转换器。它内部持有一个 List<AuthenticationConverter>

  • 工作方式:当它收到一个 HttpServletRequest 时,它会遍历内部的每一个子 converter,调用它们的 convert() 方法。

  • 哪个子 converter 能处理,就用哪个:每个子 converter 都会检查这个请求是否符合它能处理的类型(通常是通过 grant_type 判断)。例如:

    • OAuth2AuthorizationCodeAuthenticationConverter 只处理 grant_type=authorization_code 的请求。
    • OAuth2RefreshTokenAuthenticationConverter 只处理 grant_type=refresh_token 的请求。
  • 返回第一个成功转换的结果:一旦某个子 converter 成功将请求转换为对应的 Authentication 对象(如 OAuth2AuthorizationCodeAuthenticationToken),DelegatingAuthenticationConverter 就返回这个对象,不再尝试后面的 converter

authenticationManager

通过配置一个能够识别和处理多种 Authentication 类型的 AuthenticationManager,通常是 ProviderManager,它内部包含多个 AuthenticationProvider,每个 Provider 负责处理一种特定的授权类型

核心机制:AuthenticationManager 与 AuthenticationProvider

在 Spring Security 中,AuthenticationManager 的核心实现是 ProviderManager。它的职责是:

  1. 接收一个 Authentication 对象(如 OAuth2AuthorizationCodeAuthenticationToken)。
  2. 遍历其内部注册的所有 AuthenticationProvider
  3. 调用每个 provider.supports(authentication.getClass()) 方法来检查该 provider 是否支持这种 Authentication 类型。
  4. 如果支持,则调用 provider.authenticate(authentication) 进行认证。
  5. 返回第一个成功认证的结果。

我们用client-credentials授权模式进行举例子:

OAuth2ClientCredentialsAuthenticationConverter 将原始请求转化成OAuth2ClientCredentialsAuthenticationToken

OAuth2ClientCredentialsAuthenticationProvider 支持认证 OAuth2ClientCredentialsAuthenticationToken

认证之后返回的是 OAuth2AccessTokenAuthenticationToken 里面封装了accessToken。里面的细节我们可以单独写一篇文章讨论,因为涉及到了token的定制化。

authenticationFailureHandler and authenticationSuccessHandler

认证有两种结果,肯定是认证成功和认证失败两种结果,分别由对应的成功和失败处理器来决定给用户的响应

private AuthenticationSuccessHandler authenticationSuccessHandler = new OAuth2AccessTokenResponseAuthenticationSuccessHandler();

private AuthenticationFailureHandler authenticationFailureHandler = new OAuth2ErrorAuthenticationFailureHandler();

从源码上看,框架提供了默认的成功和失败处理器。

成功处理器: 负责将生成的令牌信息格式化并写回 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);
   }

   if (this.accessTokenResponseCustomizer != null) {
      // @formatter:off
      OAuth2AccessTokenAuthenticationContext accessTokenAuthenticationContext =
            OAuth2AccessTokenAuthenticationContext.with(accessTokenAuthentication)
               .accessTokenResponse(builder)
               .build();
      // @formatter:on
      this.accessTokenResponseCustomizer.accept(accessTokenAuthenticationContext);
      if (this.logger.isTraceEnabled()) {
         this.logger.trace("Customized access token response");
      }
   }

   OAuth2AccessTokenResponse accessTokenResponse = builder.build();
   ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
   this.accessTokenResponseConverter.write(accessTokenResponse, null, httpResponse);
}

上面的代码摘选自默认的成功处理器,通过构建者模式构建一个OAuth2AccessTokenResponse,最后通过accessTokenResponseConverter写入到http响应中。

总结

里面其实有很多细节我没有细说

比如token的定制化

自定义authenticationProvider如何落地。

上面的流程是因为我最近在梳理项目,发现以前读不懂的代码突然可以理解了,把那一瞬间的想法想记录下来。

最后我自己挖的坑 我也得自己埋,预告下 将要写单独的文章,实现token的定制,以及自定义认证逻辑。