Spring OAuth2客户端身份验证源码解析

2024-08-23 03:20

本文主要是介绍Spring OAuth2客户端身份验证源码解析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

本文介绍在spring oauth2.0客户端向授权服务发起token请求时,源码是如何向请求中添加客户端认证参数,来交由授权服务进行认证的

版本信息

Spring Boot 2.7.10
spring-security-oauth2-client 5.7.7


认证方式

先介绍自带的四种客户端认证方式

jwt认证方式

对应的是ClientAuthenticationMethod类中的client_secret_jwtprivate_key_jwt,客户端使用加密算法及密钥生成一个JWT字符串,传到授权服务用相同算法及密钥解密进行对比,一致则认证成功

请求格式

请求方法:POST

请求路径:/oauth2/token

请求头:

Content-Type: application/x-www-form-urlencoded

请求体表单参数(这里可以用任意授权模式,不一定是客户端凭证模式,只是演示):

client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&
client_assertion=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjbGllbnRJZCIsImF1ZCI6Imh0dHBzOi8vZXhhbXBsZS5jb20vb2F1dGgyL3Rva2VuIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c&
client_id=clientId&
grant_type=client_credentials

请求参数解释:

  1. client_assertion_type
    • 值为 urn:ietf:params:oauth:client-assertion-type:jwt-bearer(固定值),表示使用 JWT 作为客户端断言。

  2. client_assertion
    • JWT 断言的具体值。这是一个签名的 JWT 字符串,包含客户端身份信息。

  3. client_id
    • 客户端的 ID,用于标识客户端。

  4. grant_type
    • 授权类型,此处为 client_credentials,可指定为其他类型

示例 JWT 断言(简化版):

{"alg": "RS256","typ": "JWT"
}
{"sub": "clientId","aud": "https://example.com/oauth2/token","iat": 1516239022
}

client_secret_basic方式

对应配置为ClientAuthenticationMethod.CLIENT_SECRET_BASIC

请求方法:POST

请求路径:/oauth2/token

请求头:

Authorization: Basic Y2xpZW50SWQ6Y2xpZW50U2VjcmV0
Content-Type: application/x-www-form-urlencoded

请求体(这里可以用任意授权模式,不一定是客户端凭证模式,只是演示):

grant_type=client_credentials

请求头解释:

  1. Authorization

    • 值为 Basic Y2xpZW50SWQ6Y2xpZW50U2VjcmV0,表示 Basic 认证,其中 Y2xpZW50SWQ6Y2xpZW50U2VjcmV0clientId:clientSecret (客户端id:客户端密钥)经过 URL 编码后的字符串。

client_secret_post方式

对应配置ClientAuthenticationMethod.CLIENT_SECRET_POST

请求方法:POST

请求路径:/oauth2/token

请求格式如下:

POST /oauth2/token HTTP/1.1
Host: authorization-server.com
Content-Type: application/x-www-form-urlencodedclient_id=your-client-id&
client_secret=your-client-secret&
grant_type=authorization_code&
code=authorization-code&
redirect_uri=your-redirect-uri

关键点

  • 必要参数
    • client_id:客户端 ID
    • client_secret:客户端密钥
  • 其他参数:可能包括 grant_typecoderedirect_uri 等,如果有的话。

这种请求格式用于 OAuth 2.0 客户端凭据授予流程,客户端通过 POST 方法将自己的凭据发送给授权服务器,以获取访问令牌。


PKCE方式

转换的请求

请求方法:POST

请求路径:/oauth2/token

请求格式:

POST /oauth2/token HTTP/1.1
Host: authorization-server.com
Content-Type: application/x-www-form-urlencodedclient_id=your-client-id&
code_verifier=your-code-verifier&
grant_type=authorization_code&
code=authorization-code&
redirect_uri=your-redirect-uri

关键点

  • 必要参数
    • client_id:客户端 ID
    • code_verifier:PKCE 流程中的 code_verifier
  • 其他参数:可能包括 grant_typecoderedirect_uri 等。

这种请求格式用于 OAuth 2.0 授权码 + PKCE 流程,公共客户端通过 POST 方法将自己的凭据发送给授权服务器,以获取访问令牌。



配置客户端认证方式

客户端的认证方式,在授权服务注册客户端时指定

在进行/oauth2/token请求时,才会进行下面的认证

RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())// 客户端ID和密码.clientId("test-client")//指定密钥,bcrypt密文,noop明文//.clientSecret("{bcrypt}" + new BCryptPasswordEncoder().encode("secret")).clientSecret("{noop}secret")// 客户端认证方式,这里指定使用请求头的Basic Auth.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC).build();

ClientAuthenticationMethod的认证方式在源码中如下:

其中的basic、post新版本已弃用

public final class ClientAuthenticationMethod implements Serializable {private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;/*** @deprecated Use {@link #CLIENT_SECRET_BASIC} 弃用*/@Deprecatedpublic static final ClientAuthenticationMethod BASIC = new ClientAuthenticationMethod("basic");/*** @since 5.5*/public static final ClientAuthenticationMethod CLIENT_SECRET_BASIC = new ClientAuthenticationMethod("client_secret_basic");/*** @deprecated Use {@link #CLIENT_SECRET_POST} 弃用*/@Deprecatedpublic static final ClientAuthenticationMethod POST = new ClientAuthenticationMethod("post");/*** @since 5.5*/public static final ClientAuthenticationMethod CLIENT_SECRET_POST = new ClientAuthenticationMethod("client_secret_post");/*** @since 5.5*/public static final ClientAuthenticationMethod CLIENT_SECRET_JWT = new ClientAuthenticationMethod("client_secret_jwt");/*** @since 5.5*/public static final ClientAuthenticationMethod PRIVATE_KEY_JWT = new ClientAuthenticationMethod("private_key_jwt");/*** @since 5.2*/public static final ClientAuthenticationMethod NONE = new ClientAuthenticationMethod("none");}


下面介绍源码中,spring-security-oauth2-client是如何向请求中添加客户端认证参数的

客户端发起请求

源码位置

开启oidc的授权码模式下,当授权服务认证完成时,会生成code并向客户端发起/login/oauth2/code路径的请求重定向,

该重定向请求会进入客户端OAuth2LoginAuthenticationFilter过滤器attemptAuthentication方法内,对code进行认证,然后再次发起新的用code换取token的请求

该过滤器中执行的核心代码如下:

public class OAuth2LoginAuthenticationFilter extends AbstractAuthenticationProcessingFilter {@Overridepublic Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)throws AuthenticationException {//..........// 此处会发起 /oauth2/token 请求,并在请求中添加客户端认证信息参数OAuth2LoginAuthenticationToken authenticationResult = (OAuth2LoginAuthenticationToken) this.getAuthenticationManager().authenticate(authenticationRequest);//..........}
}    

