别告诉我你还不会OAuth 2.0客户端的认证:OAuth2ClientAuthenticationFilter。

OAuth 2.0客户端的认证方式:

当你需要向认证服务器的 令牌端点(Token Endpoint) 请求访问令牌(access token)或刷新令牌(refresh token)时,你可以使用哪些方式来证明你是合法的客户端

在OAuth2协议中,客户端和服务端开始交互的前提,就是客户端需要通过服务器的认证,以确保该客户端是有效的客户端。

OAuth2ClientAuthenticationFilter作用:见名之意用来对oauth2的客户端进行认证,授权服务器处理 OAuth 2.1 相关端点(如 /oauth2/token)的请求时,识别并验证发起请求的客户端(Client)的身份

认证时机 :它通常在请求到达授权服务器的 Token Endpoint(令牌端点)时被触发,用于处理 client_idclient_secret 的各种传递方式,例如:

markdown 复制代码
-   在请求体中以 `client_id` 和 `client_secret` 参数形式传递。
-   使用 HTTP Basic 认证头(`Authorization: Basic base64(client_id:client_secret)`)。
-   (在更高级配置中)处理 JWT 或其他形式的客户端断言(Client Assertion)。

认证结果 :如果客户端认证成功,该过滤器会将认证后的客户端信息(通常是一个 OAuth2ClientAuthenticationToken)放入 Spring Security 的 SecurityContext 中,供后续的授权逻辑(如 OAuth2TokenEndpointFilter)使用。

OAuth2ClientAuthenticationFilter

整个处理流程如下:

kotlin 复制代码
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
     throws ServletException, IOException {
  
  // 第一步 匹配
  if (!this.requestMatcher.matches(request)) {
     filterChain.doFilter(request, response);
     return;
  }

  try {
     // 第二步 转换
     Authentication authenticationRequest = this.authenticationConverter.convert(request);
     if (authenticationRequest instanceof AbstractAuthenticationToken) {
        ((AbstractAuthenticationToken) authenticationRequest)
           .setDetails(this.authenticationDetailsSource.buildDetails(request));
     }
     if (authenticationRequest != null) {
        // 第三步校验
        validateClientIdentifier(authenticationRequest);
        
        // 第四步认证
        Authentication authenticationResult = this.authenticationManager.authenticate(authenticationRequest);
        //认证通过后续处理
        this.authenticationSuccessHandler.onAuthenticationSuccess(request, response, authenticationResult);
     }
     filterChain.doFilter(request, response);

  }
  catch (OAuth2AuthenticationException ex) {
     if (this.logger.isTraceEnabled()) {
        this.logger.trace(LogMessage.format("Client authentication failed: %s", ex.getError()), ex);
     }
     this.authenticationFailureHandler.onAuthenticationFailure(request, response, ex);
  }
}

requestMatcher

首先this.requestMatcher.matches(request),当一个请求到达过滤器链的时候,OAuth2ClientAuthenticationFilter 会检查该请求是否是发往 Token Endpoint 的,并决定是否需要进行客户端认证。

OAuth2ClientAuthenticationFilter 的requestMatcher属性决定了要处理哪些请求,OAuth2ClientAuthenticationFilter的属性配置是由和它配对的OAuth2ClientAuthenticationConfigurer配置类决定的:从下面的代码看 requestMatcher拦截和token相关的请求 比如:tokenEndpointUri,后面我们就会知道

tokenEndpointUri="/oauth2/token"

scss 复制代码
@Override
void init(HttpSecurity httpSecurity) {
   AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
      .getAuthorizationServerSettings(httpSecurity);
   String tokenEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
         ? OAuth2ConfigurerUtils.withMultipleIssuersPattern(authorizationServerSettings.getTokenEndpoint())
         : authorizationServerSettings.getTokenEndpoint();
   ....
   this.requestMatcher = new OrRequestMatcher(new AntPathRequestMatcher(tokenEndpointUri, HttpMethod.POST.name()),
         new AntPathRequestMatcher(tokenIntrospectionEndpointUri, HttpMethod.POST.name()),
         new AntPathRequestMatcher(tokenRevocationEndpointUri, HttpMethod.POST.name()),
         new AntPathRequestMatcher(deviceAuthorizationEndpointUri, HttpMethod.POST.name()));
   ....
}

