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

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

版本信息

Spring Boot 2.7.10
spring-security-oauth2-client 5.7.7


认证方式

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

jwt认证方式

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

请求格式

请求方法:POST

请求路径:/oauth2/token

请求头:

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

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

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

请求参数解释:

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

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

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

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

示例 JWT 断言(简化版):

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

client_secret_basic方式

对应配置为ClientAuthenticationMethod.CLIENT_SECRET_BASIC

请求方法:POST

请求路径:/oauth2/token

请求头:

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

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

grant_type=client_credentials

请求头解释:

  1. Authorization

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

client_secret_post方式

对应配置ClientAuthenticationMethod.CLIENT_SECRET_POST

请求方法:POST

请求路径:/oauth2/token

请求格式如下:

POST /oauth2/token HTTP/1.1
Host: authorization-server.com
Content-Type: application/x-www-form-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:客户端 ID
    • client_secret:客户端密钥
  • 其他参数:可能包括 grant_typecoderedirect_uri 等,如果有的话。

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


PKCE方式

转换的请求

请求方法:POST

请求路径:/oauth2/token

请求格式:

POST /oauth2/token HTTP/1.1
Host: authorization-server.com
Content-Type: application/x-www-form-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:客户端 ID
    • code_verifier:PKCE 流程中的 code_verifier
  • 其他参数:可能包括 grant_typecoderedirect_uri 等。

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



配置客户端认证方式

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

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

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

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

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

public final class ClientAuthenticationMethod implements Serializable {

	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

	/**
	 * @deprecated Use {@link #CLIENT_SECRET_BASIC} 弃用
	 */
	@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(),调用的实现是ProviderManagerauthenticate方法,如下:

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)会继续调用OidcAuthorizationCodeAuthenticationProviderauthenticate方法,在这个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信息就是在OAuth2AuthorizationGrantRequestEntityUtilsgetTokenRequestHeaders方法内生成的。


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_jwtprivate_key_jwt

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

JwtClientAssertionAuthenticationConverter

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

请求的转换

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

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

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

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

示例 HTTP 请求

请求方法:POST

请求路径:/oauth2/token

请求头:

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

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

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

请求参数解释:

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

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

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

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

示例 JWT 断言(简化版):

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

源码解析

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

public final class JwtClientAssertionAuthenticationConverter implements AuthenticationConverter {
    
	private static final ClientAuthenticationMethod JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD =
			new ClientAuthenticationMethod("urn:ietf:params:oauth:client-assertion-type:jwt-bearer");

	@Nullable
	@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_typeclient_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);
}

代码功能概述

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

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

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

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

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

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

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

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

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


解码器

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

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

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

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

private static NimbusJwtDecoder buildDecoder(RegisteredClient registeredClient) {
    // 从注册客户端的设置中获取JWS算法
    JwsAlgorithm jwsAlgorithm = registeredClient.getClientSettings().getTokenEndpointAuthenticationSigningAlgorithm();
    
    // 如果JWS算法是签名算法(非对称加密)
    if (jwsAlgorithm instanceof SignatureAlgorithm) {
        // 获取JWK Set 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);
}

关键点解释

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

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

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

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

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

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

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

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

然后在:

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

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


委托模式验证

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

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

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

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

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

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

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


实际流程

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

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

  3. 验证参数

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

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




授权服务client_secret_basic认证

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

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

ClientSecretBasicAuthenticationConverter

示例请求

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

请求方法:POST

请求路径:/oauth2/token

请求头:

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

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

grant_type=client_credentials

请求头解释:

  1. Authorization

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

请求处理流程

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

源码解析

public final class ClientSecretBasicAuthenticationConverter implements AuthenticationConverter {

	@Nullable
	@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_basicclient_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());
}

源码流程概括

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

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

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

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

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

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

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

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

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

密钥匹配

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

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

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

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

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

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


授权服务client_secret_post认证

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

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

ClientSecretPostAuthenticationConverter

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

转换的请求

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

请求格式如下:

POST /oauth2/token HTTP/1.1
Host: authorization-server.com
Content-Type: application/x-www-form-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-Typeapplication/x-www-form-urlencoded
  • 必要参数
    • client_id:客户端 ID
    • client_secret:客户端密钥
  • 其他参数:可能包括 grant_typecoderedirect_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_postclient_secret_basic均使用ClientSecretAuthenticationProvider进行验证:

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

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



