别告诉我你还不会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

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

相关推荐
努力也学不会java11 小时前
【Java并发】深入解析ConcurrentHashMap
java·juc·hash table
掘金一周11 小时前
2025年还有前端不会Nodejs ?| 掘金一周 9.25
android·前端·后端
RoyLin11 小时前
前端·后端·node.js
泉城老铁11 小时前
springboot常用的注解需要了解,开发必备
spring boot·后端
qq_3168377511 小时前
spring cloud 同一服务多实例 websocket跨实例无法共享Session 的解决
java·websocket·spring cloud
草莓熊Lotso11 小时前
《算法闯关指南:优选算法--滑动窗口》--14找到字符串中所有字母异位词
java·linux·开发语言·c++·算法·java-ee
青云交11 小时前
Java 大视界 -- 基于 Java 的大数据实时流处理在金融高频交易数据分析中的应用
java·大数据·量化交易·异常检测·apache flink·实时流处理·金融高频交易
hhhhhshiyishi11 小时前
WLB公司内推|招联金融2026届校招|18薪
java·算法·机器学习·金融·求职招聘
韩立学长12 小时前
【开题答辩实录分享】以《城市网约车服务预约与管理小程序的设计与实现》为例进行答辩实录分享
java·小程序·选题
yics.12 小时前
多线程——单例模式
java·单例模式·多线程·线程安全