Spring OAuth2 PKCE介绍

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_challengecode_verifier


PKCE 授权码流程

PKCE 流程与标准的授权码流程类似,但在几个关键步骤上进行了增强:

  1. 客户端创建 code_verifier

    • 客户端生成一个高熵的随机字符串,称为 code_verifier
    • 使用哈希算法(通常是 SHA-256)对 code_verifier 进行哈希运算,生成 code_challenge
    • 如果没有使用哈希算法,也可以直接将 code_verifier 作为 code_challenge

  2. 客户端发起授权请求

    • 客户端将 code_challenge 以及其他必要的授权参数(如 client_idredirect_uri 等)一起发送到授权服务器。

  3. 用户认证和授权

    • 用户在授权服务器上进行认证并授权客户端访问其资源,授权服务器保存认证数据,包括客户端发来的 code_challenge
    • 授权服务器生成授权码,并将其重定向回客户端的 redirect_uri

  4. 客户端交换授权码

    • 客户端接收到授权码后,将授权码和 code_verifier 发送到授权服务器,以交换访问令牌。

  5. 服务器验证 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 流程的参数

  1. code_verifier
    • 随机生成的高熵字符串,长度在 43 到 128 个字符之间。
    • 可以包含字符范围为 A-Z、a-z、0-9、-._~(Base64 URL 安全字符)。

  2. code_challenge
    • code_verifier 进行哈希运算后的结果(如果使用了 S256 算法),或者直接使用 code_verifier
    • 两种生成方式:
      • plain:code_challenge = code_verifier
      • S256:code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))

  3. code_challenge_method
    • 用于指定 code_challenge 的生成方式。
    • 可以是 plainS256,默认为 plain



PKCE 的优点

  • 增强安全性:PKCE 防止授权码被拦截和滥用,因为授权服务器需要验证 code_verifiercode_challenge 的匹配关系。
  • 适用于公共客户端:尤其是单页应用(SPA)和移动应用,PKCE 能够提高其安全性,避免泄露客户端凭证。

实际应用中的 PKCE

以下是一个使用 PKCE 的 OAuth 2.0 授权码流程的具体示例:

  1. 生成 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))
    );
    
  2. 发起授权请求

    GET /authorize?response_type=code&client_id=client_id&redirect_uri=https://client.example.com/cb
    &code_challenge=codeChallenge&code_challenge_method=S256
    
  3. 交换授权码

    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
    
  4. 验证 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_verifiercode_challenge,PKCE 有效防止了授权码拦截和滥用的风险。




源码讲解

介绍源码执行PKCE请求的执行流程,默认请求是开启了oidc的授权码模式请求

1.客户端发起

假设客户端是第一次请求,还没有经过认证,这是会被捕获未认证异常,由security的异常过滤器发起/oauth2/authorization请求:

OAuth2AuthorizationRequestRedirectFilter过滤器

接收/oauth2/authorization请求,并发起/oauth2/authorize请求,在该过滤器doFilterInternal中使用DefaultOAuth2AuthorizationRequestResolverresolve方法向/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;
			}
		}
    
      //................省略

}

DefaultOAuth2AuthorizationRequestResolverresolve中,使用getBuilder方法通过客户端注册信息判断是否开启PKCE

如果在客户端应用yaml配置了client-authentication-method: nonegetBuilder方法就会认定为开启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_challengeDEFAULT_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请求时,在过滤方法中使用OAuth2AuthorizationCodeRequestAuthenticationConverterconvert方法处理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);     
}

下面先看转换,再看认证

转换

OAuth2AuthorizationCodeRequestAuthenticationConverterconvert方法,将PKCE参数code_challengecode_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);

}

认证

使用OAuth2AuthorizationCodeRequestAuthenticationProviderauthenticate进行认证,并检查PKCE参数code_challengecode_challenge_method是否合规。

如果此处认证通过,会使用authorizationService保存认证信息,用于后续客户端发起token请求时,通过client_idauthorizationService拿出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.重定向回客户端

此处的流程概括:

  1. OAuth2LoginAuthenticationFilter过滤器调用OidcAuthorizationCodeAuthenticationProvider发起code授权码认证

  2. OidcAuthorizationCodeAuthenticationProvider使用DefaultAuthorizationCodeTokenResponseClient向授权服务发起请求,用code换取token

  3. DefaultAuthorizationCodeTokenResponseClient调用OAuth2AuthorizationCodeGrantRequestEntityConverter,从重定向回来的/login/oauth2/code/*请求中获取code_verifier参数,并添加到新发起的token请求中

  4. 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方法

OidcAuthorizationCodeAuthenticationProviderauthenticate方法内,在如下getResponse中添加PKCE参数到请求中

public class OidcAuthorizationCodeAuthenticationProvider implements AuthenticationProvider {

	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
	
		//............
		
		OAuth2AccessTokenResponse accessTokenResponse = getResponse(authorizationCodeAuthentication);
	
		//............
	}

}

getResponse代码,继续调用accessTokenResponseClientgetTokenResponse

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实现类

DefaultAuthorizationCodeTokenResponseClientgetTokenResponse源码:

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;
	}

}

然后回到上面DefaultAuthorizationCodeTokenResponseClientgetTokenResponse方法,发起请求获取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);

会调用CodeVerifierAuthenticatorauthenticateCODE_CHALLENGECODE_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认证结束。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值