授权服务PKCE认证

PublicClientAuthenticationConverter

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

转换的请求

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

请求格式如下:

POST /oauth2/token HTTP/1.1
Host: authorization-server.com
Content-Type: application/x-www-form-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-Typeapplication/x-www-form-urlencoded
  • 必要参数
    • client_id:客户端 ID
    • code_verifier:PKCE 流程中的 code_verifier
  • 其他参数:可能包括 grant_typecoderedirect_uri 等。

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

源码分析

public final class PublicClientAuthenticationConverter implements AuthenticationConverter {

	@Nullable
	@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()));
	}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/871776.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Java Web —— 第七天(Mybatis案例 员工管理2)

新增员工 EmpController类 PostMappingpublic Result save(RequestBody Emp emp){log.info("新增员工操作,emp:{}",emp);empService.save(emp);return Result.success();} EmpServiceImpl实现类 //新增员工Overridepublic void save(Emp emp) {//补充基础属性 创…

台球助教在线预约小程序源码开发:打造便捷高效的台球学习新体验

在当今快节奏的生活中&#xff0c;台球作为一项集休闲、竞技与社交于一体的运动&#xff0c;受到了越来越多人的喜爱。然而&#xff0c;对于初学者而言&#xff0c;想要快速提升技能&#xff0c;往往需要专业的指导和陪练。传统的台球教练预约方式往往存在信息不对称、预约流程…

【Qt笔记】Qt界面显示时间

目录 一、前言 二、基本概念 三、代码实现 1. 获取本地时间&#xff0c;并将其转换成自己想要的格式 2.创建一个QLabel控件用于显示时间字符串 3.创建QTimer定时器更新时间 四、优化 1. 格式优化 1.1 初始化 1.2 获取星期 1.3 更改格式 1.4 定时器超时函数 1.5 …

多商户多套部署需修改注意事项

同一台服务器上部署多个多商户项目&#xff0c;需要修改和调整的地方等。 一、修改代码中的端口号&#xff0c;需要两个项目不能使用同一个端口号&#xff0c;例如&#xff1a;A项目用&#xff1a;8324&#xff0c;B项目用&#xff1a;8325&#xff1b; 二、修改反向代理&…

LCP 633 平方数之和 [leetcode - 8]

最近是在研究双指针啊&#xff0c;leetcode刷的题都是这方面的。都记录在最近的文章里&#xff0c;大家有兴趣可以去我主页看看 LCP633 平方数之和 给定一个非负整数 c &#xff0c;你要判断是否存在两个整数 a 和 b&#xff0c;使得 a2 b2 c 。 示例 1&#xff1a; 输入&…

Modbus 数据帧解析

Modbus 是一种通信协议&#xff0c;广泛应用于工业自动化系统中&#xff0c;用于连接电子设备。它是一种基于主从结构的通信协议&#xff0c;其中主设备可以与多个从设备通信。数据通过一系列的帧进行传输&#xff0c;每一帧包含多个字段&#xff0c;每个字段都有特定的功能。 …

HTML 列表和容器元素——WEB开发系列10

HTML 提供了多种方式来组织和展示内容&#xff0c;其中包括无序列表、有序列表、分区元素 ​​<div>​​ 和内联元素 ​​<span>​​、以及如何使用 ​​<div>​​​ 进行布局和表格布局。 一、HTML 列表 1. 无序列表 (​​<ul>​​) 无序列表用于展…

【Win开发环境搭建】Redis与可视化工具详细安装与配置过程

&#x1f3af;导读&#xff1a;本文档提供了Redis的简介、安装指南、配置教程及常见操作方法。包括了安装包的选择与配置环境变量的过程&#xff0c;详细说明了如何通过修改配置文件来设置密码和端口等内容。同时&#xff0c;文档还介绍了如何使用命令行工具连接Redis&#xff…

科研软件 | Diamond 4.6 安装教程

软件介绍 Diamond一个化学专业软件。它是一款在原子水平实现晶体结构可视化的软件&#xff0c;包括分子和聚合物扩展、多面体、搜索结构数据、自动和批量创建结构图片等功能&#xff0c;支持晶体结构着色和渲染以及批注。 软件下载 https://pan.quark.cn/s/37214b5bec7c 软…

