本文介绍在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-urlencoded
client_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-urlencoded
client_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} 弃用
*/
@Deprecated
public 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} 弃用
*/
@Deprecated
public 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 {
@Override
public 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 {
@Override
public 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 {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
//..................
//获取授权服务响应的token
OAuth2AccessTokenResponse 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 DefaultAuthorizationCodeTokenResponseClient
implements OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> {
private Converter<OAuth2AuthorizationCodeGrantRequest, RequestEntity<?>> requestEntityConverter = new OAuth2AuthorizationCodeGrantRequestEntityConverter();
//客户端请求token
@Override
public OAuth2AccessTokenResponse getTokenResponse(
OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest) {
Assert.notNull(authorizationCodeGrantRequest, "authorizationCodeGrantRequest cannot be null");
//这里通过转换方法向请求中添加客户端认证参数
//requestEntityConverter就是上面的成员变量OAuth2AuthorizationCodeGrantRequestEntityConverter
RequestEntity<?> 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());
@Override
public 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/token
return 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 charset
throw 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 DefaultAuthorizationCodeTokenResponseClient
implements OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> {
private Converter<OAuth2AuthorizationCodeGrantRequest, RequestEntity<?>> requestEntityConverter = new OAuth2AuthorizationCodeGrantRequestEntityConverter();
@Override
public OAuth2AccessTokenResponse getTokenResponse(
OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest) {
Assert.notNull(authorizationCodeGrantRequest, "authorizationCodeGrantRequest cannot be null");
//requestEntityConverter就是上面的成员变量OAuth2AuthorizationCodeGrantRequestEntityConverter
RequestEntity<?> 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());
@Override
public RequestEntity<?> convert(T authorizationGrantRequest) {
//getHeadersConverter获取的是上面的headersConverter
HttpHeaders headers = getHeadersConverter().convert(authorizationGrantRequest);
//这里会判断认证方式是否为client_secret_post,如果是则向请求表单参数中添加客户端id与密码,而不是请求头的Basic Auth
MultiValueMap<String, String> parameters = getParametersConverter().convert(authorizationGrantRequest);
URI uri = UriComponentsBuilder
.fromUriString(authorizationGrantRequest.getClientRegistration().getProviderDetails().getTokenUri())
.build().toUri();
//创建请求实例,包含的关键信息:
// 请求头:Authentication Basic dGVzdC1jbGllbnQ6c2VjcmV0
// 请求path:/oauth2/token
return 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 charset
throw 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 OAuth2AuthorizationCodeGrantRequestEntityConverter
extends AbstractOAuth2AuthorizationGrantRequestEntityConverter<OAuth2AuthorizationCodeGrantRequest> {
@Override
protected 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认证方式,则向请求参数添加客户端id
if (!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
@Override
public 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;
}
//获取请求中的所有参数,存入map
MultiValueMap<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'就返回null
if (!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之外的参数值存入additionalParameters
Map<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客户端断言认证,则返回null
if (!JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD.equals(clientAuthentication.getClientAuthenticationMethod())) {
return null;
}
// 获取客户端ID
String 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对象,已通过构造方法指定为JwtClientAssertionDecoderFactory
JwtDecoder 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 URL
String 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和签名算法创建并返回NimbusJwtDecoder
return 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算法创建并返回NimbusJwtDecoder
return 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
@Override
public Authentication convert(HttpServletRequest request) {
//取出请求头中的Authorization参数值,如果为空返回null
String 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_post
if (!ClientAuthenticationMethod.CLIENT_SECRET_BASIC.equals(clientAuthentication.getClientAuthenticationMethod()) &&
!ClientAuthenticationMethod.CLIENT_SECRET_POST.equals(clientAuthentication.getClientAuthenticationMethod())) {
return null;
}
// 获取客户端ID
String 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-urlencoded
client_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
@Override
public Authentication convert(HttpServletRequest request) {
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
// 取出请求中携带的client_id参数值,如果为空返回null
String 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参数值,如果为空返回null
String 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-urlencoded
client_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
@Override
public 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()));
}
}