authenticationConverter

经过requestMatcher匹配到请求后,发现是获取token的请求,接下来就要进行客户端的认证,因为合法的客户端才能获取token。 authenticationConverterOAuth2ClientAuthenticationFilter 中一个非常关键的组件,它的作用是:将 HTTP 请求(HttpServletRequest)转换为一个用于客户端认证的 Authentication 对象(具体是 OAuth2ClientAuthenticationToken 的未认证版本)

csharp 复制代码
public interface AuthenticationConverter {

   Authentication convert(HttpServletRequest request);

}

🔍 为什么需要authenticationConverter?

在 Spring Security 的认证流程中,AuthenticationManager 只认识 Authentication 类型的对象。而客户端认证的凭据(如 client_idclient_secret)是通过 HTTP 请求传递的(可能是 Header,也可能是 Body)。因此,需要一个中间组件来:

  1. 读取原始 HTTP 请求
  2. 提取认证信息
  3. 封装成 Authentication 对象
  4. 交给 AuthenticationManager 处理

这个中间组件就是 authenticationConverter

🧩 authenticationConverter 的典型实现

从OAuth2ClientAuthenticationFilter构造器上可以看出,使用了"委托模式",不自己处理具体的转换逻辑,而是将任务"委托"给一组内部的、专门化的 Converter 实例,由它们按顺序尝试处理请求

scss 复制代码
public OAuth2ClientAuthenticationFilter(AuthenticationManager authenticationManager,
      RequestMatcher requestMatcher) {
   Assert.notNull(authenticationManager, "authenticationManager cannot be null");
   Assert.notNull(requestMatcher, "requestMatcher cannot be null");
   this.authenticationManager = authenticationManager;
   this.requestMatcher = requestMatcher;
   // @formatter:off
   this.authenticationConverter = new DelegatingAuthenticationConverter(
         Arrays.asList(
               new JwtClientAssertionAuthenticationConverter(),
               new ClientSecretBasicAuthenticationConverter(),
               new ClientSecretPostAuthenticationConverter(),
               new PublicClientAuthenticationConverter(),
               new X509ClientCertificateAuthenticationConverter()));
   // @formatter:on
}

处理流程: 一旦某个 Converter 成功返回一个 OAuth2ClientAuthenticationToken(非 null),就立即停止遍历,返回该结果

它会根据不同的客户端认证方式,执行不同的提取逻辑:

✅ 示例 :请求体传参(client_secret_post)

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=0nFInXkzMSiAaXmZhxttwHJWKRB-xWHgcK4MYvFXp7EpID0kDnHKXPngx1DOqtOlnw6sNqo1-aY9y_kwKxLgUwoG5BH2p9-GWFh6QcvrlCpPOAADRqVzze5rFkcHDlxp&redirect_uri=http://127.0.0.1:8080/login/oauth2/code/oidc-client&client_id=oidc-client&client_secret=secret

ClientSecretPostAuthenticationConverter java doc 描述他的作用很清楚

Attempts to extract client credentials from POST parameters of HttpServletRequest and then converts to an OAuth2ClientAuthenticationToken used for authenticating the client.

  • 从Post请求参数中解析 client_idclient_secret(客户端凭证)
  • 构建 OAuth2ClientAuthenticationToken
  • 设置认证方式为 ClientAuthenticationMethod.CLIENT_SECRET_POST

流程如下图所示:

转换器最终返回的是一个 未认证的 OAuth2ClientAuthenticationToken OAuth2ClientAuthenticationToken构造器如下:

less 复制代码
/**
 * Constructs an {@code OAuth2ClientAuthenticationToken} using the provided
 * parameters.
 * @param clientId the client identifier
 * @param clientAuthenticationMethod the authentication method used by the client
 * @param credentials the client credentials
 * @param additionalParameters the additional parameters
 */
public OAuth2ClientAuthenticationToken(String clientId, ClientAuthenticationMethod clientAuthenticationMethod,
      @Nullable Object credentials, @Nullable Map<String, Object> additionalParameters) {
   super(Collections.emptyList());
   Assert.hasText(clientId, "clientId cannot be empty");
   Assert.notNull(clientAuthenticationMethod, "clientAuthenticationMethod cannot be null");
   this.clientId = clientId;
   this.registeredClient = null;
   this.clientAuthenticationMethod = clientAuthenticationMethod;
   this.credentials = credentials;
   this.additionalParameters = Collections
      .unmodifiableMap((additionalParameters != null) ? additionalParameters : Collections.emptyMap());
}

✅ 总结

问题 回答
authenticationConverter 是干啥的? 将 HTTP 请求中的客户端凭据提取出来,转换成 OAuth2ClientAuthenticationToken 对象
谁使用它? OAuth2ClientAuthenticationFilter 在认证前调用它
输出是什么? 一个未认证的 Authentication 对象
为什么需要它? 解耦 HTTP 层和安全认证层,使认证逻辑不直接依赖 HttpServletRequest
可以自定义吗? 可以!用于支持自定义认证方式(如 API Key、JWT 断言等)

authenticationManager

转成一个未经认证OAuth2ClientAuthenticationToken 对象后,接下来就需要使用authenticationManager对其进行认证。

我们知道 AuthenticationManager 通常是一个 ProviderManager,它本身不直接认证 ,而是委托给一系列 AuthenticationProvider ,并找到第一个能处理该 Authentication 类型的 provider 来执行认证。

ClientSecretAuthenticationProvider

ClientSecretAuthenticationProvider 恰好是用来验证client_secret的提供者provider

我们关注一下它的public Authentication authenticate(Authentication authentication)认证逻辑。

java 复制代码
if (!ClientAuthenticationMethod.CLIENT_SECRET_BASIC.equals(clientAuthentication.getClientAuthenticationMethod()) &&
      !ClientAuthenticationMethod.CLIENT_SECRET_POST.equals(clientAuthentication.getClientAuthenticationMethod())) {
   return null;
}

ClientSecretAuthenticationProvider 目前只支持CLIENT_SECRET_BASIC和CLIENT_SECRET_POST两种方式,传递client_secret的两种方式

java 复制代码
String clientId = clientAuthentication.getPrincipal().toString();
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
if (registeredClient == null) {
   throwInvalidClient(OAuth2ParameterNames.CLIENT_ID);
}

根据客户端ID 查询是不是合法客户端 registeredClientRepository 是可以用户配置的。

也是在OAuth2ClientAuthenticationConfigurer进行配置的

scss 复制代码
if (!registeredClient.getClientAuthenticationMethods()
   .contains(clientAuthentication.getClientAuthenticationMethod())) {
   throwInvalidClient("authentication_method");
}

if (clientAuthentication.getCredentials() == null) {
   throwInvalidClient("credentials");
}

要求客户端的认证方式要和客户端注册时候的认证方式保持一致 ,同时客户端的密码还不能为空

less 复制代码
String clientSecret = clientAuthentication.getCredentials().toString();
if (!this.passwordEncoder.matches(clientSecret, registeredClient.getClientSecret())) {
   if (this.logger.isDebugEnabled()) {
      this.logger.debug(LogMessage.format(
            "Invalid request: client_secret does not match" + " for registered client '%s'",
            registeredClient.getId()));
   }
   throwInvalidClient(OAuth2ParameterNames.CLIENT_SECRET);
}

