本文主要是介绍Spring OAuth2客户端身份验证源码解析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
本文介绍在spring oauth2.0客户端向授权服务发起token请求时,源码是如何向请求中添加客户端认证参数,来交由授权服务进行认证的
版本信息
Spring Boot 2.7.10
spring-security-oauth2-client 5.7.7
认证方式
先介绍自带的四种客户端认证方式
jwt认证方式
对应的是ClientAuthenticationMethod
类中的client_secret_jwt
与private_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
请求参数解释:
client_assertion_type
:- 值为
urn:ietf:params:oauth:client-assertion-type:jwt-bearer
(固定值),表示使用 JWT 作为客户端断言。
- 值为
client_assertion
:- JWT 断言的具体值。这是一个签名的 JWT 字符串,包含客户端身份信息。
- JWT 断言的具体值。这是一个签名的 JWT 字符串,包含客户端身份信息。
client_id
:- 客户端的 ID,用于标识客户端。
- 客户端的 ID,用于标识客户端。
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
请求头解释:
-
Authorization
- 值为
Basic Y2xpZW50SWQ6Y2xpZW50U2VjcmV0
,表示 Basic 认证,其中Y2xpZW50SWQ6Y2xpZW50U2VjcmV0
是clientId: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
:客户端 IDclient_secret
:客户端密钥
- 其他参数:可能包括
grant_type
、code
和redirect_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
:客户端 IDcode_verifier
:PKCE 流程中的 code_verifier
- 其他参数:可能包括
grant_type
、code
和redirect_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()
,调用的实现是ProviderManager
的authenticate
方法,如下:
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)
会继续调用OidcAuthorizationCodeAuthenticationProvider
的authenticate
方法,在这个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
信息就是在OAuth2AuthorizationGrantRequestEntityUtils
的getTokenRequestHeaders
方法内生成的。
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_jwt
及private_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
请求参数解释:
client_assertion_type
:- 值为
urn:ietf:params:oauth:client-assertion-type:jwt-bearer
,表示使用 JWT 作为客户端断言。
- 值为
client_assertion
:- JWT 断言的具体值。这是一个签名的 JWT,包含客户端身份信息。
- JWT 断言的具体值。这是一个签名的 JWT,包含客户端身份信息。
client_id
:- 客户端的 ID,用于标识客户端。
- 客户端的 ID,用于标识客户端。
grant_type
:- 授权类型,此处为
client_credentials
。
- 授权类型,此处为
示例 JWT 断言(简化版):
{"alg": "RS256","typ": "JWT"
}
{"sub": "clientId","aud": "https://example.com/oauth2/token","iat": 1516239022
}
源码解析
JwtClientAssertionAuthenticationConverter
会从请求中提取client_assertion_type
和client_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_type
及client_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);
}
代码功能概述
-
类型转换和方法检查: 首先将传入的
Authentication
对象转换为OAuth2ClientAuthenticationToken
类型,并检查其认证方法是否为JWT客户端断言认证。 -
客户端ID和注册客户端查找: 从
Authentication
对象中获取客户端ID,并在注册的客户端存储库中查找对应的RegisteredClient
对象。如果找不到,抛出异常。 -
客户端认证方法检查: 确保注册的客户端支持
PRIVATE_KEY_JWT
或CLIENT_SECRET_JWT
认证方法,如果不支持,抛出异常。 -
客户端凭据检查: 检查客户端凭据是否为空,如果为空,抛出异常。
-
JWT解码和验证: 使用
JwtDecoder
解码客户端凭据,生成JWT断言。如果解码失败,抛出异常。 -
验证
code_verifier
参数: 如果可用,验证机密客户端的code_verifier
参数。 -
确定客户端认证方法: 根据客户端的签名算法确定认证方法是
PRIVATE_KEY_JWT
还是CLIENT_SECRET_JWT
。 -
返回已验证的身份验证令牌: 创建并返回一个新的
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);
}
关键点解释
- JWS算法获取:
- 从
RegisteredClient
的设置中获取用于JWT签名的算法。
- 从
- 处理签名算法(非对称加密):
- 检查JWS算法是否是
SignatureAlgorithm
的实例。 - 获取JWK Set URL,用于验证JWT的签名。
- 如果JWK Set URL为空,抛出
OAuth2AuthenticationException
异常。 - 如果JWK Set URL存在,使用该URL和签名算法创建并返回
NimbusJwtDecoder
实例。
- 检查JWS算法是否是
- 处理MAC算法(对称加密):
- 检查JWS算法是否是
MacAlgorithm
的实例。 - 获取客户端密钥
clientSecret
,用于对称加密。 - 如果客户端密钥为空,抛出
OAuth2AuthenticationException
异常。 - 如果客户端密钥存在,创建
SecretKeySpec
对象,用于对称加密。 - 使用客户端密钥和MAC算法创建并返回
NimbusJwtDecoder
实例。
- 检查JWS算法是否是
- 处理无效的JWS算法:
- 如果JWS算法既不是签名算法也不是MAC算法,抛出
OAuth2AuthenticationException
异常,提示配置无效的JWS算法。
- 如果JWS算法既不是签名算法也不是MAC算法,抛出
以使用常用的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。
实际流程
-
接收请求:客户端发送 POST 请求到授权服务器的
/oauth2/token
端点,包含所需的参数。 -
提取参数:
JwtClientAssertionAuthenticationConverter
从请求中提取client_assertion_type
、client_assertion
和client_id
参数。 -
验证参数:
- 检查
client_assertion_type
是否为urn:ietf:params:oauth:client-assertion-type:jwt-bearer
。 - 检查
client_assertion
是否存在并且只有一个值。 - 检查
client_id
是否存在并且只有一个值。
- 检查
-
创建认证对象:如果参数验证通过,创建一个
OAuth2ClientAuthenticationToken
对象,并填充相应的参数和附加参数。 -
返回认证对象:返回生成的认证对象供后续使用。
授权服务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
请求头解释:
-
Authorization
- 值为
Basic Y2xpZW50SWQ6Y2xpZW50U2VjcmV0
,表示 Basic 认证,其中Y2xpZW50SWQ6Y2xpZW50U2VjcmV0
是clientId:clientSecret
(客户端id:客户端密钥)经过 Base64 编码后的字符串。
- 值为
请求处理流程
- 接收请求:客户端发送 POST 请求到授权服务器的
/oauth2/token
端点,包含所需的头部和参数。 - 提取头部:
ClientSecretBasicAuthenticationConverter
从请求中提取Authorization
头部。 - 验证头部:检查头部是否存在,且类型是否为
Basic
。 - 解码凭证:将 Base64 编码的凭证部分解码为用户名和密码。
- 验证凭证:检查凭证是否包含用户名和密码两个部分,且不为空。
- 创建认证对象:如果所有检查通过,创建一个
OAuth2ClientAuthenticationToken
对象,并填充相应的参数和附加参数。 - 返回认证对象:返回生成的认证对象供后续使用。
源码解析
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_basic
或client_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());
}
源码流程概括
- 转换认证对象:
- 将传入的
Authentication
对象转换为OAuth2ClientAuthenticationToken
对象。
- 将传入的
- 验证认证方法:
- 检查客户端的认证方法是否为
client_secret_basic
或client_secret_post
,如果不是,返回null
表示不支持该认证方法。
- 检查客户端的认证方法是否为
- 获取客户端ID和查找已注册的客户端信息:
- 获取客户端ID,并从存储库中查找对应的已注册客户端信息。如果找不到,抛出异常。
- 获取客户端ID,并从存储库中查找对应的已注册客户端信息。如果找不到,抛出异常。
- 检查已注册客户端的认证方法:
- 检查已注册客户端是否支持当前使用的认证方法,如果不支持,抛出异常。
- 检查已注册客户端是否支持当前使用的认证方法,如果不支持,抛出异常。
- 验证客户端凭据:
- 检查客户端凭据是否为空。
- 获取客户端密钥,并使用
passwordEncoder
验证请求中的密钥与已注册客户端密钥是否匹配。如果不匹配,抛出异常。
- 检查客户端密钥是否过期:
- 检查客户端密钥是否已过期,如果过期,抛出异常。
- 检查客户端密钥是否已过期,如果过期,抛出异常。
- 日志记录:
- 如果启用跟踪日志,则记录相关信息,如已检索到的客户端、已验证的客户端认证参数和已认证的客户端密钥。
- 如果启用跟踪日志,则记录相关信息,如已检索到的客户端、已验证的客户端认证参数和已认证的客户端密钥。
- 验证
code_verifier
参数:- 对于保密客户端,验证
code_verifier
参数(如果可用)。
- 对于保密客户端,验证
- 返回认证结果:
- 返回新的
OAuth2ClientAuthenticationToken
,表示认证成功。
- 返回新的
密钥匹配
ClientSecretAuthenticationProvider
验证的关键点在于密钥的匹配验证,通过DelegatingPasswordEncoder
的matches
方法:
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_id
和 client_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-Type:
application/x-www-form-urlencoded
- 必要参数:
client_id
:客户端 IDclient_secret
:客户端密钥
- 其他参数:可能包括
grant_type
、code
和redirect_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_post
与client_secret_basic
均使用ClientSecretAuthenticationProvider
进行验证:
client_secret_post
和 client_secret_basic
的区别在于它们的客户端凭证传递方式不同:
client_secret_post
:客户端将客户端ID和客户端密钥作为请求体参数发送。这种方法的安全性较低,因为客户端密钥以明文形式发送。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_id
和 code_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-Type:
application/x-www-form-urlencoded
- 必要参数:
client_id
:客户端 IDcode_verifier
:PKCE 流程中的 code_verifier
- 其他参数:可能包括
grant_type
、code
和redirect_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客户端身份验证源码解析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!