上面的authenticate(),调用的实现是ProviderManagerauthenticate方法,如下:

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {//..................try {//进行认证result = provider.authenticate(authentication);if (result != null) {copyDetails(authentication, result);break;}}//..................}  }

本文默认使用开启了oidc的授权码模式请求,所以上面的provider.authenticate(authentication)会继续调用OidcAuthorizationCodeAuthenticationProviderauthenticate方法,在这个authenticate内,执行如下代码:

public class OidcAuthorizationCodeAuthenticationProvider implements AuthenticationProvider {@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {//..................//获取授权服务响应的tokenOAuth2AccessTokenResponse accessTokenResponse = getResponse(authorizationCodeAuthentication);//..................}}

getResponse内会发起换取token的http请求,继续进入其中:

private OAuth2AccessTokenResponse getResponse(OAuth2LoginAuthenticationToken authorizationCodeAuthentication) {try {//由accessTokenResponseClient发起请求return this.accessTokenResponseClient.getTokenResponse(new OAuth2AuthorizationCodeGrantRequest(authorizationCodeAuthentication.getClientRegistration(),authorizationCodeAuthentication.getAuthorizationExchange()));}catch (OAuth2AuthorizationException ex) {OAuth2Error oauth2Error = ex.getError();throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex);}
}

accessTokenResponseClient通过不同的授权模式与认证方式,调用不同的实现去生成客户端认证信息,这里会根据grant_type即授权模式的不同,调用不同实现的getTokenResponse方法,实现类分别为:

  • DefaultAuthorizationCodeTokenResponseClient
  • DefaultClientCredentialsTokenResponseClient
  • DefaultJwtBearerTokenResponseClient
  • DefaultPasswordTokenResponseClient
  • DefaultRefreshTokenTokenResponseClient

以上为每种认证方式都会经过的步骤,本文以授权码模式举例说明,下面介绍每种认证方式的源码执行流程。


client_secret_basic认证方式

客户端启用client_secret_basic认证方式时,accessTokenResponseClient调用授权码模式的实现为DefaultAuthorizationCodeTokenResponseClient

只展示关键代码

public final class DefaultAuthorizationCodeTokenResponseClientimplements OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> {private Converter<OAuth2AuthorizationCodeGrantRequest, RequestEntity<?>> requestEntityConverter = new OAuth2AuthorizationCodeGrantRequestEntityConverter();//客户端请求token@Overridepublic OAuth2AccessTokenResponse getTokenResponse(OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest) {Assert.notNull(authorizationCodeGrantRequest, "authorizationCodeGrantRequest cannot be null");//这里通过转换方法向请求中添加客户端认证参数//requestEntityConverter就是上面的成员变量OAuth2AuthorizationCodeGrantRequestEntityConverterRequestEntity<?> request = this.requestEntityConverter.convert(authorizationCodeGrantRequest);ResponseEntity<OAuth2AccessTokenResponse> response = getResponse(request);return response.getBody();}
}    

上面convert方法执行的源码在OAuth2AuthorizationCodeGrantRequestEntityConverter的父类AbstractOAuth2AuthorizationGrantRequestEntityConverter

其中,OAuth2AuthorizationGrantRequestEntityUtils负责请求头认证信息生成

abstract class AbstractOAuth2AuthorizationGrantRequestEntityConverter<T extends AbstractOAuth2AuthorizationGrantRequest>implements Converter<T, RequestEntity<?>> {//实际负责转换的代码private Converter<T, HttpHeaders> headersConverter =(authorizationGrantRequest) -> OAuth2AuthorizationGrantRequestEntityUtils.getTokenRequestHeaders(authorizationGrantRequest.getClientRegistration());@Overridepublic RequestEntity<?> convert(T authorizationGrantRequest) {//向请求头添加认证信息,getHeadersConverter获取的是上面的headersConverter//此处通过OAuth2AuthorizationGrantRequestEntityUtils向请求头添加了Basic数据HttpHeaders headers = getHeadersConverter().convert(authorizationGrantRequest);MultiValueMap<String, String> parameters = getParametersConverter().convert(authorizationGrantRequest);URI uri = UriComponentsBuilder.fromUriString(authorizationGrantRequest.getClientRegistration().getProviderDetails().getTokenUri()).build().toUri();//创建请求实例,包含的关键信息://	请求头:Authentication Basic dGVzdC1jbGllbnQ6c2VjcmV0//  请求path:/oauth2/tokenreturn new RequestEntity<>(parameters, headers, HttpMethod.POST, uri);}}    

根据上面的分析,向请求中封装客户端认证信息的,就是OAuth2AuthorizationGrantRequestEntityUtils

OAuth2AuthorizationGrantRequestEntityUtils会通过传入的ClientRegistration,也就是客户端注册信息来判断其认证方式是不是client_secret_basic,如果是就使用url编码向请求头添加Authentication Basic信息

final class OAuth2AuthorizationGrantRequestEntityUtils {private static HttpHeaders DEFAULT_TOKEN_REQUEST_HEADERS = getDefaultTokenRequestHeaders();private OAuth2AuthorizationGrantRequestEntityUtils() {}//负责向请求头Authentication中添加Basic Auth信息static HttpHeaders getTokenRequestHeaders(ClientRegistration clientRegistration) {HttpHeaders headers = new HttpHeaders();//指定utf-8的MediaType类型headers.addAll(DEFAULT_TOKEN_REQUEST_HEADERS);//根据方法传入到客户端注册信息参数,判断是否为Basic认证方式if (ClientAuthenticationMethod.CLIENT_SECRET_BASIC.equals(clientRegistration.getClientAuthenticationMethod())|| ClientAuthenticationMethod.BASIC.equals(clientRegistration.getClientAuthenticationMethod())) {//如果是,进行URL编码String clientId = encodeClientCredential(clientRegistration.getClientId());String clientSecret = encodeClientCredential(clientRegistration.getClientSecret());headers.setBasicAuth(clientId, clientSecret);}return headers;}private static String encodeClientCredential(String clientCredential) {try {return URLEncoder.encode(clientCredential, StandardCharsets.UTF_8.toString());}catch (UnsupportedEncodingException ex) {// Will not happen since UTF-8 is a standard charsetthrow new IllegalArgumentException(ex);}}private static HttpHeaders getDefaultTokenRequestHeaders() {HttpHeaders headers = new HttpHeaders();headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON_UTF8));final MediaType contentType = MediaType.valueOf(MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8");headers.setContentType(contentType);return headers;}}

也就是说,当指定客户端认证方式为client_secret_basic时,其请求头的Authentication Basic信息就是在OAuth2AuthorizationGrantRequestEntityUtilsgetTokenRequestHeaders方法内生成的。


client_secret_post认证方式

client_secret_basic认证方式流程大体相同,区别是在AbstractOAuth2AuthorizationGrantRequestEntityConverter中的getParametersConverter()方法内向请求体添加认证参数

accessTokenResponseClient调用授权码模式的实现DefaultAuthorizationCodeTokenResponseClient

只展示关键代码

public final class DefaultAuthorizationCodeTokenResponseClientimplements OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> {private Converter<OAuth2AuthorizationCodeGrantRequest, RequestEntity<?>> requestEntityConverter = new OAuth2AuthorizationCodeGrantRequestEntityConverter();@Overridepublic OAuth2AccessTokenResponse getTokenResponse(OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest) {Assert.notNull(authorizationCodeGrantRequest, "authorizationCodeGrantRequest cannot be null");//requestEntityConverter就是上面的成员变量OAuth2AuthorizationCodeGrantRequestEntityConverterRequestEntity<?> request = this.requestEntityConverter.convert(authorizationCodeGrantRequest);ResponseEntity<OAuth2AccessTokenResponse> response = getResponse(request);return response.getBody();}
}    

上面convert方法执行的源码在OAuth2AuthorizationCodeGrantRequestEntityConverter的父类AbstractOAuth2AuthorizationGrantRequestEntityConverter

OAuth2AuthorizationGrantRequestEntityUtils负责请求头认证信息生成

abstract class AbstractOAuth2AuthorizationGrantRequestEntityConverter<T extends AbstractOAuth2AuthorizationGrantRequest>implements Converter<T, RequestEntity<?>> {//实际负责转换的代码private Converter<T, HttpHeaders> headersConverter =(authorizationGrantRequest) -> OAuth2AuthorizationGrantRequestEntityUtils.getTokenRequestHeaders(authorizationGrantRequest.getClientRegistration());@Overridepublic RequestEntity<?> convert(T authorizationGrantRequest) {//getHeadersConverter获取的是上面的headersConverterHttpHeaders headers = getHeadersConverter().convert(authorizationGrantRequest);//这里会判断认证方式是否为client_secret_post,如果是则向请求表单参数中添加客户端id与密码,而不是请求头的Basic AuthMultiValueMap<String, String> parameters = getParametersConverter().convert(authorizationGrantRequest);URI uri = UriComponentsBuilder.fromUriString(authorizationGrantRequest.getClientRegistration().getProviderDetails().getTokenUri()).build().toUri();//创建请求实例,包含的关键信息://	请求头:Authentication Basic dGVzdC1jbGllbnQ6c2VjcmV0//  请求path:/oauth2/tokenreturn new RequestEntity<>(parameters, headers, HttpMethod.POST, uri);}}    

根据上面的分析,向请求中封装客户端认证信息的,就是OAuth2AuthorizationGrantRequestEntityUtils

OAuth2AuthorizationGrantRequestEntityUtils会通过传入的ClientRegistration,也就是客户端注册信息来判断其认证方式是不是client_secret_basic,如果是就使用url编码向请求头添加Authentication Basic信息,如果不是就不添加

因为使用client_secret_post认证方式,所以这里请求头不会添加Basic Auth参数

final class OAuth2AuthorizationGrantRequestEntityUtils {private static HttpHeaders DEFAULT_TOKEN_REQUEST_HEADERS = getDefaultTokenRequestHeaders();private OAuth2AuthorizationGrantRequestEntityUtils() {}//负责向请求头Authentication中添加Basic Auth信息static HttpHeaders getTokenRequestHeaders(ClientRegistration clientRegistration) {HttpHeaders headers = new HttpHeaders();//指定utf-8的MediaType类型headers.addAll(DEFAULT_TOKEN_REQUEST_HEADERS);//根据方法传入到客户端注册信息参数,判断是否为Basic认证方式if (ClientAuthenticationMethod.CLIENT_SECRET_BASIC.equals(clientRegistration.getClientAuthenticationMethod())|| ClientAuthenticationMethod.BASIC.equals(clientRegistration.getClientAuthenticationMethod())) {//如果是,进行URL编码String clientId = encodeClientCredential(clientRegistration.getClientId());String clientSecret = encodeClientCredential(clientRegistration.getClientSecret());headers.setBasicAuth(clientId, clientSecret);}return headers;}private static String encodeClientCredential(String clientCredential) {try {return URLEncoder.encode(clientCredential, StandardCharsets.UTF_8.toString());}catch (UnsupportedEncodingException ex) {// Will not happen since UTF-8 is a standard charsetthrow new IllegalArgumentException(ex);}}private static HttpHeaders getDefaultTokenRequestHeaders() {HttpHeaders headers = new HttpHeaders();headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON_UTF8));final MediaType contentType = MediaType.valueOf(MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8");headers.setContentType(contentType);return headers;}}

因为使用client_secret_post认证方式,所以上面代码不会向请求头添加Authentication Basic信息,

而是在AbstractOAuth2AuthorizationGrantRequestEntityConverter中的getParametersConverter()方法内向请求表单添加认证参数

public class OAuth2AuthorizationCodeGrantRequestEntityConverterextends AbstractOAuth2AuthorizationGrantRequestEntityConverter<OAuth2AuthorizationCodeGrantRequest> {@Overrideprotected MultiValueMap<String, String> createParameters(OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest) {ClientRegistration clientRegistration = authorizationCodeGrantRequest.getClientRegistration();OAuth2AuthorizationExchange authorizationExchange = authorizationCodeGrantRequest.getAuthorizationExchange();MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();parameters.add(OAuth2ParameterNames.GRANT_TYPE, authorizationCodeGrantRequest.getGrantType().getValue());parameters.add(OAuth2ParameterNames.CODE, authorizationExchange.getAuthorizationResponse().getCode());String redirectUri = authorizationExchange.getAuthorizationRequest().getRedirectUri();String codeVerifier = authorizationExchange.getAuthorizationRequest().getAttribute(PkceParameterNames.CODE_VERIFIER);if (redirectUri != null) {parameters.add(OAuth2ParameterNames.REDIRECT_URI, redirectUri);}//如果不是client_secret_basic认证方式,则向请求参数添加客户端idif (!ClientAuthenticationMethod.CLIENT_SECRET_BASIC.equals(clientRegistration.getClientAuthenticationMethod())&& !ClientAuthenticationMethod.BASIC.equals(clientRegistration.getClientAuthenticationMethod())) {parameters.add(OAuth2ParameterNames.CLIENT_ID, clientRegistration.getClientId());}//如果是client_secret_post认证方式,则向请求参数添加客户端密码if (ClientAuthenticationMethod.CLIENT_SECRET_POST.equals(clientRegistration.getClientAuthenticationMethod())|| ClientAuthenticationMethod.POST.equals(clientRegistration.getClientAuthenticationMethod())) {parameters.add(OAuth2ParameterNames.CLIENT_SECRET, clientRegistration.getClientSecret());}if (codeVerifier != null) {parameters.add(PkceParameterNames.CODE_VERIFIER, codeVerifier);}//最后返回生成的参数,添加到请求中return parameters;}}

client_secret_jwt及private_key_jwt认证

需结合org.springframework.security.oauth2.client.endpoint.NimbusJwtClientAuthenticationParametersConverter做自定义配置实现



授权服务接收请求

OAuth2ClientAuthenticationFilter

对客户端token请求中的信息进行认证

protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)throws ServletException, IOException {//检查当前请求是否与 requestMatcher 匹配。如果不匹配,继续执行过滤链的下一个过滤器,并返回,表示这个过滤器不处理当前请求if (!this.requestMatcher.matches(request)) {filterChain.doFilter(request, response);return;}try {//将请求转换为 Authentication 权限对象。这个 Authentication 对象封装了客户端的认证信息//使用委托模式,遍历所有实现类执行convert方法看哪个支持就使用哪个进行转换Authentication authenticationRequest = this.authenticationConverter.convert(request);//如果 authenticationRequest 是 AbstractAuthenticationToken 的实例,//调用 setDetails 方法将请求的详细信息(如 IP 地址、session ID 等)设置到认证请求中if (authenticationRequest instanceof AbstractAuthenticationToken) {((AbstractAuthenticationToken) authenticationRequest).setDetails(this.authenticationDetailsSource.buildDetails(request));}if (authenticationRequest != null) {//验证客户端标识符。这个方法确保认证请求中包含有效的客户端标识符。validateClientIdentifier(authenticationRequest);//进行实际认证,使用委托模式,遍历所有实现类使用其supports方法判断哪个支持就用哪个验证Authentication authenticationResult = this.authenticationManager.authenticate(authenticationRequest);//认证成功处理this.authenticationSuccessHandler.onAuthenticationSuccess(request, response, authenticationResult);}//无论是否进行了认证,都调用 filterChain.doFilter(request, response) 方法继续执行过滤链的下一个过滤器//如果成功,就会向下后续由OAuth2TokenEndpointFilter进行token生成处理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);}
}

匹配的请求

匹配包含如下路径三种的POST请求

  • /oauth2/token
  • /oauth2/introspect
  • /oauth2/revoke

上面源码的

this.requestMatcher.matches(request)

OAuth2ClientAuthenticationConfigurer初始化时指定匹配规则

@Override
void init(HttpSecurity httpSecurity) {AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(httpSecurity);//规定了匹配的请求this.requestMatcher = new OrRequestMatcher(new AntPathRequestMatcher(authorizationServerSettings.getTokenEndpoint(),HttpMethod.POST.name()),new AntPathRequestMatcher(authorizationServerSettings.getTokenIntrospectionEndpoint(),HttpMethod.POST.name()),new AntPathRequestMatcher(authorizationServerSettings.getTokenRevocationEndpoint(),HttpMethod.POST.name()));List<AuthenticationProvider> authenticationProviders = createDefaultAuthenticationProviders(httpSecurity);if (!this.authenticationProviders.isEmpty()) {authenticationProviders.addAll(0, this.authenticationProviders);}this.authenticationProvidersConsumer.accept(authenticationProviders);authenticationProviders.forEach(authenticationProvider ->httpSecurity.authenticationProvider(postProcess(authenticationProvider)));
}

总结,OAuth2ClientAuthenticationFilter的处理大体分为三步:

  • authenticationConverter将过滤的请求转为Authentication认证对象
  • 使用authenticationManager进行认证
  • 使用Handler做认证成功或失败的处理,如果成功则向下执行其他过滤器

根据委托设计模式,authenticationConverter会将不同类型的请求转为不同的认证对象,authenticationManager又会根据不同类型的认证对象,使用不同的Provider进行认证




下面以认证方式为区分,分别介绍不同认证方式源码流程

授权服务jwt认证

对应客户端注册时指定client_secret_jwtprivate_key_jwt

RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())// 客户端ID和密钥.clientId("test-client").clientSecret("FjKNY8p2&Xw9Lqe$GH7Rd3Bt*5mZ4Pv#CV2sE6J!n")// 客户端认证方式.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT)//.clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT).build()

JwtClientAssertionAuthenticationConverter

客户端使用JWT认证方式时的请求转换器

请求的转换

JwtClientAssertionAuthenticationConverter 是一个请求转换器,负责把符合规范的请求转换为权限对象,转换的是包含以下参数的post请求:

  • client_assertion_type:值必须是 urn:ietf:params:oauth:client-assertion-type:jwt-bearer
  • client_assertion:JWT 断言的具体值。这是一个签名的 JWT,包含客户端身份信息。
  • client_id:客户端的 ID(必须存在并且唯一)。

如果请求中包含这些参数并且它们的值符合要求,则会将请求转换为一个 OAuth2ClientAuthenticationToken,该 token 包含了客户端 ID、认证方法(JWT 客户端断言)和 JWT 断言以及附加参数。

通过这种转换机制,Spring Security 能够识别并处理使用 JWT 客户端断言进行认证的 OAuth2 请求,从而实现对客户端的认证和授权。

示例 HTTP 请求

请求方法:POST

请求路径:/oauth2/token

请求头:

Content-Type: application/x-www-form-urlencoded

请求体表单参数(这里可以用任意授权模式,不一定是客户端凭证模式,只是演示):

client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&
client_assertion=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjbGllbnRJZCIsImF1ZCI6Imh0dHBzOi8vZXhhbXBsZS5jb20vb2F1dGgyL3Rva2VuIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c&
client_id=clientId&
grant_type=client_credentials

请求参数解释:

  1. client_assertion_type
    • 值为 urn:ietf:params:oauth:client-assertion-type:jwt-bearer,表示使用 JWT 作为客户端断言。

  2. client_assertion
    • JWT 断言的具体值。这是一个签名的 JWT,包含客户端身份信息。

  3. client_id
    • 客户端的 ID,用于标识客户端。

  4. grant_type
    • 授权类型,此处为 client_credentials

示例 JWT 断言(简化版):

{"alg": "RS256","typ": "JWT"
}
{"sub": "clientId","aud": "https://example.com/oauth2/token","iat": 1516239022
}

源码解析

JwtClientAssertionAuthenticationConverter 会从请求中提取 client_assertion_typeclient_assertion 参数,并验证其存在和格式。如果符合预期格式,则会创建一个 OAuth2ClientAuthenticationToken,其中包含客户端的 ID 和 JWT 断言,供后续的身份验证流程使用。

public final class JwtClientAssertionAuthenticationConverter implements AuthenticationConverter {private static final ClientAuthenticationMethod JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD =new ClientAuthenticationMethod("urn:ietf:params:oauth:client-assertion-type:jwt-bearer");@Nullable@Overridepublic Authentication convert(HttpServletRequest request) {//如果请求中取不到client_assertion_type或client_assertion参数,转换方法返回空if (request.getParameter(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE) == null ||request.getParameter(OAuth2ParameterNames.CLIENT_ASSERTION) == null) {return null;}//获取请求中的所有参数,存入mapMultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);// 请求必须带有client_assertion_type参数,且其值只能是一个,否则抛出invalid_request异常String clientAssertionType = parameters.getFirst(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE);if (parameters.get(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE).size() != 1) {throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);}// 请求中client_assertion_type属性的值如果不是'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'就返回nullif (!JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD.getValue().equals(clientAssertionType)) {return null;}// 请求必须带有client_assertion参数,且其值只能是一个,否则抛出invalid_request异常String jwtAssertion = parameters.getFirst(OAuth2ParameterNames.CLIENT_ASSERTION);if (parameters.get(OAuth2ParameterNames.CLIENT_ASSERTION).size() != 1) {throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);}// 如果请求中携带了client_id参数,其值必须是一个,否则抛出invalid_request异常String clientId = parameters.getFirst(OAuth2ParameterNames.CLIENT_ID);if (!StringUtils.hasText(clientId) ||parameters.get(OAuth2ParameterNames.CLIENT_ID).size() != 1) {throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);}// 获取请求中除了client_assertion_type、client_assertion、client_id之外的参数值存入additionalParametersMap<String, Object> additionalParameters = OAuth2EndpointUtils.getParametersIfMatchesAuthorizationCodeGrantRequest(request,OAuth2ParameterNames.CLIENT_ASSERTION_TYPE,OAuth2ParameterNames.CLIENT_ASSERTION,OAuth2ParameterNames.CLIENT_ID);// 结合验证过的请求参数创建权限对象并返回return new OAuth2ClientAuthenticationToken(clientId, JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD,jwtAssertion, additionalParameters);}}

委托模式执行转换获取结果

OAuth2ClientAuthenticationFilter过滤器的doFilterInternal方法中,如下代码会通过委托模式调用转换器来获取认证对象

Authentication authenticationRequest = this.authenticationConverter.convert(request);

委托模式的实现类DelegatingAuthenticationConverter获取实际转换器并返回认证对象

@Nullable
@Override
public Authentication convert(HttpServletRequest request) {Assert.notNull(request, "request cannot be null");//循环所有的converter实现,那个能转换成功,就返回那个成功的结果for (AuthenticationConverter converter : this.converters) {Authentication authentication = converter.convert(request);if (authentication != null) {return authentication;}}return null;
}

通过上面源码分析,如果请求包含client_assertion_typeclient_assertion参数,则会被JwtClientAssertionAuthenticationConverter转换成功并返回认证对象OAuth2ClientAuthenticationToken,交由Provider进行验证


JwtClientAssertionAuthenticationProvider

客户端使用JWT认证方式时的请求权限认证类

源码解析

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {// 将传入的Authentication对象转换为OAuth2ClientAuthenticationToken类型OAuth2ClientAuthenticationToken clientAuthentication = (OAuth2ClientAuthenticationToken) authentication;// 如果客户端的认证方法不是JWT客户端断言认证,则返回nullif (!JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD.equals(clientAuthentication.getClientAuthenticationMethod())) {return null;}// 获取客户端IDString clientId = clientAuthentication.getPrincipal().toString();// 根据客户端ID查找注册的客户端RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);if (registeredClient == null) {// 如果找不到注册的客户端,则抛出异常throwInvalidClient(OAuth2ParameterNames.CLIENT_ID);}// 如果日志级别为trace,则记录日志if (this.logger.isTraceEnabled()) {this.logger.trace("Retrieved registered client");}// 检查客户端是否支持PRIVATE_KEY_JWT或CLIENT_SECRET_JWT认证方法if (!registeredClient.getClientAuthenticationMethods().contains(ClientAuthenticationMethod.PRIVATE_KEY_JWT) &&!registeredClient.getClientAuthenticationMethods().contains(ClientAuthenticationMethod.CLIENT_SECRET_JWT)) {// 如果不支持,则抛出异常throwInvalidClient("authentication_method");}// 检查客户端凭据是否为空if (clientAuthentication.getCredentials() == null) {// 如果为空,则抛出异常throwInvalidClient("credentials");}// 初始化Jwt对象Jwt jwtAssertion = null;// 创建JwtDecoder对象,已通过构造方法指定为JwtClientAssertionDecoderFactoryJwtDecoder jwtDecoder = this.jwtDecoderFactory.createDecoder(registeredClient);try {// 使用JwtDecoder解码客户端凭据jwtAssertion = jwtDecoder.decode(clientAuthentication.getCredentials().toString());} catch (JwtException ex) {// 如果解码失败,则抛出异常throwInvalidClient(OAuth2ParameterNames.CLIENT_ASSERTION, ex);}// 如果日志级别为trace,则记录日志if (this.logger.isTraceEnabled()) {this.logger.trace("Validated client authentication parameters");}// 验证机密客户端的"code_verifier"参数,如果可用this.codeVerifierAuthenticator.authenticateIfAvailable(clientAuthentication, registeredClient);// 确定客户端认证方法ClientAuthenticationMethod clientAuthenticationMethod =registeredClient.getClientSettings().getTokenEndpointAuthenticationSigningAlgorithm() instanceof SignatureAlgorithm ?ClientAuthenticationMethod.PRIVATE_KEY_JWT :ClientAuthenticationMethod.CLIENT_SECRET_JWT;// 如果日志级别为trace,则记录日志if (this.logger.isTraceEnabled()) {this.logger.trace("Authenticated client assertion");}// 返回新的OAuth2ClientAuthenticationToken对象,其中包含已验证的客户端和JWT断言return new OAuth2ClientAuthenticationToken(registeredClient, clientAuthenticationMethod, jwtAssertion);
}

代码功能概述

  1. 类型转换和方法检查: 首先将传入的Authentication对象转换为OAuth2ClientAuthenticationToken类型,并检查其认证方法是否为JWT客户端断言认证。

  2. 客户端ID和注册客户端查找: 从Authentication对象中获取客户端ID,并在注册的客户端存储库中查找对应的RegisteredClient对象。如果找不到,抛出异常。

  3. 客户端认证方法检查: 确保注册的客户端支持PRIVATE_KEY_JWTCLIENT_SECRET_JWT认证方法,如果不支持,抛出异常。

  4. 客户端凭据检查: 检查客户端凭据是否为空,如果为空,抛出异常。

  5. JWT解码和验证: 使用JwtDecoder解码客户端凭据,生成JWT断言。如果解码失败,抛出异常。

  6. 验证code_verifier参数: 如果可用,验证机密客户端的code_verifier参数。

  7. 确定客户端认证方法: 根据客户端的签名算法确定认证方法是PRIVATE_KEY_JWT还是CLIENT_SECRET_JWT

  8. 返回已验证的身份验证令牌: 创建并返回一个新的OAuth2ClientAuthenticationToken对象,包含已验证的客户端和JWT断言。

认证完成后,则向下执行过滤器,由OAuth2TokenEndpointFilter进行token处理


解码器

上面源码中,通过JwtClientAssertionAuthenticationProvider构造方法制定了默认的解码器JwtClientAssertionDecoderFactory

public JwtClientAssertionAuthenticationProvider(RegisteredClientRepository registeredClientRepository,OAuth2AuthorizationService authorizationService) {Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");Assert.notNull(authorizationService, "authorizationService cannot be null");this.registeredClientRepository = registeredClientRepository;this.codeVerifierAuthenticator = new CodeVerifierAuthenticator(authorizationService);//指定默认解码器this.jwtDecoderFactory = new JwtClientAssertionDecoderFactory();
}

解码器JwtClientAssertionDecoderFactory中的buildDecoder方法构建了解析jwt的逻辑:

根据注册客户端RegisteredClient的设置来决定如何验证JWT签名。以下是逐行解释:

private static NimbusJwtDecoder buildDecoder(RegisteredClient registeredClient) {// 从注册客户端的设置中获取JWS算法JwsAlgorithm jwsAlgorithm = registeredClient.getClientSettings().getTokenEndpointAuthenticationSigningAlgorithm();// 如果JWS算法是签名算法(非对称加密)if (jwsAlgorithm instanceof SignatureAlgorithm) {// 获取JWK Set URLString jwkSetUrl = registeredClient.getClientSettings().getJwkSetUrl();// 如果JWK Set URL为空,则抛出异常if (!StringUtils.hasText(jwkSetUrl)) {OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT,"Failed to find a Signature Verifier for Client: '"+ registeredClient.getId()+ "'. Check to ensure you have configured the JWK Set URL.",JWT_CLIENT_AUTHENTICATION_ERROR_URI);throw new OAuth2AuthenticationException(oauth2Error);}// 使用JWK Set URL和签名算法创建并返回NimbusJwtDecoderreturn NimbusJwtDecoder.withJwkSetUri(jwkSetUrl).jwsAlgorithm((SignatureAlgorithm) jwsAlgorithm).build();}// 如果JWS算法是MAC算法(对称加密)if (jwsAlgorithm instanceof MacAlgorithm) {// 获取客户端密钥String clientSecret = registeredClient.getClientSecret();// 如果客户端密钥为空,则抛出异常if (!StringUtils.hasText(clientSecret)) {OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT,"Failed to find a Signature Verifier for Client: '"+ registeredClient.getId()+ "'. Check to ensure you have configured the client secret.",JWT_CLIENT_AUTHENTICATION_ERROR_URI);throw new OAuth2AuthenticationException(oauth2Error);}// 创建SecretKeySpec,用于对称加密SecretKeySpec secretKeySpec = new SecretKeySpec(clientSecret.getBytes(StandardCharsets.UTF_8),JCA_ALGORITHM_MAPPINGS.get(jwsAlgorithm));// 使用客户端密钥和MAC算法创建并返回NimbusJwtDecoderreturn NimbusJwtDecoder.withSecretKey(secretKeySpec).macAlgorithm((MacAlgorithm) jwsAlgorithm).build();}// 如果JWS算法既不是签名算法也不是MAC算法,则抛出异常OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT,"Failed to find a Signature Verifier for Client: '"+ registeredClient.getId()+ "'. Check to ensure you have configured a valid JWS Algorithm: '" + jwsAlgorithm + "'.",JWT_CLIENT_AUTHENTICATION_ERROR_URI);throw new OAuth2AuthenticationException(oauth2Error);
}

关键点解释

  1. JWS算法获取
    • RegisteredClient的设置中获取用于JWT签名的算法。

  2. 处理签名算法(非对称加密)
    • 检查JWS算法是否是SignatureAlgorithm的实例。
    • 获取JWK Set URL,用于验证JWT的签名。
    • 如果JWK Set URL为空,抛出OAuth2AuthenticationException异常。
    • 如果JWK Set URL存在,使用该URL和签名算法创建并返回NimbusJwtDecoder实例。

  3. 处理MAC算法(对称加密)
    • 检查JWS算法是否是MacAlgorithm的实例。
    • 获取客户端密钥clientSecret,用于对称加密。
    • 如果客户端密钥为空,抛出OAuth2AuthenticationException异常。
    • 如果客户端密钥存在,创建SecretKeySpec对象,用于对称加密。
    • 使用客户端密钥和MAC算法创建并返回NimbusJwtDecoder实例。

  4. 处理无效的JWS算法
    • 如果JWS算法既不是签名算法也不是MAC算法,抛出OAuth2AuthenticationException异常,提示配置无效的JWS算法。

以使用常用的HS256签名算法JWT为例,关键在于

JwsAlgorithm jwsAlgorithm = registeredClient.getClientSettings().getTokenEndpointAuthenticationSigningAlgorithm();

会去读取客户端注册配置,获取签名算法:

// 客户端相关配置
ClientSettings clientSettings = ClientSettings.builder()// 是否需要用户授权确认.requireAuthorizationConsent(true)//指定使用client_secret_jwt认证方式时的签名算法.tokenEndpointAuthenticationSigningAlgorithm(MacAlgorithm.HS256).build();

然后在:

SecretKeySpec secretKeySpec = new SecretKeySpec(clientSecret.getBytes(StandardCharsets.UTF_8),JCA_ALGORITHM_MAPPINGS.get(jwsAlgorithm));

中,获取客户端的密钥client-secret进行JWT解析


委托模式验证

委托模式执行转换获取结果

OAuth2ClientAuthenticationFilter过滤器的doFilterInternal方法中,在如下代码处通过委托模式调用Provider来验证认证对象OAuth2ClientAuthenticationToken

Authentication authenticationResult = this.authenticationManager.authenticate(authenticationRequest);

上面的authenticationManager会调用ProviderManager,遍历所有Provider的实现,执行其每个的supports方法,判断是否支持验证,此处以JwtClientAssertionAuthenticationProvider重写的supports方法为例:

@Override
public boolean supports(Class<?> authentication) {return OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication);
}

前面的JwtClientAssertionAuthenticationConverter转换器返回的OAuth2ClientAuthenticationToken,与JwtClientAssertionAuthenticationProvider重写的supports方法中判断的类型一致,所以可以被JwtClientAssertionAuthenticationProvider处理进行认证。

当多个Provider实现的supports方法判断的类型一致,则会依赖于实现类的具体认证方法进行处理,比如JwtClientAssertionAuthenticationProvider的认证方法中,会判断客户端的认证方法不是JWT客户端断言认证,不是则返回null。


实际流程

  1. 接收请求:客户端发送 POST 请求到授权服务器的 /oauth2/token 端点,包含所需的参数。

  2. 提取参数JwtClientAssertionAuthenticationConverter 从请求中提取 client_assertion_typeclient_assertionclient_id 参数。

  3. 验证参数

    • 检查 client_assertion_type 是否为 urn:ietf:params:oauth:client-assertion-type:jwt-bearer
    • 检查 client_assertion 是否存在并且只有一个值。
    • 检查 client_id 是否存在并且只有一个值。
  4. 创建认证对象:如果参数验证通过,创建一个 OAuth2ClientAuthenticationToken 对象,并填充相应的参数和附加参数。

  5. 返回认证对象:返回生成的认证对象供后续使用。




授权服务client_secret_basic认证

使用ClientSecretBasicAuthenticationConverter将请求转为认证对象,使用ClientSecretAuthenticationProvider对转换后的认证对象进行验证:

  • ClientSecretBasicAuthenticationConverter会从请求头Authorization参数中,取出Basic及其后面的客户端id与密钥的URL编码值,并解码取出密钥部分
  • ClientSecretAuthenticationProvider负责将取出的密钥部分,与授权服务中已注册客户端的密钥做对比,对比成功则验证通过

ClientSecretBasicAuthenticationConverter

示例请求

下面是一个可以被 ClientSecretBasicAuthenticationConverter 处理的 HTTP 请求示例:

请求方法:POST

请求路径:/oauth2/token

请求头:

Authorization: Basic Y2xpZW50SWQ6Y2xpZW50U2VjcmV0
Content-Type: application/x-www-form-urlencoded

请求体(这里可以用任意授权模式,不一定是客户端凭证模式,只是演示):

grant_type=client_credentials

请求头解释:

  1. Authorization

    • 值为 Basic Y2xpZW50SWQ6Y2xpZW50U2VjcmV0,表示 Basic 认证,其中 Y2xpZW50SWQ6Y2xpZW50U2VjcmV0clientId:clientSecret (客户端id:客户端密钥)经过 Base64 编码后的字符串。

请求处理流程

  1. 接收请求:客户端发送 POST 请求到授权服务器的 /oauth2/token 端点,包含所需的头部和参数。
  2. 提取头部ClientSecretBasicAuthenticationConverter 从请求中提取 Authorization 头部。
  3. 验证头部:检查头部是否存在,且类型是否为 Basic
  4. 解码凭证:将 Base64 编码的凭证部分解码为用户名和密码。
  5. 验证凭证:检查凭证是否包含用户名和密码两个部分,且不为空。
  6. 创建认证对象:如果所有检查通过,创建一个 OAuth2ClientAuthenticationToken 对象,并填充相应的参数和附加参数。
  7. 返回认证对象:返回生成的认证对象供后续使用。

源码解析

public final class ClientSecretBasicAuthenticationConverter implements AuthenticationConverter {@Nullable@Overridepublic Authentication convert(HttpServletRequest request) {//取出请求头中的Authorization参数值,如果为空返回nullString header = request.getHeader(HttpHeaders.AUTHORIZATION);if (header == null) {return null;}//斜杠小写s正则匹配的是不可见字符,包括空格、制表符、换页符等,//这里就是按空格拆分Authorization参数值String[] parts = header.split("\\s");//如果拆分出来的第一个值在忽略大小写情况下不是Basic,直接结束方法返回null,//从此处看出这个转换器匹配的是请求头Authorization参数值为'Basic ***'、携带未加密用户名密码的if (!parts[0].equalsIgnoreCase("Basic")) {return null;}//拆分完的Authorization参数值如果不是2个,直接抛出invalid_request异常if (parts.length != 2) {throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);}//解析Authorization参数值两段中的第二段,先转为utf-8字节,再用Base64解码,解析失败则抛出invalid_request异常byte[] decodedCredentials;try {decodedCredentials = Base64.getDecoder().decode(parts[1].getBytes(StandardCharsets.UTF_8));} catch (IllegalArgumentException ex) {throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST), ex);}//将解码后的凭证转换为字符串,并按 : 分割成用户名和密码。//检查分割后的数组是否包含用户名和密码两个部分,并且两部分内容都不为空。//如果不满足上面这些条件,抛出 OAuth2AuthenticationException 异常,表示请求无效。String credentialsString = new String(decodedCredentials, StandardCharsets.UTF_8);String[] credentials = credentialsString.split(":", 2);if (credentials.length != 2 ||!StringUtils.hasText(credentials[0]) ||!StringUtils.hasText(credentials[1])) {throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);}//尝试解码用户名和密码部分。如果解码失败,抛出 OAuth2AuthenticationException 异常,表示请求无效。String clientID;String clientSecret;try {clientID = URLDecoder.decode(credentials[0], StandardCharsets.UTF_8.name());clientSecret = URLDecoder.decode(credentials[1], StandardCharsets.UTF_8.name());} catch (Exception ex) {throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST), ex);}//如果解码成功,创建一个新的 OAuth2ClientAuthenticationToken 权限对象,//并将客户端 ID、认证方法(CLIENT_SECRET_BASIC)和客户端密钥作为参数传入。return new OAuth2ClientAuthenticationToken(clientID, ClientAuthenticationMethod.CLIENT_SECRET_BASIC, clientSecret,OAuth2EndpointUtils.getParametersIfMatchesAuthorizationCodeGrantRequest(request));}}


ClientSecretAuthenticationProvider

源码解析

这段代码是ClientSecretAuthenticationProvider类中的authenticate方法,用于处理客户端使用client_secret_basicclient_secret_post方法进行认证的逻辑。以下是逐行解释:

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {// 将传入的Authentication对象转换为OAuth2ClientAuthenticationToken对象OAuth2ClientAuthenticationToken clientAuthentication =(OAuth2ClientAuthenticationToken) authentication;// 检查客户端的认证方法是否为client_secret_basic或client_secret_postif (!ClientAuthenticationMethod.CLIENT_SECRET_BASIC.equals(clientAuthentication.getClientAuthenticationMethod()) &&!ClientAuthenticationMethod.CLIENT_SECRET_POST.equals(clientAuthentication.getClientAuthenticationMethod())) {return null;}// 获取客户端IDString clientId = clientAuthentication.getPrincipal().toString();// 从存储库中查找已注册的客户端信息RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);if (registeredClient == null) {throwInvalidClient(OAuth2ParameterNames.CLIENT_ID);}// 如果启用跟踪日志,则记录已检索到的客户端信息if (this.logger.isTraceEnabled()) {this.logger.trace("Retrieved registered client");}// 检查客户端注册信息中是否包含当前使用的认证方法if (!registeredClient.getClientAuthenticationMethods().contains(clientAuthentication.getClientAuthenticationMethod())) {throwInvalidClient("authentication_method");}// 检查客户端凭据是否为空if (clientAuthentication.getCredentials() == null) {throwInvalidClient("credentials");}// 获取客户端密钥String clientSecret = clientAuthentication.getCredentials().toString();// 验证客户端密钥是否匹配,使用委托模式调用DelegatingPasswordEncoder来进行对比if (!this.passwordEncoder.matches(clientSecret, registeredClient.getClientSecret())) {throwInvalidClient(OAuth2ParameterNames.CLIENT_SECRET);}// 检查客户端密钥是否过期if (registeredClient.getClientSecretExpiresAt() != null &&Instant.now().isAfter(registeredClient.getClientSecretExpiresAt())) {throwInvalidClient("client_secret_expires_at");}// 如果启用跟踪日志,则记录已验证的客户端认证参数if (this.logger.isTraceEnabled()) {this.logger.trace("Validated client authentication parameters");}// 验证保密客户端的“code_verifier”参数(如果可用)this.codeVerifierAuthenticator.authenticateIfAvailable(clientAuthentication, registeredClient);// 如果启用跟踪日志,则记录已认证的客户端密钥if (this.logger.isTraceEnabled()) {this.logger.trace("Authenticated client secret");}// 返回新的OAuth2ClientAuthenticationToken,表示认证成功return new OAuth2ClientAuthenticationToken(registeredClient,clientAuthentication.getClientAuthenticationMethod(), clientAuthentication.getCredentials());
}

源码流程概括

  1. 转换认证对象
    • 将传入的Authentication对象转换为OAuth2ClientAuthenticationToken对象。

  2. 验证认证方法
    • 检查客户端的认证方法是否为client_secret_basicclient_secret_post,如果不是,返回null表示不支持该认证方法。

  3. 获取客户端ID和查找已注册的客户端信息
    • 获取客户端ID,并从存储库中查找对应的已注册客户端信息。如果找不到,抛出异常。

  4. 检查已注册客户端的认证方法
    • 检查已注册客户端是否支持当前使用的认证方法,如果不支持,抛出异常。

  5. 验证客户端凭据
    • 检查客户端凭据是否为空。
    • 获取客户端密钥,并使用passwordEncoder验证请求中的密钥与已注册客户端密钥是否匹配。如果不匹配,抛出异常。

  6. 检查客户端密钥是否过期
    • 检查客户端密钥是否已过期,如果过期,抛出异常。

  7. 日志记录
    • 如果启用跟踪日志,则记录相关信息,如已检索到的客户端、已验证的客户端认证参数和已认证的客户端密钥。

  8. 验证code_verifier参数
    • 对于保密客户端,验证code_verifier参数(如果可用)。

  9. 返回认证结果
    • 返回新的OAuth2ClientAuthenticationToken,表示认证成功。

密钥匹配

ClientSecretAuthenticationProvider验证的关键点在于密钥的匹配验证,通过DelegatingPasswordEncodermatches方法:

DelegatingPasswordEncoder是Spring Security中的一个密码编码器,用于根据不同的密码编码算法来匹配密码,它可以根据密码的前缀来选择适当的编码器进行密码匹配

@Override
public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {//如果rawPassword和prefixEncodedPassword都为null,则认为匹配成功。//这是为了处理特殊情况,比如在密码为空的情况下进行比较。if (rawPassword == null && prefixEncodedPassword == null) {return true;}//提取出密码编码器的ID。这个ID用于确定使用哪个具体的PasswordEncoder进行密码匹配String id = extractId(prefixEncodedPassword);//根据提取出的ID从idToPasswordEncoder映射中获取具体的PasswordEncoder实例。PasswordEncoder delegate = this.idToPasswordEncoder.get(id);//如果没有找到对应的编码器,则使用默认的密码匹配器进行验证。if (delegate == null) {return this.defaultPasswordEncoderForMatches.matches(rawPassword, prefixEncodedPassword);}//提取出编码后的密码部分,然后使用对应的PasswordEncoder进行实际的密码匹配操作String encodedPassword = extractEncodedPassword(prefixEncodedPassword);return delegate.matches(rawPassword, encodedPassword);
}

对于一个密码{bcrypt}$2a$10$...extractId方法提取到的ID是bcrypt,然后从idToPasswordEncoder映射中获取BCryptPasswordEncoder实例来验证密码。

DelegatingPasswordEncoder下的密码编码器实现有很多,具体参考如下路径源码的注解:

org.springframework.security.crypto.password.DelegatingPasswordEncoder
String idForEncode = "bcrypt";Map<String,PasswordEncoder> encoders = new HashMap<>();encoders.put(idForEncode, new BCryptPasswordEncoder());encoders.put("noop", NoOpPasswordEncoder.getInstance());encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());encoders.put("scrypt", new SCryptPasswordEncoder());encoders.put("sha256", new StandardPasswordEncoder()); PasswordEncoder passwordEncoder = new DelegatingPasswordEncoder(idForEncode, encoders);


授权服务client_secret_post认证

使用ClientSecretPostAuthenticationConverter将请求转为认证对象,使用ClientSecretAuthenticationProvider对转换后的认证对象进行验证:

  • ClientSecretBasicAuthenticationConverter会从请求体表单参数中取出client_secret的值,这里的client_secret未经过任何加密
  • ClientSecretAuthenticationProvider负责将取出的密钥部分与存储中的客户端信息密钥做对比,对比成功则验证通过

ClientSecretPostAuthenticationConverter

ClientSecretPostAuthenticationConverter 用于将通过 POST 请求方式提交客户端 ID 和客户端密钥的请求转换为 OAuth2ClientAuthenticationToken 对象。这种转换器主要用于 OAuth2 客户端认证。

转换的请求

ClientSecretPostAuthenticationConverter 转换的请求是通过 POST 方法提交的,内容包含 client_idclient_secret 参数。

请求格式如下:

POST /oauth2/token HTTP/1.1
Host: authorization-server.com
Content-Type: application/x-www-form-urlencodedclient_id=your-client-id&
client_secret=your-client-secret&
grant_type=authorization_code&
code=authorization-code&
redirect_uri=your-redirect-uri

关键点

  • 请求方法:POST
  • Content-Typeapplication/x-www-form-urlencoded
  • 必要参数
    • client_id:客户端 ID
    • client_secret:客户端密钥
  • 其他参数:可能包括 grant_typecoderedirect_uri 等,如果有的话。

这种请求格式用于 OAuth 2.0 客户端凭据授予流程,客户端通过 POST 方法将自己的凭据发送给授权服务器,以获取访问令牌。

源码解析

public final class ClientSecretPostAuthenticationConverter implements AuthenticationConverter {@Nullable@Overridepublic Authentication convert(HttpServletRequest request) {MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);// 取出请求中携带的client_id参数值,如果为空返回nullString clientId = parameters.getFirst(OAuth2ParameterNames.CLIENT_ID);if (!StringUtils.hasText(clientId)) {return null;}// client_id参数值只能是1个,否则抛出invalid_request异常if (parameters.get(OAuth2ParameterNames.CLIENT_ID).size() != 1) {throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);}// 取出请求中携带的client_secret参数值,如果为空返回nullString clientSecret = parameters.getFirst(OAuth2ParameterNames.CLIENT_SECRET);if (!StringUtils.hasText(clientSecret)) {return null;}// client_secret参数值只能是1个,否则抛出invalid_request异常if (parameters.get(OAuth2ParameterNames.CLIENT_SECRET).size() != 1) {throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);}// 获取其他请求参数,这些参数必须匹配授权码授权请求的格式,并排除 client_id 和 client_secret 参数Map<String, Object> additionalParameters = OAuth2EndpointUtils.getParametersIfMatchesAuthorizationCodeGrantRequest(request,OAuth2ParameterNames.CLIENT_ID,OAuth2ParameterNames.CLIENT_SECRET);// 创建权限对象并返回,其中包含客户端 ID、认证方法(CLIENT_SECRET_POST)、客户端密钥和额外的参数。return new OAuth2ClientAuthenticationToken(clientId, ClientAuthenticationMethod.CLIENT_SECRET_POST, clientSecret,additionalParameters);}}

认证

见上面的ClientSecretAuthenticationProvider

client_secret_postclient_secret_basic均使用ClientSecretAuthenticationProvider进行验证:

client_secret_postclient_secret_basic 的区别在于它们的客户端凭证传递方式不同:

  1. client_secret_post:客户端将客户端ID和客户端密钥作为请求体参数发送。这种方法的安全性较低,因为客户端密钥以明文形式发送。
  2. client_secret_basic:客户端将客户端ID和客户端密钥编码为Base64,并将其作为HTTP Basic认证的头部发送。这种方法比client_secret_post稍微安全一些,因为客户端密钥在传输时经过了Base64编码,但仍然不提供足够的安全性。



授权服务PKCE认证

PublicClientAuthenticationConverter

PublicClientAuthenticationConverter 是一个用于处理公共客户端认证请求的转换器。公共客户端通常是在没有客户端密钥的情况下进行认证的,例如通过使用 OAuth 2.0 授权码 + PKCE(Proof Key for Code Exchange) 流程进行认证。这个转换器会将符合条件的请求转换为 OAuth2ClientAuthenticationToken 对象。

转换的请求

PublicClientAuthenticationConverter 转换的请求是通过 POST 方法提交的,内容包含 client_idcode_verifier 参数,通常用于 OAuth 2.0 授权码 + PKCE 流程。

请求格式如下:

POST /oauth2/token HTTP/1.1
Host: authorization-server.com
Content-Type: application/x-www-form-urlencodedclient_id=your-client-id&
code_verifier=your-code-verifier&
grant_type=authorization_code&
code=authorization-code&
redirect_uri=your-redirect-uri

关键点

  • 请求方法:POST
  • Content-Typeapplication/x-www-form-urlencoded
  • 必要参数
    • client_id:客户端 ID
    • code_verifier:PKCE 流程中的 code_verifier
  • 其他参数:可能包括 grant_typecoderedirect_uri 等。

这种请求格式用于 OAuth 2.0 授权码 + PKCE 流程,公共客户端通过 POST 方法将自己的凭据发送给授权服务器,以获取访问令牌。

源码分析

public final class PublicClientAuthenticationConverter implements AuthenticationConverter {@Nullable@Overridepublic Authentication convert(HttpServletRequest request) {// 请求必须携带code_verifier参数且不为null;// 请求的grant_type参数值必须是'authorization_code',且code参数不能为空。// 即:检查请求是否匹配 PKCE 令牌请求。如果请求不匹配,则返回 null,表示无法进行转换。if (!OAuth2EndpointUtils.matchesPkceTokenRequest(request)) {return null;}//获取请求中的所有参数及其值MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);//获取 client_id 参数,并检查它是否为空。//如果为空或者 client_id 参数的值不唯一,则抛出 invalid_request异常,表示请求无效String clientId = parameters.getFirst(OAuth2ParameterNames.CLIENT_ID);if (!StringUtils.hasText(clientId) ||parameters.get(OAuth2ParameterNames.CLIENT_ID).size() != 1) {throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);}// code_verifier必须不为空且必须只有1个值if (parameters.get(PkceParameterNames.CODE_VERIFIER).size() != 1) {throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);}// 从请求中移除client_id//从参数列表中移除client_id参数目的是为了确保在创建 OAuth2ClientAuthenticationToken 对象时不会包含此参数。parameters.remove(OAuth2ParameterNames.CLIENT_ID);// 创建权限对象并返回,其中包含客户端 ID、认证方法(ClientAuthenticationMethod.NONE)、客户端密钥(此处为 null)和额外的参数return new OAuth2ClientAuthenticationToken(clientId, ClientAuthenticationMethod.NONE, null,new HashMap<>(parameters.toSingleValueMap()));}
}

这篇关于Spring OAuth2客户端身份验证源码解析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1098212

相关文章

网页解析 lxml 库--实战

lxml库使用流程 lxml 是 Python 的第三方解析库,完全使用 Python 语言编写,它对 XPath表达式提供了良好的支 持,因此能够了高效地解析 HTML/XML 文档。本节讲解如何通过 lxml 库解析 HTML 文档。 pip install lxml lxm| 库提供了一个 etree 模块,该模块专门用来解析 HTML/XML 文档,下面来介绍一下 lxml 库

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去

Security OAuth2 单点登录流程

单点登录(英语:Single sign-on,缩写为 SSO),又译为单一签入,一种对于许多相互关连,但是又是各自独立的软件系统,提供访问控制的属性。当拥有这项属性时,当用户登录时,就可以获取所有系统的访问权限,不用对每个单一系统都逐一登录。这项功能通常是以轻型目录访问协议(LDAP)来实现,在服务器上会将用户信息存储到LDAP数据库中。相同的,单一注销(single sign-off)就是指

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

Spring Security--Architecture Overview

1 核心组件 这一节主要介绍一些在Spring Security中常见且核心的Java类,它们之间的依赖,构建起了整个框架。想要理解整个架构,最起码得对这些类眼熟。 1.1 SecurityContextHolder SecurityContextHolder用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

Java架构师知识体认识

源码分析 常用设计模式 Proxy代理模式Factory工厂模式Singleton单例模式Delegate委派模式Strategy策略模式Prototype原型模式Template模板模式 Spring5 beans 接口实例化代理Bean操作 Context Ioc容器设计原理及高级特性Aop设计原理Factorybean与Beanfactory Transaction 声明式事物

Java进阶13讲__第12讲_1/2

多线程、线程池 1.  线程概念 1.1  什么是线程 1.2  线程的好处 2.   创建线程的三种方式 注意事项 2.1  继承Thread类 2.1.1 认识  2.1.2  编码实现  package cn.hdc.oop10.Thread;import org.slf4j.Logger;import org.slf4j.LoggerFactory