OAuth 2.0 的授权码流程(Authorization Code Flow)是目前比较流行的一种授权模式,用于允许客户端(通常是Web应用)代表用户向资源服务器请求访问令牌(access token)。然而,原始的授权码流程对于移动应用和单页应用(SPA)等公有客户端来说并不安全,因为授权码容易被窃取。因此,引入了PKCE(Proof Key for Code Exchange)以增强授权码流程的安全性。
PKCE介绍
什么是 PKCE?
PKCE 是“Proof Key for Code Exchange”的缩写,它是一种安全增强机制,主要用于公共客户端,如移动应用和单页应用。PKCE 的工作原理是通过增加一个动态密钥来防止授权码被劫持。它在 OAuth 2.0 授权码流程的基础上,增加了两个新的参数:code_challenge
和 code_verifier
。
PKCE 授权码流程
PKCE 流程与标准的授权码流程类似,但在几个关键步骤上进行了增强:
-
客户端创建 code_verifier:
- 客户端生成一个高熵的随机字符串,称为
code_verifier
。 - 使用哈希算法(通常是 SHA-256)对
code_verifier
进行哈希运算,生成code_challenge
。 - 如果没有使用哈希算法,也可以直接将
code_verifier
作为code_challenge
。
- 客户端生成一个高熵的随机字符串,称为
-
客户端发起授权请求:
- 客户端将
code_challenge
以及其他必要的授权参数(如client_id
、redirect_uri
等)一起发送到授权服务器。
- 客户端将
-
用户认证和授权:
- 用户在授权服务器上进行认证并授权客户端访问其资源,授权服务器保存认证数据,包括客户端发来的
code_challenge
。 - 授权服务器生成授权码,并将其重定向回客户端的
redirect_uri
。
- 用户在授权服务器上进行认证并授权客户端访问其资源,授权服务器保存认证数据,包括客户端发来的
-
客户端交换授权码:
- 客户端接收到授权码后,将授权码和
code_verifier
发送到授权服务器,以交换访问令牌。
- 客户端接收到授权码后,将授权码和
-
服务器验证 code_verifier:
- 授权服务器接收到请求后,使用相同的哈希算法(如果适用)对
code_verifier
进行哈希运算,并验证结果是否与之前认证时保存的code_challenge
匹配。 - 如果匹配,则授权服务器返回访问令牌给客户端。
- 授权服务器接收到请求后,使用相同的哈希算法(如果适用)对
PKCE 授权码流程示意图
+--------+ +---------------+
| |--(A)- Authorization Request -------------->| |
| | + code_challenge & code_challenge_method |
| | | Authorization |
| |<--(B)---- Authorization Code -------------| Server |
| | | |
| Client |--(C)-- Access Token Request -------------->| |
| | + code_verifier | |
| | | |
| |<--(D)------ Access Token -----------------| |
+--------+ +---------------+
PKCE 流程的参数
- code_verifier:
- 随机生成的高熵字符串,长度在 43 到 128 个字符之间。
- 可以包含字符范围为 A-Z、a-z、0-9、-._~(Base64 URL 安全字符)。
- code_challenge:
- 对
code_verifier
进行哈希运算后的结果(如果使用了 S256 算法),或者直接使用code_verifier
。 - 两种生成方式:
plain
:code_challenge = code_verifierS256
:code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))
- 对
- code_challenge_method:
- 用于指定
code_challenge
的生成方式。 - 可以是
plain
或S256
,默认为plain
。
- 用于指定
PKCE 的优点
- 增强安全性:PKCE 防止授权码被拦截和滥用,因为授权服务器需要验证
code_verifier
与code_challenge
的匹配关系。 - 适用于公共客户端:尤其是单页应用(SPA)和移动应用,PKCE 能够提高其安全性,避免泄露客户端凭证。
实际应用中的 PKCE
以下是一个使用 PKCE 的 OAuth 2.0 授权码流程的具体示例:
-
生成 code_verifier 和 code_challenge:
// 生成随机的 code_verifier String codeVerifier = generateRandomString(); // 使用 SHA-256 生成 code_challenge String codeChallenge = Base64.getUrlEncoder().withoutPadding().encodeToString( MessageDigest.getInstance("SHA-256").digest(codeVerifier.getBytes(StandardCharsets.UTF_8)) );
-
发起授权请求:
GET /authorize?response_type=code&client_id=client_id&redirect_uri=https://client.example.com/cb &code_challenge=codeChallenge&code_challenge_method=S256
-
交换授权码:
POST /token Content-Type: application/x-www-form-urlencoded grant_type=authorization_code&code=authorization_code&redirect_uri=https://client.example.com/cb &code_verifier=codeVerifier&client_id=client_id
-
验证 code_verifier:
// 授权服务器验证 code_verifier 与 code_challenge 是否匹配 String generatedCodeChallenge = Base64.getUrlEncoder().withoutPadding().encodeToString( MessageDigest.getInstance("SHA-256").digest(codeVerifier.getBytes(StandardCharsets.UTF_8)) ); if (!generatedCodeChallenge.equals(codeChallenge)) { throw new OAuth2AuthenticationException("Invalid code verifier"); }
PKCE 提高了 OAuth 2.0 授权码流程的安全性,是保护公共客户端的一种重要机制。通过使用 code_verifier
和 code_challenge
,PKCE 有效防止了授权码拦截和滥用的风险。
源码讲解
介绍源码执行PKCE请求的执行流程,默认请求是开启了oidc的授权码模式请求
1.客户端发起
假设客户端是第一次请求,还没有经过认证,这是会被捕获未认证异常,由security的异常过滤器发起/oauth2/authorization
请求:
OAuth2AuthorizationRequestRedirectFilter过滤器
接收/oauth2/authorization
请求,并发起/oauth2/authorize
请求,在该过滤器doFilterInternal
中使用DefaultOAuth2AuthorizationRequestResolver
的resolve
方法向/oauth2/authorize
请求添加code_challenge
参数:
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)throws ServletException, IOException {
try {
//resolve中向请求添加`code_challenge`参数
//resolve实现类在过滤器构造方法默认指定为DefaultOAuth2AuthorizationRequestResolver
OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestResolver.resolve(request);
if (authorizationRequest != null) {
//发起重定向
this.sendRedirectForAuthorization(request, response, authorizationRequest);
return;
}
}
//................省略
}
DefaultOAuth2AuthorizationRequestResolver
的resolve
中,使用getBuilder
方法通过客户端注册信息判断是否开启PKCE
如果在客户端应用yaml配置了client-authentication-method: none
,getBuilder
方法就会认定为开启PKCE
private OAuth2AuthorizationRequest resolve(HttpServletRequest request, String registrationId,
String redirectUriAction) {
if (registrationId == null) {
return null;
}
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
if (clientRegistration == null) {
throw new IllegalArgumentException("Invalid Client Registration with Id: " + registrationId);
}
//通过客户端注册信息判断是否开启PKCE
OAuth2AuthorizationRequest.Builder builder = getBuilder(clientRegistration);
String redirectUriStr = expandRedirectUri(request, clientRegistration, redirectUriAction);
builder.clientId(clientRegistration.getClientId())
.authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri())
.redirectUri(redirectUriStr)
.scopes(clientRegistration.getScopes())
.state(DEFAULT_STATE_GENERATOR.generateKey());
this.authorizationRequestCustomizer.accept(builder);
return builder.build();
}
getBuilder源码
如果客户端认证方式为none
,则使用PKCE,并通过DEFAULT_PKCE_APPLIER
添加参数 code_challenge
private OAuth2AuthorizationRequest.Builder getBuilder(ClientRegistration clientRegistration) {
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(clientRegistration.getAuthorizationGrantType())) {
// @formatter:off
OAuth2AuthorizationRequest.Builder builder = OAuth2AuthorizationRequest.authorizationCode()
.attributes((attrs) ->
attrs.put(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId()));
// @formatter:on
if (!CollectionUtils.isEmpty(clientRegistration.getScopes())
&& clientRegistration.getScopes().contains(OidcScopes.OPENID)) {
applyNonce(builder);
}
//如果客户端认证方式为none,则使用PKCE
if (ClientAuthenticationMethod.NONE.equals(clientRegistration.getClientAuthenticationMethod())) {
//反向添加PKCE参数 `code_challenge`
DEFAULT_PKCE_APPLIER.accept(builder);
}
return builder;
}
if (AuthorizationGrantType.IMPLICIT.equals(clientRegistration.getAuthorizationGrantType())) {
return OAuth2AuthorizationRequest.implicit();
}
throw new IllegalArgumentException(
"Invalid Authorization Grant Type (" + clientRegistration.getAuthorizationGrantType().getValue()
+ ") for Client Registration with Id: " + clientRegistration.getRegistrationId());
}
添加PKCE参数
code_challenge
的DEFAULT_PKCE_APPLIER
:
在DefaultOAuth2AuthorizationRequestResolver
被指定为OAuth2AuthorizationRequestCustomizers
,通过withPkce
方法添加PKCE参数:
public final class DefaultOAuth2AuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver {
private static final Consumer<OAuth2AuthorizationRequest.Builder> DEFAULT_PKCE_APPLIER = OAuth2AuthorizationRequestCustomizers
.withPkce();
}
进入
OAuth2AuthorizationRequestCustomizers.withPkce()
:
使用哈希算法(通常是 SHA-256)对 code_verifier
进行哈希运算,生成 code_challenge
private static void applyPkce(OAuth2AuthorizationRequest.Builder builder) {
if (isPkceAlreadyApplied(builder)) {
return;
}
String codeVerifier = DEFAULT_SECURE_KEY_GENERATOR.generateKey();
builder.attributes((attrs) -> attrs.put(PkceParameterNames.CODE_VERIFIER, codeVerifier));
//使用哈希算法(通常是 SHA-256)对 code_verifier 进行哈希运算,生成 code_challenge
builder.additionalParameters((params) -> {
try {
String codeChallenge = createHash(codeVerifier);
params.put(PkceParameterNames.CODE_CHALLENGE, codeChallenge);
params.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256");
}
catch (NoSuchAlgorithmException ex) {
params.put(PkceParameterNames.CODE_CHALLENGE, codeVerifier);
}
});
}
这里的builder
,最终会通过accept
设置到DefaultOAuth2AuthorizationRequestResolver
中,从而向请求中添加PKCE参数 code_challenge
最后通过
resolve
方法,将请求参数添加完毕,在OAuth2AuthorizationRequestRedirectFilter
中执行sendRedirectForAuthorization
方法发起重定向,请求路径为/oauth2/authorize
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)throws ServletException, IOException {
try {
//resolve中向请求添加`code_challenge`参数
//resolve实现类在过滤器构造方法默认指定为DefaultOAuth2AuthorizationRequestResolver
OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestResolver.resolve(request);
if (authorizationRequest != null) {
//发起重定向
this.sendRedirectForAuthorization(request, response, authorizationRequest);
return;
}
}
//................省略
}
2.授权服务验证参数
上面客户端发起/oauth2/authorize
请求重定向时,OAuth2AuthorizationEndpointFilter
过滤器会过滤该请求,并进行PKCE参数提取及检查,并保存包含code_challenge
的授权记录
OAuth2AuthorizationEndpointFilter
OAuth2AuthorizationEndpointFilter
过滤器中主要是检查是否有PKCE参数及其是否合规
接收/oauth2/authorize
请求时,在过滤方法中使用OAuth2AuthorizationCodeRequestAuthenticationConverter
的convert
方法处理PKCE参数,将PKCE参数添加到认证对象中:
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)throws ServletException, IOException {
if (!this.authorizationEndpointMatcher.matches(request)) {
filterChain.doFilter(request, response);
return;
}
try {
//先调用OAuth2AuthorizationCodeRequestAuthenticationConverter,将转换请求为认证对象
Authentication authentication = this.authenticationConverter.convert(request);
if (authentication instanceof AbstractAuthenticationToken) {
((AbstractAuthenticationToken) authentication)
.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
//对认证对象进行认证
Authentication authenticationResult = this.authenticationManager.authenticate(authentication);
//.............
//认证成功处理,携带code重定向回客户端
this.authenticationSuccessHandler.onAuthenticationSuccess(
request, response, authenticationResult);
}
下面先看转换,再看认证
转换
OAuth2AuthorizationCodeRequestAuthenticationConverter
的convert
方法,将PKCE参数code_challenge
与code_challenge_method
取出并添加到创建的认证对象中,用于后续检验。
截取处理PKCE部分关键代码:
@Override
public Authentication convert(HttpServletRequest request) {
//...................省略
// code_challenge (REQUIRED for public clients) - RFC 7636 (PKCE)
String codeChallenge = parameters.getFirst(PkceParameterNames.CODE_CHALLENGE);
if (StringUtils.hasText(codeChallenge) &&
parameters.get(PkceParameterNames.CODE_CHALLENGE).size() != 1) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE, PKCE_ERROR_URI);
}
// code_challenge_method (OPTIONAL for public clients) - RFC 7636 (PKCE)
//获取加密方法,默认HS256
String codeChallengeMethod = parameters.getFirst(PkceParameterNames.CODE_CHALLENGE_METHOD);
if (StringUtils.hasText(codeChallengeMethod) &&
parameters.get(PkceParameterNames.CODE_CHALLENGE_METHOD).size() != 1) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE_METHOD, PKCE_ERROR_URI);
}
Map<String, Object> additionalParameters = new HashMap<>();
parameters.forEach((key, value) -> {
if (!key.equals(OAuth2ParameterNames.RESPONSE_TYPE) &&
!key.equals(OAuth2ParameterNames.CLIENT_ID) &&
!key.equals(OAuth2ParameterNames.REDIRECT_URI) &&
!key.equals(OAuth2ParameterNames.SCOPE) &&
!key.equals(OAuth2ParameterNames.STATE)) {
additionalParameters.put(key, value.get(0));
}
});
//additionalParameters包含PKCE参数code_challenge与code_challenge_method,并添加到新创建的认证对象中。
//然后返回此对象进行下一步认证
return new OAuth2AuthorizationCodeRequestAuthenticationToken(authorizationUri, clientId, principal,
redirectUri, state, scopes, additionalParameters);
}
认证
使用OAuth2AuthorizationCodeRequestAuthenticationProvider
的authenticate
进行认证,并检查PKCE参数code_challenge
与code_challenge_method
是否合规。
如果此处认证通过,会使用authorizationService
保存认证信息,用于后续客户端发起token请求时,通过client_id
从authorizationService
拿出code_challenge
做对比验证
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
(OAuth2AuthorizationCodeRequestAuthenticationToken) authentication;
//............
//从 authorizationCodeRequestAuthentication 的附加参数中获取 code_challenge。这个参数是 PKCE 的一部分,用于防止授权码被截获和重放
String codeChallenge = (String) authorizationCodeRequestAuthentication.getAdditionalParameters().get(PkceParameterNames.CODE_CHALLENGE);
//获取 code_challenge_method 参数并检查其值。如果 code_challenge_method 不存在或不等于 S256,则抛出 INVALID_REQUEST 错误
if (StringUtils.hasText(codeChallenge)) {
String codeChallengeMethod = (String) authorizationCodeRequestAuthentication.getAdditionalParameters().get(PkceParameterNames.CODE_CHALLENGE_METHOD);
if (!StringUtils.hasText(codeChallengeMethod) || !"S256".equals(codeChallengeMethod)) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE_METHOD, PKCE_ERROR_URI,
authorizationCodeRequestAuthentication, registeredClient, null);
}
//如果 code_challenge 不存在,并且 registeredClient 的配置要求使用 PKCE,则抛出 INVALID_REQUEST 错误
} else if (registeredClient.getClientSettings().isRequireProofKey()) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE, PKCE_ERROR_URI,
authorizationCodeRequestAuthentication, registeredClient, null);
}
//............
//封装参数到请求中,用于向下游传递
OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest.authorizationCode()
.authorizationUri(authorizationCodeRequestAuthentication.getAuthorizationUri())
.clientId(registeredClient.getClientId())
.redirectUri(authorizationCodeRequestAuthentication.getRedirectUri())
.scopes(authorizationCodeRequestAuthentication.getScopes())
.state(authorizationCodeRequestAuthentication.getState())
.additionalParameters(authorizationCodeRequestAuthentication.getAdditionalParameters())
.build();
//............
OAuth2Authorization authorization = authorizationBuilder(registeredClient, principal, authorizationRequest)
.authorizedScopes(authorizationRequest.getScopes())
.token(authorizationCode)
.build();
//如果认证成功,保存授权信息,authorization包含code_challenge参数
//这里非常关键,后续客户端发起token请求时,会通过client_id,从authorizationService拿出code_challenge做对比验证
this.authorizationService.save(authorization);
//............
}
认证成功重定向
OAuth2AuthorizationEndpointFilter
过滤器转换并认证成功后,发起/login/oauth2/code/*
请求重定向回客户端
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)throws ServletException, IOException {
if (!this.authorizationEndpointMatcher.matches(request)) {
filterChain.doFilter(request, response);
return;
}
try {
//先调用OAuth2AuthorizationCodeRequestAuthenticationConverter,将转换请求为认证对象
Authentication authentication = this.authenticationConverter.convert(request);
if (authentication instanceof AbstractAuthenticationToken) {
((AbstractAuthenticationToken) authentication)
.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
//对认证对象进行认证
Authentication authenticationResult = this.authenticationManager.authenticate(authentication);
//.............
//认证成功处理,携带code重定向回客户端
this.authenticationSuccessHandler.onAuthenticationSuccess(
request, response, authenticationResult);
}
3.重定向回客户端
此处的流程概括:
-
OAuth2LoginAuthenticationFilter
过滤器调用OidcAuthorizationCodeAuthenticationProvider
发起code授权码认证 -
OidcAuthorizationCodeAuthenticationProvider
使用DefaultAuthorizationCodeTokenResponseClient
向授权服务发起请求,用code换取token -
DefaultAuthorizationCodeTokenResponseClient
调用OAuth2AuthorizationCodeGrantRequestEntityConverter
,从重定向回来的/login/oauth2/code/*
请求中获取code_verifier
参数,并添加到新发起的token请求中 -
DefaultAuthorizationCodeTokenResponseClient
得到相应获取token
重定向code回客户端
OAuth2LoginAuthenticationFilter
过滤器接收授权服务重定向回来得/login/oauth2/code/*
请求,在它attemptAuthentication
方法内如下代码处,发起code换取token的请求:
public class OAuth2LoginAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
//............
OAuth2LoginAuthenticationToken authenticationResult = (OAuth2LoginAuthenticationToken) this
.getAuthenticationManager().authenticate(authenticationRequest);
//............
}
}
开启oidc后,上面的代码则默认使用
OidcAuthorizationCodeAuthenticationProvider
实现类进行认证处理,调用其重写的authenticate
方法
OidcAuthorizationCodeAuthenticationProvider
的authenticate
方法内,在如下getResponse
中添加PKCE参数到请求中
public class OidcAuthorizationCodeAuthenticationProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
//............
OAuth2AccessTokenResponse accessTokenResponse = getResponse(authorizationCodeAuthentication);
//............
}
}
getResponse
代码,继续调用accessTokenResponseClient
的getTokenResponse
:
public class OidcAuthorizationCodeAuthenticationProvider implements AuthenticationProvider {
private OAuth2AccessTokenResponse getResponse(OAuth2LoginAuthenticationToken authorizationCodeAuthentication) {
try {
//根据授权模式的不同,使用不同的accessTokenResponseClient
//授权码模式下为DefaultAuthorizationCodeTokenResponseClient
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
使用DefaultAuthorizationCodeTokenResponseClient
实现类
DefaultAuthorizationCodeTokenResponseClient
的getTokenResponse
源码:
public final class DefaultAuthorizationCodeTokenResponseClient
implements OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> {
@Override
public OAuth2AccessTokenResponse getTokenResponse(
OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest) {
Assert.notNull(authorizationCodeGrantRequest, "authorizationCodeGrantRequest cannot be null");
//进行请求转换,从重定向回来的请求中取出PKCE参数,添加到新的token请求中
RequestEntity<?> request = this.requestEntityConverter.convert(authorizationCodeGrantRequest);
//发起token请求
ResponseEntity<OAuth2AccessTokenResponse> response = getResponse(request);
//获取响应数据,包含token
return response.getBody();
}
}
授权码模式中
requestEntityConverter
调用OAuth2AuthorizationCodeGrantRequestEntityConverter
实现类的convert
方法(源码在其父类AbstractOAuth2AuthorizationGrantRequestEntityConverter
中)
abstract class AbstractOAuth2AuthorizationGrantRequestEntityConverter<T extends AbstractOAuth2AuthorizationGrantRequest> implements Converter<T, RequestEntity<?>> {
@Override
public RequestEntity<?> convert(T authorizationGrantRequest) {
HttpHeaders headers = getHeadersConverter().convert(authorizationGrantRequest);
//getParametersConverter中向请求添加了CODE_VERIFIER参数
MultiValueMap<String, String> parameters = getParametersConverter().convert(authorizationGrantRequest);
URI uri = UriComponentsBuilder
.fromUriString(authorizationGrantRequest.getClientRegistration().getProviderDetails().getTokenUri())
.build().toUri();
return new RequestEntity<>(parameters, headers, HttpMethod.POST, uri);
}
}
convert方法中又使用createParameters
方法,从授权服务重定向到客户端的/login/oauth2/code/*
请求中获取code_verifier
参数,并添加到新发起的token请求中:
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);
}
if (!ClientAuthenticationMethod.CLIENT_SECRET_BASIC.equals(clientRegistration.getClientAuthenticationMethod())
&& !ClientAuthenticationMethod.BASIC.equals(clientRegistration.getClientAuthenticationMethod())) {
parameters.add(OAuth2ParameterNames.CLIENT_ID, clientRegistration.getClientId());
}
if (ClientAuthenticationMethod.CLIENT_SECRET_POST.equals(clientRegistration.getClientAuthenticationMethod())
|| ClientAuthenticationMethod.POST.equals(clientRegistration.getClientAuthenticationMethod())) {
parameters.add(OAuth2ParameterNames.CLIENT_SECRET, clientRegistration.getClientSecret());
}
//添加PKCE参数
if (codeVerifier != null) {
parameters.add(PkceParameterNames.CODE_VERIFIER, codeVerifier);
}
return parameters;
}
}
然后回到上面DefaultAuthorizationCodeTokenResponseClient
的getTokenResponse
方法,发起请求获取token:
@Override
public OAuth2AccessTokenResponse getTokenResponse(
OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest) {
Assert.notNull(authorizationCodeGrantRequest, "authorizationCodeGrantRequest cannot be null");
//进行请求转换,从重定向回来的请求中取出PKCE参数,添加到新的token请求中
RequestEntity<?> request = this.requestEntityConverter.convert(authorizationCodeGrantRequest);
//发起token请求
ResponseEntity<OAuth2AccessTokenResponse> response = getResponse(request);
//获取响应数据,包含token
return response.getBody();
}
4.授权服务认证客户端
OAuth2ClientAuthenticationFilter
对客户端发起的token请求进行过滤,认证其客户端信息,步骤依然是先转换,后认证
public final class OAuth2ClientAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)throws ServletException, IOException {
if (!this.requestMatcher.matches(request)) {
filterChain.doFilter(request, response);
return;
}
try {
//请求转换
Authentication authenticationRequest = this.authenticationConverter.convert(request);
if (authenticationRequest instanceof AbstractAuthenticationToken) {
((AbstractAuthenticationToken) authenticationRequest).setDetails(
this.authenticationDetailsSource.buildDetails(request));
}
if (authenticationRequest != null) {
validateClientIdentifier(authenticationRequest);
//认证
Authentication authenticationResult = this.authenticationManager.authenticate(authenticationRequest);
this.authenticationSuccessHandler.onAuthenticationSuccess(request, response, authenticationResult);
}
filterChain.doFilter(request, response);
} catch (OAuth2AuthenticationException ex) {
if (this.logger.isTraceEnabled()) {
this.logger.trace(LogMessage.format("Client authentication failed: %s", ex.getError()), ex);
}
this.authenticationFailureHandler.onAuthenticationFailure(request, response, ex);
}
}
}
请求转换
OAuth2ClientAuthenticationFilter
过滤器doFilterInternal
方法中的:
Authentication authenticationRequest = this.authenticationConverter.convert(request);
使用委托设计模式,通过DelegatingAuthenticationConverter
来确定使用的转换实现类为PublicClientAuthenticationConverter
:
public final class PublicClientAuthenticationConverter implements AuthenticationConverter {
@Nullable
@Override
public Authentication convert(HttpServletRequest request) {
if (!OAuth2EndpointUtils.matchesPkceTokenRequest(request)) {
return null;
}
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
// 判断请求必须带有client_id
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
if (parameters.get(PkceParameterNames.CODE_VERIFIER).size() != 1) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
}
parameters.remove(OAuth2ParameterNames.CLIENT_ID);
//将code_verifier传入新创建的认证对象,做下一步认证
return new OAuth2ClientAuthenticationToken(clientId, ClientAuthenticationMethod.NONE, null,
new HashMap<>(parameters.toSingleValueMap()));
}
}
认证
OAuth2ClientAuthenticationFilter
过滤器doFilterInternal
方法中的:
Authentication authenticationResult = this.authenticationManager.authenticate(authenticationRequest);
在ProviderManager
中遍历所有的AuthenticationProvider
实现类,执行每个实现的supports
方法的到具体处理PKCE的PublicClientAuthenticationProvider
:
public final class PublicClientAuthenticationProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
OAuth2ClientAuthenticationToken clientAuthentication =
(OAuth2ClientAuthenticationToken) authentication;
//检查客户端的身份验证方法是否为 NONE。如果不是,返回 null,表示该 AuthenticationProvider 无法处理此请求。
if (!ClientAuthenticationMethod.NONE.equals(clientAuthentication.getClientAuthenticationMethod())) {
return null;
}
//获取 clientId,并从 registeredClientRepository 中查找相应的注册客户端。如果未找到,抛出 INVALID_CLIENT 错误。
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");
}
//检查注册客户端是否支持方法传入authentication中指定的身份验证方法。如果不支持,抛出 INVALID_CLIENT 错误
if (!registeredClient.getClientAuthenticationMethods().contains(
clientAuthentication.getClientAuthenticationMethod())) {
throwInvalidClient("authentication_method");
}
if (this.logger.isTraceEnabled()) {
this.logger.trace("Validated client authentication parameters");
}
//调用 codeVerifierAuthenticator 的 authenticateRequired 方法,验证公共客户端的 code_verifier 参数
this.codeVerifierAuthenticator.authenticateRequired(clientAuthentication, registeredClient);
if (this.logger.isTraceEnabled()) {
this.logger.trace("Authenticated public client");
}
//创建并返回一个新的 OAuth2ClientAuthenticationToken,其中包含已注册的客户端、客户端身份验证方法以及 null 凭据
return new OAuth2ClientAuthenticationToken(registeredClient,
clientAuthentication.getClientAuthenticationMethod(), null);
}
}
上面的关键之处:
this.codeVerifierAuthenticator.authenticateRequired(clientAuthentication, registeredClient);
会调用CodeVerifierAuthenticator
的authenticate
对CODE_CHALLENGE
及CODE_VERIFIER
进行认证:
final class CodeVerifierAuthenticator {
private boolean authenticate(OAuth2ClientAuthenticationToken clientAuthentication,
RegisteredClient registeredClient) {
//获取客户端身份验证请求中的附加参数,并检查该请求是否为授权码类型。如果不是,返回 false
Map<String, Object> parameters = clientAuthentication.getAdditionalParameters();
if (!authorizationCodeGrant(parameters)) {
return false;
}
//使用附加参数中的授权码从 authorizationService 查找相应的授权信息。如果找不到,抛出 INVALID_GRANT 错误
//这里的authorizationService对应之前在OAuth2AuthorizationCodeRequestAuthenticationProvider中保存的客户端授权记录,里面存有客户端请求授权码时传过来的code_challenge
OAuth2Authorization authorization = this.authorizationService.findByToken(
(String) parameters.get(OAuth2ParameterNames.CODE),
AUTHORIZATION_CODE_TOKEN_TYPE);
if (authorization == null) {
throwInvalidGrant(OAuth2ParameterNames.CODE);
}
//如果日志记录级别设置为 TRACE,记录一条日志,表示已检索到授权信息。
if (this.logger.isTraceEnabled()) {
this.logger.trace("Retrieved authorization with authorization code");
}
//从authorizationService中取出的授权信息中获取授权请求
OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(
OAuth2AuthorizationRequest.class.getName());
//从授权请求中提取 code_challenge。
//如果 code_challenge 为空且注册客户端要求使用 Proof Key,则抛出 INVALID_GRANT 错误。
//如果 code_challenge 为空且不要求 Proof Key,记录日志并返回 false
String codeChallenge = (String) authorizationRequest.getAdditionalParameters()
.get(PkceParameterNames.CODE_CHALLENGE);
if (!StringUtils.hasText(codeChallenge)) {
if (registeredClient.getClientSettings().isRequireProofKey()) {
throwInvalidGrant(PkceParameterNames.CODE_CHALLENGE);
} else {
if (this.logger.isTraceEnabled()) {
this.logger.trace("Did not authenticate code verifier since requireProofKey=false");
}
return false;
}
}
//如果日志记录级别设置为 TRACE,记录一条日志,表示已验证 code_verifier 参数
if (this.logger.isTraceEnabled()) {
this.logger.trace("Validated code verifier parameters");
}
//从授权请求的附加参数中获取 code_challenge_method, 即加密方法
String codeChallengeMethod = (String) authorizationRequest.getAdditionalParameters()
.get(PkceParameterNames.CODE_CHALLENGE_METHOD);
//从客户端身份验证请求的附加参数中获取 code_verifier
String codeVerifier = (String) parameters.get(PkceParameterNames.CODE_VERIFIER);
//调用 codeVerifierValid 方法验证 code_verifier 是否有效。如果无效,抛出 INVALID_GRANT 错误
//使用SHA-256的算法对code_verifier进行哈希运算,将运算结果与code_challenge对比
if (!codeVerifierValid(codeVerifier, codeChallenge, codeChallengeMethod)) {
throwInvalidGrant(PkceParameterNames.CODE_VERIFIER);
}
//如果日志记录级别设置为 TRACE,记录一条日志,表示已认证 code_verifierV
if (this.logger.isTraceEnabled()) {
this.logger.trace("Authenticated code verifier");
}
//如果所有检查都通过,返回 true,表示认证成功。
return true;
}
}
PKCE的关键验证为使用SHA-256算法加密进行对比认证,这里取出之前客户端请求授权码时保存的
code_challenge
,与此次发来的code_verifier
运算后的结果进行对比
上面代码的codeVerifierValid
对比方法源码
private static boolean codeVerifierValid(String codeVerifier, String codeChallenge, String codeChallengeMethod) {
if (!StringUtils.hasText(codeVerifier)) {
return false;
} else if ("S256".equals(codeChallengeMethod)) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] digest = md.digest(codeVerifier.getBytes(StandardCharsets.US_ASCII));
String encodedVerifier = Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
return encodedVerifier.equals(codeChallenge);
} catch (NoSuchAlgorithmException ex) {
// It is unlikely that SHA-256 is not available on the server. If it is not available,
// there will likely be bigger issues as well. We default to SERVER_ERROR.
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.SERVER_ERROR);
}
}
return false;
}
返回true则对比一致,客户端认证通过,交由OAuth2TokenEndpointFilter
做后续处理,PKCE认证结束。