if (registeredClient.getClientSecretExpiresAt() != null
      && Instant.now().isAfter(registeredClient.getClientSecretExpiresAt())) {
   throwInvalidClient("client_secret_expires_at");
}

使用passwordEncoder比对密码 同时校验客户端的密码是否过期,客户端注册的时候可以设置密钥过期时间。

kotlin 复制代码
// Validate the "code_verifier" parameter for the confidential client, if
// available
this.codeVerifierAuthenticator.authenticateIfAvailable(clientAuthentication, registeredClient);

这步 是验证Code 同时也有一个PKCE。CODE_VERIFIER是 客户端(如 App)生成的一个一次性、高强度的随机字符串,用于"证明"自己是原始发起授权请求的一方。这个我们后续再讲。

校验code使用了如下的代码,用authorizationService来对code的合法性进行校验,前提是授权码模式才校验,

authenticateIfAvailable名字起的也对。有必要校验的时候采取校验,比如授权码模式参数中得有code

authorizationService是个很重要的类。大家有知道的吗?评论区教教我。

typescript 复制代码
void authenticateIfAvailable(OAuth2ClientAuthenticationToken clientAuthentication,
      RegisteredClient registeredClient) {
   authenticate(clientAuthentication, registeredClient);
}

private boolean authenticate(OAuth2ClientAuthenticationToken clientAuthentication,
      RegisteredClient registeredClient) {

   Map<String, Object> parameters = clientAuthentication.getAdditionalParameters();
   if (!authorizationCodeGrant(parameters)) {
      return false;
   }

   OAuth2Authorization authorization = this.authorizationService
      .findByToken((String) parameters.get(OAuth2ParameterNames.CODE), AUTHORIZATION_CODE_TOKEN_TYPE);
   if (authorization == null) {
      throwInvalidGrant(OAuth2ParameterNames.CODE);
   }}
   

最后返回一个经过验证的OAuth2ClientAuthenticationToken,

scss 复制代码
return new OAuth2ClientAuthenticationToken(registeredClient,
      clientAuthentication.getClientAuthenticationMethod(), clientAuthentication.getCredentials());

多了一步 setAuthenticated(true);

ini 复制代码
public OAuth2ClientAuthenticationToken(RegisteredClient registeredClient,
      ClientAuthenticationMethod clientAuthenticationMethod, @Nullable Object credentials) {
   super(Collections.emptyList());
   Assert.notNull(registeredClient, "registeredClient cannot be null");
   Assert.notNull(clientAuthenticationMethod, "clientAuthenticationMethod cannot be null");
   this.clientId = registeredClient.getClientId();
   this.registeredClient = registeredClient;
   this.clientAuthenticationMethod = clientAuthenticationMethod;
   this.credentials = credentials;
   this.additionalParameters = Collections.unmodifiableMap(Collections.emptyMap());
   setAuthenticated(true);
}

authenticationSuccessHandler OAuth 2.1 客户端认证成功处理逻辑

认证通过之后成功回调: Java 8 引入的 方法引用(Method Reference) 语法,是一种更简洁的函数式编程写法。

ini 复制代码
private AuthenticationSuccessHandler authenticationSuccessHandler = this::onAuthenticationSuccess;
kotlin 复制代码
this.authenticationSuccessHandler.onAuthenticationSuccess(request, response, authenticationResult);

其实没做什么就是把认证结果放到上下文securityContext.setAuthentication(authentication);中,后续可以继续使用

scss 复制代码
private void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
      Authentication authentication) {

   SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
   securityContext.setAuthentication(authentication);
   SecurityContextHolder.setContext(securityContext);
   if (this.logger.isDebugEnabled()) {
      this.logger.debug(LogMessage.format("Set SecurityContextHolder authentication to %s",
            authentication.getClass().getSimpleName()));
   }
}

AuthenticationFailureHandler OAuth 2.1 客户端认证失败处理逻辑

