OAuth 2.0客户端的认证方式:
当你需要向认证服务器的 令牌端点(Token Endpoint) 请求访问令牌(access token)或刷新令牌(refresh token)时,你可以使用哪些方式来证明你是合法的客户端
在OAuth2协议中,客户端和服务端开始交互的前提,就是客户端需要通过服务器的认证,以确保该客户端是有效的客户端。
OAuth2ClientAuthenticationFilter
作用:见名之意用来对oauth2的客户端进行认证,授权服务器处理 OAuth 2.1 相关端点(如 /oauth2/token
)的请求时,识别并验证发起请求的客户端(Client)的身份。
认证时机 :它通常在请求到达授权服务器的 Token Endpoint(令牌端点)时被触发,用于处理 client_id
和 client_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。 authenticationConverter
是 OAuth2ClientAuthenticationFilter
中一个非常关键的组件,它的作用是:将 HTTP 请求(HttpServletRequest
)转换为一个用于客户端认证的 Authentication
对象(具体是 OAuth2ClientAuthenticationToken
的未认证版本) 。
csharp
public interface AuthenticationConverter {
Authentication convert(HttpServletRequest request);
}
🔍 为什么需要authenticationConverter?
在 Spring Security 的认证流程中,AuthenticationManager
只认识 Authentication
类型的对象。而客户端认证的凭据(如 client_id
和 client_secret
)是通过 HTTP 请求传递的(可能是 Header,也可能是 Body)。因此,需要一个中间组件来:
- 读取原始 HTTP 请求
- 提取认证信息
- 封装成
Authentication
对象 - 交给
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_id
和client_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_client
、invalid_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
很多组件由于篇幅没有细致分享 其实每一个点都可以单独拿出来分享。后面我会陆续给出。