05、Redis实战:优惠券秒杀、优化异步下单

6、秒杀优化 6.1 秒杀优化-异步秒杀思路 我们先来回顾一下下单流程当用户发起请求&#xff0c;此时会先请求Nginx&#xff0c;Nginx反向代理到Tomcat&#xff0c;而Tomcat中的程序&#xff0c;会进行串行操作&#xff0c;分为如下几个步骤 查询优惠券判断秒杀库存是否足够查询…

音视频相关知识

H.264编码格式 音频 PCM就是要把声音从模拟信号转换成数字信号的一种技术&#xff0c;他的原理简单地说就是利用一个固定的频率对模拟信号进行采样。 pcm是无损音频音频文件格式 每秒15帧 一秒钟300kb 单位&#xff1a;像素

故障频发,给我一个完美的解释...

1.盘点事故 8月19日&#xff0c;网易云音乐「崩了」&#xff0c;网页端报错&#xff0c;App 无法使用&#xff0c;什么原因&#xff1f;你那受影响了吗&#xff1f; 一次更新&#xff0c;一串代码&#xff0c;全球宕机。7月19日下午发生了全球范围内的Windows大面积蓝屏事件&a…

Django | 从中间件的角度来认识Django发送邮件功能

文章目录 概要中间件中间件 ---> 钩子实现中间件demo 邮件发送过程Django如何做邮件服务配置流程 中间件结合邮件服务实现告警 概要 摘要 业务告警 邮件验证 密码找回 邮件告警 中间件 中间件 —> ‘钩子’ 例如 访问路由 的次数【请求】 中间件类须实现下列五个方法…

C++,std::queue 详解

文章目录 1. 概述2. 包含头文件3. 基本操作3.1 构造函数3.2 赋值操作3.3 成员函数 4. 迭代器5. 示例6. 注意事项参考 1. 概述 std::queue 是 C 标准模板库&#xff08;STL&#xff09;中的一个容器适配器&#xff0c;它提供了一种先进先出&#xff08;FIFO&#xff09;的数据结…

[数据集][目标检测]木材缺陷检测数据集VOC+YOLO格式2383张10类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;2383 标注数量(xml文件个数)&#xff1a;2383 标注数量(txt文件个数)&#xff1a;2383 标注…

网络安全入门教程(非常详细)从零基础入门到精通_网路安全 教程

前言 1.入行网络安全这是一条坚持的道路&#xff0c;三分钟的热情可以放弃往下看了。2.多练多想&#xff0c;不要离开了教程什么都不会了&#xff0c;最好看完教程自己独立完成技术方面的开发。3.有时多百度&#xff0c;我们往往都遇不到好心的大神&#xff0c;谁会无聊天天给…

线性数据结构的基本概念(数组,链表,栈,队列)

数组 数组由相同类型的元素组成&#xff0c;使用一块连续的内存来存储。 数组的特点是&#xff1a; 1.利用索引进行访问 2.容量固定 3.使用一块连续的内存来存储 各种操作的时间复杂度&#xff1a; 查找/修改&#xff1a;O&#xff08;1&#xff09;//访问特定位置的元素 插入…

[鹏城杯 2022]简单的php

题目源代码 <?phpshow_source(__FILE__); $code $_GET[code]; if(strlen($code) > 80 or preg_match(/[A-Za-z0-9]|\|"||\ |,|\.|-|\||\/|\\|<|>|\$|\?|\^|&|\|/is,$code)){die( Hello); }else if(; preg_replace(/[^\s\(\)]?\((?R)?\)/, , $code…

Qt实现tcp协议

void Widget::readyRead_slot() {//读取服务器发来的数据QByteArray msg socket->readAll();QString str QString::fromLocal8Bit(msg);QStringList list str.split(:);if(list.at(0) userName){QString str2;for (int i 1; i < list.count(); i) {str2 list.at(i);…

使用DOM破坏启动xss

目录 实验环境&#xff1a; 分析&#xff1a; 找破坏点&#xff1a; 查看源码找函数&#xff1a; 找到了三个方法&#xff0c;loadComments、escapeHTM 、displayComments loadComments escapeHTM displayComments&#xff1a; GOGOGO 实验环境&#xff1a; Lab: Exp…