ini 复制代码
private AuthenticationFailureHandler authenticationFailureHandler = this::onAuthenticationFailure;
scss 复制代码
private void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
      AuthenticationException exception) throws IOException {

   SecurityContextHolder.clearContext();

   // TODO
   // The authorization server MAY return an HTTP 401 (Unauthorized) status code
   // to indicate which HTTP authentication schemes are supported.
   // If the client attempted to authenticate via the "Authorization" request header
   // field,
   // the authorization server MUST respond with an HTTP 401 (Unauthorized) status
   // code and
   // include the "WWW-Authenticate" response header field
   // matching the authentication scheme used by the client.

   OAuth2Error error = ((OAuth2AuthenticationException) exception).getError();
   ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
   if (OAuth2ErrorCodes.INVALID_CLIENT.equals(error.getErrorCode())) {
      httpResponse.setStatusCode(HttpStatus.UNAUTHORIZED);
   }
   else {
      httpResponse.setStatusCode(HttpStatus.BAD_REQUEST);
   }
   // We don't want to reveal too much information to the caller so just return the
   // error code
   OAuth2Error errorResponse = new OAuth2Error(error.getErrorCode());
   this.errorHttpResponseConverter.write(errorResponse, null, httpResponse);
}

认证失败之后 清理安全上下文

拿到OAuth2AuthenticationException中包装的OAuth2Error,里面有错误码 错误信息

  • 获取其中封装的 OAuth2Error 对象,它包含:

    • errorCode:如 invalid_clientinvalid_request
    • description:可选的错误描述
    • uri:可选的文档链接

构造最小化错误响应(安全设计)

java 复制代码
// We don't want to reveal too much information to the caller so just return the error code OAuth2Error errorResponse = new OAuth2Error(error.getErrorCode());
  • 只返回 errorCode不返回 description 或堆栈信息

  • 目的是:

    • ✅ 防止信息泄露(如内部实现细节)
    • ✅ 防止攻击者枚举有效 client_id
    • ✅ 符合安全最小化原则

写出错误响应

kotlin 复制代码
this.errorHttpResponseConverter.write(errorResponse, null, httpResponse);
  • 使用配置的 HttpMessageConverter(如 MappingJackson2HttpMessageConverter)将 OAuth2Error 序列化为 JSON。
  • 写入 HTTP 响应体。

典型的响应体如下:

json 复制代码
{
  "error": "invalid_client"
}

所以如果客户端验证不通过可能返回的结果是:

http 复制代码
HTTP/1.1 401 Unauthorized
Content-Type: application/json

{
  "error": "invalid_client"
}

总结

OAuth2ClientAuthenticationFilter用来对oauth客户端进行认证,他要依赖很多组件相互配合。

  • requestMatcher
  • authenticationConverter
  • authenticationManager
  • authenticationSuccessHandler
  • authenticationFailureHandler
  • ClientSecretAuthenticationProvider
  • registeredClientRepository
  • authorizationService
  • codeVerifierAuthenticator

很多组件由于篇幅没有细致分享 其实每一个点都可以单独拿出来分享。后面我会陆续给出。

相关推荐
北京_宏哥7 小时前
《刚刚问世》系列初窥篇-Java+Playwright自动化测试-38-屏幕截图利器-上篇(详细教程)
java·前端·面试
二闹7 小时前
别再混淆了 is 和 ==的区别
后端·python
awei09168 小时前
SpringBoot3中使用Caffeine缓存组件
java·缓存·springboot·springboot3
用户098 小时前
Kotlin后端开发指南
android·后端
双向338 小时前
K8s Pod CrashLoopBackOff:从镜像构建到探针配置的排查过程
后端
xiguolangzi8 小时前
mysql业务笔记
java
杨杨杨大侠8 小时前
手搓责任链框架 3:处理器实现
java·spring·github
用户4099322502128 小时前
如何在FastAPI中巧妙隔离依赖项,让单元测试不再头疼?
后端·ai编程·trae