spring-security-oauth2授权服务原理

embedded/2024/12/22 2:07:16/

本文为Spring Security OAuth2 授权服务的源码解析,代码版本信息:

<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.7.10</version>
</parent><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-oauth2-authorization-server</artifactId><version>0.4.0</version></dependency></dependencies>


OAuth2.0授权码流程示意

请求流程:

  1. 客户端发起授权码请求:用户通过客户端的前端(例如网页)触发请求(点击按钮或扫码登录等方式)。

    GET /oauth2/authorize?response_type=code&client_id=client_id_example&redirect_uri=https://client.example.com/callback&scope=read&state=xyz
    
  2. 客户端将用户重定向到授权服务器:授权服务器向用户展示登录页面和授权同意页面。

  3. 用户同意授权并提交同意请求:用户同意后提交表单。

    POST /oauth2/authorize 
    Content-Type: application/x-www-form-urlencodedclient_id=client_id_example&state=xyz&scope=read
    
  4. 授权服务器重定向回客户端:如果用户同意授权,授权服务器会生成授权码,并携带授权码重定向用户浏览器到客户端指定的 地址(客户端请求中的redirect_uri参数)。

    HTTP/1.1 302 Found
    Location: https://client.example.com/callback?code=authorization_code_example&state=xyz
    
  5. 客户端携带授权码请求令牌:客户端得到授权服务返回的code后,携带code去换取token,但在获取token令牌之前,会由授权服务先进行客户端认证,认证通过后在生成token返回:

    请求头除了Host与Content-Type,多了一个Authorization

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

    POST /oauth2/token HTTP/1.1
    Host: authorization-server.com
    Content-Type: application/x-www-form-urlencoded
    Authorization: Basic Y2xpZW50SWQ6Y2xpZW50U2VjcmV0grant_type=authorization_code
    code=授权码实际值
    redirect_uri=重定向地址
    client_id=客户端id
    client_secret=客户端id
    
  • 授权码请求 GET /oauth2/authorize 是客户端向授权服务器请求授权码的 HTTP GET 请求。
  • 授权同意请求 POST /oauth2/authorize 是用户通过浏览器在授权同意页面上提交的 HTTP POST 请求。
  • 令牌请求 POST /oauth2/token 是在经过用户授权后,由客户端用授权码获取token的 HTTP POST 请求。

这些请求共同构成了 OAuth 2.0 授权码授权流程的一部分,确保客户端在访问受保护资源之前获得用户的明确授权。


/oauth2/authorize请求源码处理流程

  1. 客户端先获取授权码,发起/oauth2/authorize请求(GET),进入授权服务OAuth2AuthorizationEndpointFilter过滤器的doFilterInternal方法进行处理。
  2. OAuth2AuthorizationEndpointFilter过滤器使用转换器OAuth2AuthorizationCodeRequestAuthenticationConverter将请求转换为权限对象OAuth2AuthorizationCodeRequestAuthenticationToken
  3. 转换后进行验证,过滤器调用OAuth2AuthorizationCodeRequestAuthenticationProviderauthenticate方法对上一步得到的权限对象OAuth2AuthorizationCodeRequestAuthenticationToken进行验证,如果用户未登录,则重定向到登录页面进行登录,登录后重新发起/oauth2/authorize(使用过滤器缓存实现的)。
  4. 重新进行1、2步的过程。
  5. 请求回到权限验证器OAuth2AuthorizationCodeRequestAuthenticationProviderauthenticate方法中,验证已登录后,检查权限对象是否已授权(数据库实现使用oauth2_authorization_consent表),如果未授权,重定向到授权页面进行授权
  6. 前台点击授权按钮,重新发起/oauth2/authorize(这次是POST请求)。这次会将请求转换为OAuth2AuthorizationConsentAuthenticationToken权限对象,因为转换条件是请求为POST、且请求路径为/oauth2/authorize(源码的判断条件)
  7. 进入权限提供者OAuth2AuthorizationConsentAuthenticationProviderauthenticate方法进行授权验证,检查oauth2_authorization表存在授权请求记录,且oauth2_registered_client表中查clientid信息无误,则进行授权,向oauth2_authorization_consent表插入授权记录,并生成code授权码,调用/oauth2/authorize请求携带的重定向地址返回授权码。

下面结合此流程进行源码分析



授权码请求过滤器

简介

OAuth2AuthorizationEndpointFilter

OAuth2AuthorizationEndpointFilter 过滤器在 Spring Security OAuth2 中扮演了重要的角色,用于处理 OAuth 2.0 授权码请求(/oauth2/authorize路径),并启动授权流程。大致处理:

  1. 处理授权请求:拦截到达授权端点的客户端请求,验证请求的有效性,并根据请求的参数和状态决定后续操作。
  2. 启动授权流程:引导用户进行认证和授权,如果用户已登录且授予了相关权限,则生成授权码,并返回给客户端

处理的两种请求

1. OAuth 2.0 授权码请求(Authorization Code Request)

OAuth 2.0 授权码请求是由客户端发起的,用于请求授权服务器,进而向资源所有者(通常是用户)请求授权。请求通常包含以下内容:

  • HTTP 方法:GET

  • 路径:授权服务器的授权端点(默认 /oauth2/authorize

  • 请求参数:

    • response_type: 固定为 code,表示请求的是授权码。
    • client_id: 客户端的唯一标识符。
    • redirect_uri: 用户授权后重定向的 URI。
    • scope: 请求的权限范围。
    • state: 推荐使用的参数,用于防止跨站请求伪造(CSRF)攻击。
    • code_challengecode_challenge_method:可选参数,用于支持 PKCE(Proof Key for Code Exchange)流程。

示例 HTTP 请求

GET /oauth2/authorize?response_type=code&client_id=client_id_example&redirect_uri=https%3A%2F%2Fclient.example.com%2Fcallback&scope=read&state=xyz&code_challenge=abcdef&code_challenge_method=S256 HTTP/1.1
Host: authorization-server.example.com

2. OAuth 2.0 授权同意请求(Consent Request)

授权同意请求通常是由用户在授权码流程中,客户端重定向到授权服务器之后,授权服务器向用户展示授权同意页面的请求。授权服务器会显示一个页面,让用户选择是否同意客户端请求的权限。

用户通过浏览器与授权服务器交互,最终授权同意请求由用户在同意页面上提交。该请求通常包含以下内容:

  • HTTP 方法:POST

  • 路径:授权服务器的授权同意端点(默认 /oauth2/authorize

  • 请求参数:

    • client_id: 客户端的唯一标识符。
    • state: 同授权码请求中的 state 参数,保持请求和响应的状态一致。
    • scope: 用户同意的权限范围。

示例 HTTP 请求

POST /oauth2/authorize HTTP/1.1
Host: authorization-server.example.com
Content-Type: application/x-www-form-urlencodedclient_id=client_id_example&state=xyz&scope=read


过滤器源码解析

概括

OAuth2AuthorizationEndpointFilter过滤器源码概括:

一句话概括过滤器的核心逻辑为:接收请求,将请求转为权限对象,然后验证权限对象,认证通过后携带授权码重定向回客户端:

  • 针对授权码请求 /oauth2/authorize GET: 使用OAuth2AuthorizationCodeRequestAuthenticationConverter将请求转为权限对象,用OAuth2AuthorizationCodeRequestAuthenticationProvider来验证权限对象。如果验证用户未登录则向登录页重定向,验证未授权则向授权页面重定向。
  • 针对授权同意请求 /oauth2/authorize POST: 使用OAuth2AuthorizationConsentAuthenticationConverter 将请求转为权限对象,会用OAuth2AuthorizationConsentAuthenticationProvider来验证权限对象。
  • 用户已登录,且已授权同意后,使用DefaultRedirectStrategy向客户端/oauth2/authorize请求参数redirect_uri中的地址进行重定向

为何只处理授权码与授权同意两种请求,由过滤器构造方法可知:

public OAuth2AuthorizationEndpointFilter(AuthenticationManager authenticationManager, String authorizationEndpointUri) {Assert.notNull(authenticationManager, "authenticationManager cannot be null");Assert.hasText(authorizationEndpointUri, "authorizationEndpointUri cannot be empty");this.authenticationManager = authenticationManager;//创建请求匹配器,只匹配`/oauth2/authorize`的GET与POST,即授权码与授权同意两种请求 this.authorizationEndpointMatcher = createDefaultRequestMatcher(authorizationEndpointUri);//为DelegatingAuthenticationConverter添加转换器实现(委托模式实现转换)this.authenticationConverter = new DelegatingAuthenticationConverter(//只针对授权码与授权同意请求的两个转换器Arrays.asList(new OAuth2AuthorizationCodeRequestAuthenticationConverter(),new OAuth2AuthorizationConsentAuthenticationConverter()));
}

关于请求路径为何是/oauth2/authorize,同样由构造方法决定(可通过配置修改):

public final class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilter {//默认路径private static final String DEFAULT_AUTHORIZATION_ENDPOINT_URI = "/oauth2/authorize";//将默认路径传入,这个构造方法会继续调用上面那个,进而传入authorizationEndpointMatcher请求匹配器中,限定处理的请求public OAuth2AuthorizationEndpointFilter(AuthenticationManager authenticationManager) {this(authenticationManager, DEFAULT_AUTHORIZATION_ENDPOINT_URI);}}	

总体处理

核心处理方法doFilterInternal/oauth2/authorize请求到来时,会触发此方法进行转换与验证

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)throws ServletException, IOException {//这里的authorizationEndpointMatcher就是构造方法中创建的请求匹配器//对请求进行匹配,不符合要求的不处理,即请求必须匹配下面两种://	1.包含/oauth2/authorize路径的get请求,用于请求授权码code//	2.包含/oauth2/authorize路径的post请求,用于进行授权同意if (!this.authorizationEndpointMatcher.matches(request)) {filterChain.doFilter(request, response);return;}try {//将请求转为权限对象,主要检查请求中是否包含response_type、client_id等参数且是否符合规范//这里的'authenticationConverter'使用'DelegatingAuthenticationConverter',对应此过滤器的构造方法,不同请求使用不同转换Authentication authentication = this.authenticationConverter.convert(request);//添加一些附加的信息,包括客户端 IP 地址、会话 ID 等if (authentication instanceof AbstractAuthenticationToken) {((AbstractAuthenticationToken) authentication).setDetails(this.authenticationDetailsSource.buildDetails(request));}//对上面转换的请求对象进行验证,大致验证项:// 1.检查客户端信息是否有效(oauth2_registered_client表或内存中是否有客户信息),// 2.检查客户端授权模式是否为authorization_code授权码模式,// 3.验证PKCE参数正确性,// 4.检查资源所有者身份有效性,无效需要登陆// 5.检查请求客户端是否已被授权(oauth2_authorization_consent表或内存中是否有授权记录)Authentication authenticationResult = this.authenticationManager.authenticate(authentication);if (!authenticationResult.isAuthenticated()) {// 如果请求未通过身份验证,则向下执行过滤器链,// 并预期由后面过滤器中的AuthenticationEntryPoint来执行身份验证过程,进行用户登录(登录请求过滤器UsernamePasswordAuthenticationFilter)filterChain.doFilter(request, response);return;}//如果请求是授权同意请求,进入if中向用户(资源所有者)同意授权页面进行重定向,也就是验证未进行授权时,//在上一步authenticate认证时,如果用户已登录,但客户端没有被授权,返回验证结果会是OAuth2AuthorizationConsentAuthenticationToken类型,才会进入此处if代码内处理if (authenticationResult instanceof OAuth2AuthorizationConsentAuthenticationToken) {if (this.logger.isTraceEnabled()) {this.logger.trace("Authorization consent is required");}//发起授权页面重定向sendAuthorizationConsent(request, response,(OAuth2AuthorizationCodeRequestAuthenticationToken) authentication,(OAuth2AuthorizationConsentAuthenticationToken) authenticationResult);return;}//如果前面的认证都通过(用户已登录,且对请求完成授权),这里调用sendAuthorizationResponse方法进行带code的重定向,//重定向地址在客户端请求/oauth2/authorize的redirect_url参数中this.authenticationSuccessHandler.onAuthenticationSuccess(request, response, authenticationResult);} catch (OAuth2AuthenticationException ex) {if (this.logger.isTraceEnabled()) {this.logger.trace(LogMessage.format("Authorization request failed: %s", ex.getError()), ex);}this.authenticationFailureHandler.onAuthenticationFailure(request, response, ex);}
}

1.转换

Authentication authentication = this.authenticationConverter.convert(request);

这里的authenticationConverter使用DelegatingAuthenticationConverter,使用委托模式实现转换。在过滤器构造方法中,DelegatingAuthenticationConverter内部被存入了两个转换器:

  • OAuth2AuthorizationCodeRequestAuthenticationConverter

  • OAuth2AuthorizationConsentAuthenticationConverter

当请求到来时,DelegatingAuthenticationConverter遍历其内部所有转换器,挨个调用其转换方法,哪个成功转换就用哪个:

  • 请求是/oauth2/authorize GET时,会被OAuth2AuthorizationCodeRequestAuthenticationConverter成功转换,所以授权码请求转换器用的是OAuth2AuthorizationCodeRequestAuthenticationConverter
  • 请求是/oauth2/authorize POST会被OAuth2AuthorizationConsentAuthenticationConverter成功转换,所以授权同意请求转换器用的是OAuth2AuthorizationConsentAuthenticationConverter

两个转换器的convert方法中直接判断了请求能否被处理

  • OAuth2AuthorizationCodeRequestAuthenticationConverter

    @Override
    public Authentication convert(HttpServletRequest request) {if (!"GET".equals(request.getMethod()) && !OIDC_REQUEST_MATCHER.matches(request)) {return null;}//........
    }
    
  • OAuth2AuthorizationConsentAuthenticationConverter

    @Override
    public Authentication convert(HttpServletRequest request) {if (!"POST".equals(request.getMethod()) ||request.getParameter(OAuth2ParameterNames.RESPONSE_TYPE) != null) {return null;}/........
    }
    

2.验证

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

此处authenticationManager会调用ProviderManagerauthenticate方法,遍历AuthenticationProvider的所有的实现类,看哪个实现类支持验证当前的权限认证对象(通过supports方法判断),就调用哪个实现类进行认证。

ProviderManagerauthenticate方法:

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {Class<? extends Authentication> toTest = authentication.getClass();AuthenticationException lastException = null;AuthenticationException parentException = null;Authentication result = null;Authentication parentResult = null;int currentPosition = 0;int size = this.providers.size();//遍历所有权限认证实现for (AuthenticationProvider provider : getProviders()) {//判断当前循环的provider是否支持处理传入权限对象if (!provider.supports(toTest)) {//不支持继续循环continue;}if (logger.isTraceEnabled()) {logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",provider.getClass().getSimpleName(), ++currentPosition, size));}try {//如果支持,进行认证result = provider.authenticate(authentication);if (result != null) {copyDetails(authentication, result);break;}}//.................
}		

上面的源码概括中说到:

授权码请求 /oauth2/authorize GET使用OAuth2AuthorizationCodeRequestAuthenticationProvider来验证权限对象。

授权同意请求 /oauth2/authorize POST会用OAuth2AuthorizationConsentAuthenticationProvider来验证权限对象。

原因是因为:

  • 转换授权码请求 /oauth2/authorize GETOAuth2AuthorizationCodeRequestAuthenticationConverter转换器会将请求转为权限对象OAuth2AuthorizationCodeRequestAuthenticationToken

    public final class OAuth2AuthorizationCodeRequestAuthenticationConverter implements AuthenticationConverter {@Overridepublic Authentication convert(HttpServletRequest request) {//.................return new OAuth2AuthorizationCodeRequestAuthenticationToken(authorizationUri, clientId, principal,redirectUri, state, scopes, additionalParameters);}}
    
  • 转换授权同意请求 /oauth2/authorize POSTOAuth2AuthorizationConsentAuthenticationConverter 转换器会将请求转为权限对象OAuth2AuthorizationConsentAuthenticationToken

    public final class OAuth2AuthorizationConsentAuthenticationConverter implements AuthenticationConverter {@Overridepublic Authentication convert(HttpServletRequest request) {//.................return new OAuth2AuthorizationConsentAuthenticationToken(authorizationUri, clientId, principal,state, scopes, additionalParameters);}}
    

再看两个Providersupports方法:

  • OAuth2AuthorizationCodeRequestAuthenticationProvidersupports方法:

    @Override
    public boolean supports(Class<?> authentication) {//判断权限对象类型是否为OAuth2AuthorizationCodeRequestAuthenticationToken,是则支持处理//对应转换器OAuth2AuthorizationCodeRequestAuthenticationConverter返回的权限对象return OAuth2AuthorizationCodeRequestAuthenticationToken.class.isAssignableFrom(authentication);
    }
    
  • OAuth2AuthorizationConsentAuthenticationProvidersupports方法:

    @Override
    public boolean supports(Class<?> authentication) {//判断权限对象类型是否为OAuth2AuthorizationConsentAuthenticationToken,是则支持处理//对应转换器OAuth2AuthorizationConsentAuthenticationConverter返回的权限对象return OAuth2AuthorizationConsentAuthenticationToken.class.isAssignableFrom(authentication);
    }
    

结论

  • 针对授权码请求 /oauth2/authorize GET: 使用OAuth2AuthorizationCodeRequestAuthenticationConverter将请求转为权限对象OAuth2AuthorizationCodeRequestAuthenticationToken,用OAuth2AuthorizationCodeRequestAuthenticationProvider来验证此权限对象。
  • 针对授权同意请求 /oauth2/authorize POST: 使用OAuth2AuthorizationConsentAuthenticationConverter 将请求转为权限对象OAuth2AuthorizationConsentAuthenticationToken,用OAuth2AuthorizationConsentAuthenticationProvider来验证此权限对象。

3.用户登录

请求授权码时,授权服务需要用户登录的原因:

授权码请求 /oauth2/authorize GET到来时,会进入OAuth2AuthorizationCodeRequestAuthenticationConverter转换器的convert方法内如下代码处,去获取上下文:

Authentication principal = SecurityContextHolder.getContext().getAuthentication();
if (principal == null) {principal = ANONYMOUS_AUTHENTICATION;
}

假设用户未登录,此时获取的上下文为空,赋予principal的就是匿名权限ANONYMOUS_AUTHENTICATION(如果是登陆后,这里的principal是UsernamePasswordAuthenticationToken)。

然后在OAuth2AuthorizationCodeRequestAuthenticationConverterconvert方法最后返回转换完成的权限对象时,返回的权限对象OAuth2AuthorizationCodeRequestAuthenticationToken默认继承父类AbstractAuthenticationToken,并继承其authenticated 属性,值为false

public abstract class AbstractAuthenticationToken implements Authentication, CredentialsContainer {private final Collection<GrantedAuthority> authorities;private Object details;//默认falseprivate boolean authenticated = false;//.................
}    

然后对授权码请求转换过来的权限对象进行验证,在OAuth2AuthorizationCodeRequestAuthenticationProviderauthenticate方法如下代码处,通过isPrincipalAuthenticated方法判断authenticated 属性:

由于上面转换返回的OAuth2AuthorizationCodeRequestAuthenticationTokenauthenticated = false,if条件成立:

Authentication principal = (Authentication) authorizationCodeRequestAuthentication.getPrincipal();//对authenticated进行判断
if (!isPrincipalAuthenticated(principal)) {if (this.logger.isTraceEnabled()) {this.logger.trace("Did not authenticate authorization code request since principal not authenticated");}// Return the authorization request as-is where isAuthenticated() is false//返回一个authenticated = false的权限对象return authorizationCodeRequestAuthentication;
}

然后将上面isPrincipalAuthenticated方法中,authenticated = false的验证结果authorizationCodeRequestAuthentication对象返回到OAuth2AuthorizationEndpointFilter过滤器中,过滤器doFilterInternal方法的如下代码会判断验证结果的authenticated属性,为false则向下执行过滤器到UsernamePasswordAuthenticationFilter过滤器进行用户登录

if (!authenticationResult.isAuthenticated()) {// If the Principal (Resource Owner) is not authenticated then// pass through the chain with the expectation that the authentication process// will commence via AuthenticationEntryPointfilterChain.doFilter(request, response);return;
}

反之,如果登录后,在OAuth2AuthorizationEndpointFilter过滤器doFilterInternal方法如下代码中,不会在isPrincipalAuthenticated(principal)的if条件处进行return,就不需要登录了

Authentication principal = (Authentication) authorizationCodeRequestAuthentication.getPrincipal();//已登录后,if不成立if (!isPrincipalAuthenticated(principal)) {if (this.logger.isTraceEnabled()) {this.logger.trace("Did not authenticate authorization code request since principal not authenticated");}return authorizationCodeRequestAuthentication;}//............//判断是否要进行授权if (requireAuthorizationConsent(registeredClient, authorizationRequest, currentAuthorizationConsent)) {String state = DEFAULT_STATE_GENERATOR.generateKey();OAuth2Authorization authorization = authorizationBuilder(registeredClient, principal, authorizationRequest).attribute(OAuth2ParameterNames.STATE, state).build();if (this.logger.isTraceEnabled()) {logger.trace("Generated authorization consent state");}this.authorizationService.save(authorization);Set<String> currentAuthorizedScopes = currentAuthorizationConsent != null ?currentAuthorizationConsent.getScopes() : null;if (this.logger.isTraceEnabled()) {this.logger.trace("Saved authorization");}//此处返回的权限对象authenticated属性为truereturn new OAuth2AuthorizationConsentAuthenticationToken(authorizationRequest.getAuthorizationUri(),registeredClient.getClientId(), principal, state, currentAuthorizedScopes, null);
}

4.用户授权同意

源码中的授权同意处理

接上一步,授权码请求后用户完成登录,UsernamePasswordAuthenticationFilter过滤器会在登录后通过缓存,重新向之前需要登录的地址进行重定向(即重新发起授权码请求/oauth2/authorize GET),授权码请求经过转换后,再次到达OAuth2AuthorizationCodeRequestAuthenticationProviderauthenticate方法中进行验证:

此时已登录,不会再进入if代码中,也不会返回isAuthenticated()falseauthorizationCodeRequestAuthentication对象,而是继续向下执行代码,判断请求是否已经过授权:

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {//..................Authentication principal = (Authentication) authorizationCodeRequestAuthentication.getPrincipal();//登录后,不会再进入此if进行return,而是继续向下执行代码if (!isPrincipalAuthenticated(principal)) {if (this.logger.isTraceEnabled()) {this.logger.trace("Did not authenticate authorization code request since principal not authenticated");}// Return the authorization request as-is where isAuthenticated() is falsereturn authorizationCodeRequestAuthentication;}//已登录向下执行//构建OAuth2AuthorizationRequest请求对象,包含所有请求参数和已注册的客户端信息OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest.authorizationCode().authorizationUri(authorizationCodeRequestAuthentication.getAuthorizationUri()).clientId(registeredClient.getClientId()).redirectUri(authorizationCodeRequestAuthentication.getRedirectUri()).scopes(authorizationCodeRequestAuthentication.getScopes()).state(authorizationCodeRequestAuthentication.getState()).additionalParameters(authorizationCodeRequestAuthentication.getAdditionalParameters()).build();//使用当前请求中的客户端id、当前已登录的用户名称,去查询授权同意记录,并将记录作为对象返回//如果使用JDBC实现,这里会去查询数据库的oauth2_authorization_consent表OAuth2AuthorizationConsent currentAuthorizationConsent = this.authorizationConsentService.findById(registeredClient.getId(), principal.getName());//requireAuthorizationConsent方法判断是否需要进行授权同意//主要判断当前请求客户端是否开启了授权同意,以及请求中的scope权限范围,是否被包含在授权记录中的权限范围内//在开启了授权同意情况下,如果查询的授权记录为空,或者授权记录不为空但不包含本次请求的权限范围,则本次请求需要进行用户授权if (requireAuthorizationConsent(registeredClient, authorizationRequest, currentAuthorizationConsent)) {String state = DEFAULT_STATE_GENERATOR.generateKey();OAuth2Authorization authorization = authorizationBuilder(registeredClient, principal, authorizationRequest).attribute(OAuth2ParameterNames.STATE, state).build();if (this.logger.isTraceEnabled()) {logger.trace("Generated authorization consent state");}this.authorizationService.save(authorization);Set<String> currentAuthorizedScopes = currentAuthorizationConsent != null ?currentAuthorizationConsent.getScopes() : null;if (this.logger.isTraceEnabled()) {this.logger.trace("Saved authorization");}//返回需要授权的权限对象,回到OAuth2AuthorizationEndpointFilter过滤器中进行授权页面重定向return new OAuth2AuthorizationConsentAuthenticationToken(authorizationRequest.getAuthorizationUri(),registeredClient.getClientId(), principal, state, currentAuthorizedScopes, null);}//.............}

关键点

  • this.authorizationConsentService.findById():授权记录查询,使用OAuth2AuthorizationConsentService接口,该接口有内存及JDBC两种实现,用于保存客户端授权信息。

  • requireAuthorizationConsent授权判断方法,判断:在请求客户端开启了需要授权同意的情况下,如果该客户端没有授权记录,或者有授权记录、但是授权记录内不包含此次请求的权限范围,则需要进行授权同意;相反则代表此客户端已经过授权

    private static boolean requireAuthorizationConsent(RegisteredClient registeredClient,OAuth2AuthorizationRequest authorizationRequest, OAuth2AuthorizationConsent authorizationConsent) {//发起请求的客户端未开启授权同意,则不需要进行授权if (!registeredClient.getClientSettings().isRequireAuthorizationConsent()) {return false;}// 'openid' 权限的请求不需要进行授权if (authorizationRequest.getScopes().contains(OidcScopes.OPENID) &&authorizationRequest.getScopes().size() == 1) {return false;}//授权记录对象不为空,且请求中的权限范围包含在查询出的授权记录内,也不需要认证//这里if条件为true,代表请求已经被授权过,且请求的权限范围也在授权的范围内if (authorizationConsent != null &&authorizationConsent.getScopes().containsAll(authorizationRequest.getScopes())) {return false;}//不满足以上条件则需要授权return true;
    }
    
  • 返回OAuth2AuthorizationConsentAuthenticationToken:将OAuth2AuthorizationCodeRequestAuthenticationProviderauthenticate方法验证后的结果,返回到OAuth2AuthorizationEndpointFilter过滤器中,进行授权页面重定向:

    public final class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilter {@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {//................Authentication authenticationResult = this.authenticationManager.authenticate(authentication);//如果认证返回的是OAuth2AuthorizationConsentAuthenticationToken,则向授权页面重定向if (authenticationResult instanceof OAuth2AuthorizationConsentAuthenticationToken) {if (this.logger.isTraceEnabled()) {this.logger.trace("Authorization consent is required");}//重定向方法sendAuthorizationConsent(request, response,(OAuth2AuthorizationCodeRequestAuthenticationToken) authentication,(OAuth2AuthorizationConsentAuthenticationToken) authenticationResult);return;}//................	}
    }
    

5.生成授权码并返回

OAuth2AuthorizationCodeRequestAuthenticationProvider中生成授权码code,并通过OAuth2AuthorizationEndpointFilter过滤器重定向回客户端

继续接上面的步骤,当登录、授权都通过后,代码执行到OAuth2AuthorizationCodeRequestAuthenticationProviderauthenticate方法如下生成授权码注释位置:

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {//..................//判断登录处if (!isPrincipalAuthenticated(principal)) {if (this.logger.isTraceEnabled()) {this.logger.trace("Did not authenticate authorization code request since principal not authenticated");}return authorizationCodeRequestAuthentication;}//.............//判断授权处if (requireAuthorizationConsent(registeredClient, authorizationRequest, currentAuthorizationConsent)) {//.............return new OAuth2AuthorizationConsentAuthenticationToken(authorizationRequest.getAuthorizationUri(),registeredClient.getClientId(), principal, state, currentAuthorizedScopes, null);}//生成授权码OAuth2TokenContext tokenContext = createAuthorizationCodeTokenContext(authorizationCodeRequestAuthentication, registeredClient, null, authorizationRequest.getScopes());//使用authorizationCodeGenerator生成授权码OAuth2AuthorizationCode authorizationCode = this.authorizationCodeGenerator.generate(tokenContext);if (authorizationCode == null) {OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,"The token generator failed to generate the authorization code.", ERROR_URI);throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, null);}if (this.logger.isTraceEnabled()) {this.logger.trace("Generated authorization code");}OAuth2Authorization authorization = authorizationBuilder(registeredClient, principal, authorizationRequest).authorizedScopes(authorizationRequest.getScopes()).token(authorizationCode).build();//保存带有授权码的授权信息this.authorizationService.save(authorization);if (this.logger.isTraceEnabled()) {this.logger.trace("Saved authorization");}//取得客户端的重定向地址,用于授权服务将授权码返回给客户端String redirectUri = authorizationRequest.getRedirectUri();if (!StringUtils.hasText(redirectUri)) {redirectUri = registeredClient.getRedirectUris().iterator().next();}if (this.logger.isTraceEnabled()) {this.logger.trace("Authenticated authorization code request");}//返回包含授权码的权限对象,用于在过滤器OAuth2AuthorizationEndpointFilter中进行重定向返回return new OAuth2AuthorizationCodeRequestAuthenticationToken(authorizationRequest.getAuthorizationUri(),registeredClient.getClientId(), principal, authorizationCode, redirectUri,authorizationRequest.getState(), authorizationRequest.getScopes());}

授权码返回客户端

回到OAuth2AuthorizationEndpointFilter过滤器的doFilterInternal方法中,经过转换与验证,且登录与授权都通过后,最后会通过authenticationSuccessHandler向客户端请求中的redirect_url进行重定向(携带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 {//将请求转为权限对象Authentication authentication = this.authenticationConverter.convert(request);//添加一些附加的信息,包括客户端 IP 地址、会话 ID 等if (authentication instanceof AbstractAuthenticationToken) {((AbstractAuthenticationToken) authentication).setDetails(this.authenticationDetailsSource.buildDetails(request));}//对上面转换的请求对象进行验证Authentication authenticationResult = this.authenticationManager.authenticate(authentication);//登录判断if (!authenticationResult.isAuthenticated()) {filterChain.doFilter(request, response);return;}//授权判断if (authenticationResult instanceof OAuth2AuthorizationConsentAuthenticationToken) {if (this.logger.isTraceEnabled()) {this.logger.trace("Authorization consent is required");}//发起授权页面重定向sendAuthorizationConsent(request, response,(OAuth2AuthorizationCodeRequestAuthenticationToken) authentication,(OAuth2AuthorizationConsentAuthenticationToken) authenticationResult);return;}//如果前面的认证都通过(用户已登录,且对请求完成授权),这里调用sendAuthorizationResponse方法进行带code的重定向,//重定向地址是客户端请求/oauth2/authorize时带的redirect_urlthis.authenticationSuccessHandler.onAuthenticationSuccess(request, response, authenticationResult);} catch (OAuth2AuthenticationException ex) {if (this.logger.isTraceEnabled()) {this.logger.trace(LogMessage.format("Authorization request failed: %s", ex.getError()), ex);}this.authenticationFailureHandler.onAuthenticationFailure(request, response, ex);}
}

重定向执行

this.authenticationSuccessHandler使用的是OAuth2AuthorizationEndpointFilter过滤器中自带的方法,方法内使用的是DefaultRedirectStrategy进行实际重定向:

public final class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilter {private AuthenticationSuccessHandler authenticationSuccessHandler = this::sendAuthorizationResponse;private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();private void sendAuthorizationResponse(HttpServletRequest request, HttpServletResponse response,Authentication authentication) throws IOException {OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =(OAuth2AuthorizationCodeRequestAuthenticationToken) authentication;//使用构建器向url中添加授权码		UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(authorizationCodeRequestAuthentication.getRedirectUri()).queryParam(OAuth2ParameterNames.CODE, authorizationCodeRequestAuthentication.getAuthorizationCode().getTokenValue());//构建请求urlString redirectUri;if (StringUtils.hasText(authorizationCodeRequestAuthentication.getState())) {uriBuilder.queryParam(OAuth2ParameterNames.STATE, "{state}");Map<String, String> queryParams = new HashMap<>();queryParams.put(OAuth2ParameterNames.STATE, authorizationCodeRequestAuthentication.getState());redirectUri = uriBuilder.build(queryParams).toString();} else {redirectUri = uriBuilder.toUriString();}//发起重定向this.redirectStrategy.sendRedirect(request, response, redirectUri);}}

6.总结

综合OAuth2AuthorizationEndpointFilter过滤器及其使用的转换器与Provider验证器来看,整个流程中OAuth2AuthorizationCodeRequestAuthenticationProvider起到关键作用,其内部主要做了以下事情:

  1. 验证用户是否已登录。
  2. 验证请求是否已授权。
  3. 在用户已登录且已对请求进行授权后,为授权请求生成授权码。

OAuth2AuthorizationEndpointFilter过滤器在调用OAuth2AuthorizationCodeRequestAuthenticationProvider时,做了如下事情:

  1. Provider检测用户未登录时,过滤器向下执行过滤,让用户进行登录。
  2. Provider检测请求未授权时,过滤器调用授权同意请求的转换器及验证器完成授权。
  3. Provider检测用户已登录且已授权后,过滤器调用authenticationSuccessHandler携带授权码向客户端重定向。


授权请求转换源码解析

OAuth2AuthorizationCodeRequestAuthenticationConverter

OAuth2AuthorizationCodeRequestAuthenticationConverter 是一个转换器类,用于将 OAuth 2.0 授权码请求(/oauth2/authorize GET)转换为认证对象。

在授权码授权流程中,该转换器负责将 HTTP 请求中的参数提取并封装到 OAuth2AuthorizationCodeRequestAuthenticationToken 权限对象中,以便后续的认证处理。下面是详细的讲解:

代码详细讲解

public Authentication convert(HttpServletRequest request) {// 仅处理 GET 请求或符合 OIDC 请求匹配器的请求if (!"GET".equals(request.getMethod()) && !OIDC_REQUEST_MATCHER.matches(request)) {return null;}// 获取请求参数,从请求中提取所有参数并存储在 `MultiValueMap` 中MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);// 处理 response_type 参数// 检查 `response_type` 参数是否存在且仅有一个值,且其值必须为 `code`。// 如果检查失败,则抛出相应的 OAuth2 错误。String responseType = request.getParameter(OAuth2ParameterNames.RESPONSE_TYPE);if (!StringUtils.hasText(responseType) || parameters.get(OAuth2ParameterNames.RESPONSE_TYPE).size() != 1) {throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.RESPONSE_TYPE);} else if (!responseType.equals(OAuth2AuthorizationResponseType.CODE.getValue())) {throwError(OAuth2ErrorCodes.UNSUPPORTED_RESPONSE_TYPE, OAuth2ParameterNames.RESPONSE_TYPE);}//获取重定向地址String authorizationUri = request.getRequestURL().toString();// 处理 client_id 参数,检查 `client_id` 参数是否存在且仅有一个值,否则抛出错误String clientId = parameters.getFirst(OAuth2ParameterNames.CLIENT_ID);if (!StringUtils.hasText(clientId) || parameters.get(OAuth2ParameterNames.CLIENT_ID).size() != 1) {throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID);}// 从上下文存储中获取已经过认证的用户信息。如果为空,则使用匿名认证Authentication principal = SecurityContextHolder.getContext().getAuthentication();if (principal == null) {principal = ANONYMOUS_AUTHENTICATION;}// 处理 redirect_uri 参数(可选)// 判断请求是否包含redirect_uri参数,如果包含,其值只能有一个,否则抛出异常String redirectUri = parameters.getFirst(OAuth2ParameterNames.REDIRECT_URI);if (StringUtils.hasText(redirectUri) && parameters.get(OAuth2ParameterNames.REDIRECT_URI).size() != 1) {throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI);}// 处理 scope 参数(可选)// 判断请求是否包含scope参数,如果包含,其值只能有一个,否则抛出异常Set<String> scopes = null;String scope = parameters.getFirst(OAuth2ParameterNames.SCOPE);if (StringUtils.hasText(scope) && parameters.get(OAuth2ParameterNames.SCOPE).size() != 1) {throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.SCOPE);}if (StringUtils.hasText(scope)) {scopes = new HashSet<>(Arrays.asList(StringUtils.delimitedListToStringArray(scope, " ")));}// 处理 state 参数(推荐)// 判断请求是否包含state参数,如果包含,其值只能有一个,否则抛出异常String state = parameters.getFirst(OAuth2ParameterNames.STATE);if (StringUtils.hasText(state) && parameters.get(OAuth2ParameterNames.STATE).size() != 1) {throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE);}// 处理 code_challenge 参数(公共客户端需要)- 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 参数(公共客户端可选)- RFC 7636 (PKCE)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);}// 处理其他附加参数// 将请求参数中不属于`response_type`, `client_id`, `redirect_uri`, `scope`, `state`的其他参数作为附加参数存入mapMap<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));}});// 返回封装后的认证对象// 将提取的参数和信息封装成一个 `OAuth2AuthorizationCodeRequestAuthenticationToken` 对象并返回。// 该对象包含了授权请求的所有必要信息,如 `authorizationUri`, `clientId`, `principal`, `redirectUri`, `state`, `scopes`, 以及附加参数 `additionalParameters`。return new OAuth2AuthorizationCodeRequestAuthenticationToken(authorizationUri, clientId, principal,redirectUri, state, scopes, additionalParameters);
}

总结

OAuth2AuthorizationCodeRequestAuthenticationConverter 负责将 OAuth 2.0 授权码请求转换为 OAuth2AuthorizationCodeRequestAuthenticationToken 对象。具体步骤包括:

  1. 检查请求方法是否为 GET 或是否符合 OIDC 请求匹配器。
  2. 提取请求参数并进行验证。
  3. 验证和处理 response_type, client_id, redirect_uri, scope, state, code_challenge, 和 code_challenge_method 等参数。其中response_type, client_id为必须,没有会抛异常,其他为可选。
  4. 提取附加参数。
  5. 将所有提取的信息封装到 OAuth2AuthorizationCodeRequestAuthenticationToken 对象中并返回。

通过这些步骤,该转换器确保授权码请求的参数有效且完整,为后续的认证处理提供所需的信息。



授权请求认证源码解析

OAuth2AuthorizationCodeRequestAuthenticationProviderauthenticate方法

OAuth2AuthorizationCodeRequestAuthenticationProvider 类负责处理 OAuth 2.0 授权码请求( /oauth2/authorize GET)的验证过程,主要检查用户是否在授权服务登录,以及请求是否被同意授权,已登录且已授权后生成code返回给过滤器

源码解析

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {//将传入的权限对象进行类型转换,方便下文处理OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =(OAuth2AuthorizationCodeRequestAuthenticationToken) authentication;//根据请求中的客户端id,去查询授权服务中已注册的客户端信息RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(authorizationCodeRequestAuthentication.getClientId());//如果请求中的客户端不存在(即未注册),则抛出错误			if (registeredClient == null) {throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID,authorizationCodeRequestAuthentication, null);}if (this.logger.isTraceEnabled()) {this.logger.trace("Retrieved registered client");}//创建 `OAuth2AuthorizationCodeRequestAuthenticationContext` 上下文对象,参数包含授权请求和已注册的客户端信息OAuth2AuthorizationCodeRequestAuthenticationContext authenticationContext = OAuth2AuthorizationCodeRequestAuthenticationContext.with(authorizationCodeRequestAuthentication).registeredClient(registeredClient).build();/*** 使用 `authenticationValidator` 对上下文对象进行验证,验证的是请求中的重定向地址redirect_uri:*  1.redirect_uri不能是null与localhost;*  2.redirect_uri如果不是回环地址(127.0.0.1),则请求中的redirect_uri必须与注册客户端中的redirect_uri一致*  3.redirect_uri如果是回环地址,则授权服务必须允许redirect_uri可以指定使用任何端口*  4.如果请求的范围scopes中包含openid,redirect_uri必须有且仅有一个值* */this.authenticationValidator.accept(authenticationContext);//检查已注册的客户端是否支持授权码授权类型,如果注册客户端不支持,也无法进行请求认证if (!,registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.AUTHORIZATION_CODE)) {throwError(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT, OAuth2ParameterNames.CLIENT_ID,authorizationCodeRequestAuthentication, registeredClient);}//获取code_challenge参数用于PKCE验证,PKCE是一种安全增强机制,主要用于公共客户端String codeChallenge = (String) authorizationCodeRequestAuthentication.getAdditionalParameters().get(PkceParameterNames.CODE_CHALLENGE);//如果有code_challenge则进行校验if (StringUtils.hasText(codeChallenge)) {//获取code_challenge的加密方法String codeChallengeMethod = (String) authorizationCodeRequestAuthentication.getAdditionalParameters().get(PkceParameterNames.CODE_CHALLENGE_METHOD);//如果code_challenge的加密方法为空或不是"S256"(哈希算法SHA-256)方法,则报错if (!StringUtils.hasText(codeChallengeMethod) || !"S256".equals(codeChallengeMethod)) {throwError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE_METHOD, PKCE_ERROR_URI,authorizationCodeRequestAuthentication, registeredClient, null);}//如果 code_challenge 不存在,并且注册客户端的配置还要求使用PKCE,则抛出 INVALID_REQUEST 错误    } else if (registeredClient.getClientSettings().isRequireProofKey()) {throwError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE, PKCE_ERROR_URI,authorizationCodeRequestAuthentication, registeredClient, null);}if (this.logger.isTraceEnabled()) {this.logger.trace("Validated authorization code request parameters");}// ---------------// The request is valid - ensure the resource owner is authenticated// ---------------//检查用户是否已登录,未登录则返回authenticated=false的权限对象//当`principal`不为空,不是匿名访问,并且通过登录认证,三个条件同时成立才能算已经认证,否则就是未认证//不满足条件后进行return结束方法,后面会重定向到`/login`页面进行用户登录Authentication principal = (Authentication) authorizationCodeRequestAuthentication.getPrincipal();if (!isPrincipalAuthenticated(principal)) {if (this.logger.isTraceEnabled()) {this.logger.trace("Did not authenticate authorization code request since principal not authenticated");}// Return the authorization request as-is where isAuthenticated() is falsereturn authorizationCodeRequestAuthentication;}//构建 `OAuth2AuthorizationRequest` 请求对象,包含所有请求参数和已注册的客户端信息OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest.authorizationCode().authorizationUri(authorizationCodeRequestAuthentication.getAuthorizationUri()).clientId(registeredClient.getClientId()).redirectUri(authorizationCodeRequestAuthentication.getRedirectUri()).scopes(authorizationCodeRequestAuthentication.getScopes()).state(authorizationCodeRequestAuthentication.getState()).additionalParameters(authorizationCodeRequestAuthentication.getAdditionalParameters()).build();//使用当前请求中的客户端id、当前已登录的用户名称,去查询授权同意记录,并将记录作为对象返回//如果authorizationConsentService使用JDBC实现,这里会去查询数据库的oauth2_authorization_consent表OAuth2AuthorizationConsent currentAuthorizationConsent = this.authorizationConsentService.findById(registeredClient.getId(), principal.getName());//requireAuthorizationConsent方法判断是否需要进行授权同意// 主要判断当前请求客户端是否开启了授权同意,以及请求中的scope权限范围,是否被包含在授权记录中的权限范围内// 在开启了授权同意情况下,如果查询的授权记录为空,或者授权记录不为空但不包含本次请求的权限范围,则本次请求需要进行用户授权if (requireAuthorizationConsent(registeredClient, authorizationRequest, currentAuthorizationConsent)) {//生成一个随机的 state 参数。这个参数用于防止 CSRF(跨站请求伪造)攻击,确保请求的完整性。// state参数也会在授权同意请求进行验证时被用到String state = DEFAULT_STATE_GENERATOR.generateKey();//创建一个新的 OAuth2Authorization 对象,该对象包含了客户端、用户和授权请求的信息。//使用.attribute(OAuth2ParameterNames.STATE, state) 将生成的 state 参数添加到授权对象中。OAuth2Authorization authorization = authorizationBuilder(registeredClient, principal, authorizationRequest).attribute(OAuth2ParameterNames.STATE, state).build();if (this.logger.isTraceEnabled()) {logger.trace("Generated authorization consent state");}//将创建的包含state参数的授权对象保存到持久化存储中, 后续流程验证会使用this.authorizationService.save(authorization);//如果上面根据客户带你id能查到授权记录,则取出此授权记录的权限范围Scopes,否则为nullSet<String> currentAuthorizedScopes = currentAuthorizationConsent != null ?currentAuthorizationConsent.getScopes() : null;if (this.logger.isTraceEnabled()) {this.logger.trace("Saved authorization");}//结束方法的执行,创建并返回一个代表需要用户授权的权限对象,由OAuth2AuthorizationEndpointFilter过滤器向用户授权界面进行重定向//对象中包含授权请求的 URI、客户端 ID、用户主体、生成的状态参数、当前已授权的范围等信息return new OAuth2AuthorizationConsentAuthenticationToken(authorizationRequest.getAuthorizationUri(),registeredClient.getClientId(), principal, state, currentAuthorizedScopes, null);}/**下面的代码,在用户已登录授权服务,且已对授权请求完成授权同意的情况下执行*///使用权限对象、注册的客户端以及请求的授权范围等信息,来创建一个生成授权码所需的上下文对象。OAuth2TokenContext tokenContext = createAuthorizationCodeTokenContext(authorizationCodeRequestAuthentication, registeredClient, null, authorizationRequest.getScopes());//使用authorizationCodeGenerator工具,根据上下文信息来生成一个授权码OAuth2AuthorizationCode authorizationCode = this.authorizationCodeGenerator.generate(tokenContext);//为空则代表生成失败,抛出异常if (authorizationCode == null) {OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,"The token generator failed to generate the authorization code.", ERROR_URI);throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, null);}//记录日志if (this.logger.isTraceEnabled()) {this.logger.trace("Generated authorization code");}//如果授权码生成成功,用授权构建器创建一个新的权限验证对象,将生成的授权码和请求的授权范围添加到此对象中,OAuth2Authorization authorization = authorizationBuilder(registeredClient, principal, authorizationRequest).authorizedScopes(authorizationRequest.getScopes()).token(authorizationCode).build();//然后将上面的对象保存到 authorizationService 中,如果是JDBC实现则保存到'oauth2_authorization'表this.authorizationService.save(authorization);if (this.logger.isTraceEnabled()) {this.logger.trace("Saved authorization");		}//获取请求中的客户端重定向地址String redirectUri = authorizationRequest.getRedirectUri();//如果没有,则从注册客户端的重定向 URI 列表中获取第一个作为默认重定向 URI。if (!StringUtils.hasText(redirectUri)) {redirectUri = registeredClient.getRedirectUris().iterator().next();}if (this.logger.isTraceEnabled()) {this.logger.trace("Authenticated authorization code request");}//创建并返回一个新的对象,其中包含了授权请求的 URI、客户端 ID、主体、授权码、重定向 URI、状态以及请求的范围等信息//返回到OAuth2AuthorizationEndpointFilter过滤器中,由其将授权码重定向到客户端return new OAuth2AuthorizationCodeRequestAuthenticationToken(authorizationRequest.getAuthorizationUri(),registeredClient.getClientId(), principal, authorizationCode, redirectUri,authorizationRequest.getState(), authorizationRequest.getScopes());
}

总结

OAuth2AuthorizationCodeRequestAuthenticationProviderauthenticate方法的大体处理流程

  1. 根据请求的客户端来检索授权服务中已注册的客户端,未注册的客户端验证不通过
  2. 验证请求redirect_uri重定向地址的有效性
  3. 检查请求的授权类型是否为授权码类型,校验code_challenge参数(如果有)
  4. 检查用户是否已登录,未登录则结束认证方法,由授权服务向登录页重定向,来进行用户登录
  5. 验证已登录后,检查客户端请求是否被同意授权,未授权则结束方法,由授权服务向收取按同意页重定向,来进行用户授权
  6. 验证已登录且已完成请求授权后,生成授权码code,返回给过滤器并携带授权码向客户端重定向,把授权码给客户端


授权同意请求转换源码解析

OAuth2AuthorizationConsentAuthenticationConverter

OAuth2AuthorizationConsentAuthenticationConverter 用于将 OAuth 2.0 授权同意( /oauth2/authorize POST)请求转换为 OAuth2AuthorizationConsentAuthenticationToken 对象。

授权同意请求通常是在用户同意授权客户端访问其资源时发生的。下面是代码的详细解释:

源码详解

public Authentication convert(HttpServletRequest request) {// 确保请求方法是 POST。// 如果请求包含 `response_type` 参数,则返回 null。授权同意请求不应该包含 `response_type` 参数if (!"POST".equals(request.getMethod()) ||request.getParameter(OAuth2ParameterNames.RESPONSE_TYPE) != null) {return null;}//从请求中提取所有参数,并存储在一个 `MultiValueMap` 中。MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);//获取请求的完整 URL 作为授权 URIString authorizationUri = request.getRequestURL().toString();// `client_id` 参数是必需的,且只能有一个值。如果不符合条件,则抛出错误。String clientId = parameters.getFirst(OAuth2ParameterNames.CLIENT_ID);if (!StringUtils.hasText(clientId) ||parameters.get(OAuth2ParameterNames.CLIENT_ID).size() != 1) {throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID);}//从当前安全上下文中获取用户认证信息。如果不存在,则设置为匿名认证Authentication principal = SecurityContextHolder.getContext().getAuthentication();if (principal == null) {principal = ANONYMOUS_AUTHENTICATION;}// `state` 参数是必需的,且只能有一个值。如果不符合条件,则抛出错误。String state = parameters.getFirst(OAuth2ParameterNames.STATE);if (!StringUtils.hasText(state) ||parameters.get(OAuth2ParameterNames.STATE).size() != 1) {throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE);}// `scope` 参数是可选的。如果存在,则将其值存储在一个集合中。Set<String> scopes = null;if (parameters.containsKey(OAuth2ParameterNames.SCOPE)) {scopes = new HashSet<>(parameters.get(OAuth2ParameterNames.SCOPE));}//将请求参数中不属于 `client_id`, `state`, `scope` 的其他参数作为附加参数存入 `additionalParameters`。Map<String, Object> additionalParameters = new HashMap<>();parameters.forEach((key, value) -> {if (!key.equals(OAuth2ParameterNames.CLIENT_ID) &&!key.equals(OAuth2ParameterNames.STATE) &&!key.equals(OAuth2ParameterNames.SCOPE)) {additionalParameters.put(key, value.get(0));}});/*将提取的参数和信息封装成一个 `OAuth2AuthorizationConsentAuthenticationToken` 对象并返回。该对象包含了授权同意请求的所有必要信息,如 `authorizationUri`, `clientId`, `principal`, `state`, `scopes`, 以及附加参数 `additionalParameters`。*/return new OAuth2AuthorizationConsentAuthenticationToken(authorizationUri, clientId, principal,state, scopes, additionalParameters);
}

总结

OAuth2AuthorizationConsentAuthenticationConverter 负责将 OAuth 2.0 授权同意请求转换为 OAuth2AuthorizationConsentAuthenticationToken 对象。具体步骤包括:

  1. 检查请求方法是否为 POST,并确保请求不包含 response_type 参数。
  2. 提取请求参数并进行验证。
  3. 验证和处理 client_id, state, scope 等参数。
  4. 提取附加参数。
  5. 将所有提取的信息封装到 OAuth2AuthorizationConsentAuthenticationToken 对象中并返回。

通过这些步骤,该转换器确保授权同意请求的参数有效且完整,为后续的认证处理提供所需的信息。



授权同意请求认证源码解析

OAuth2AuthorizationConsentAuthenticationProviderauthenticate方法

OAuth2AuthorizationConsentAuthenticationProvider 类负责处理 OAuth 2.0 授权同意请求( /oauth2/authorize POST)的验证过程,主要检查用户身份是否有效,客户端是否注册,以及请求权限范围是否有效等,并在验证成功后生成code返回给过滤器

源码详解

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {//转换参数类型方便下文处理OAuth2AuthorizationConsentAuthenticationToken authorizationConsentAuthentication =(OAuth2AuthorizationConsentAuthenticationToken) authentication;//通过`authorizationService`使用`state`查找授权对象。如果授权对象为空,则抛出`INVALID_REQUEST`错误//在前面的`OAuth2AuthorizationCodeRequestAuthenticationProvider`的`authenticate`方法中,在检测到请求需要用户授权同意后,会生成一个`state`参数,并通过`OAuth2AuthorizationService`进行保存OAuth2Authorization authorization = this.authorizationService.findByToken(authorizationConsentAuthentication.getState(), STATE_TOKEN_TYPE);if (authorization == null) {throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE,authorizationConsentAuthentication, null, null);}//如果授权对象成功获取,记录日志if (this.logger.isTraceEnabled()) {this.logger.trace("Retrieved authorization with authorization consent state");}//获取传入权限对象中的principal,如果用户已登录,principal中会包含用户信息Authentication principal = (Authentication) authorizationConsentAuthentication.getPrincipal();//如果用户未登录,或者权限对象中的用户信息与上面通过state查出来的不一致,则报错if (!isPrincipalAuthenticated(principal) || !principal.getName().equals(authorization.getPrincipalName())) {throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE,authorizationConsentAuthentication, null, null);}//根据权限对象中的客户端id,去找授权服务中已注册的客户端信息,并进行对比//如果是JDBC实现,对应查询`oauth2_registered_client`表RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(authorizationConsentAuthentication.getClientId());//授权服务中不存在权限对象中的客户端、或者查出来的客户端信息对不上,则报错if (registeredClient == null || !registeredClient.getId().equals(authorization.getRegisteredClientId())) {throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID,authorizationConsentAuthentication, registeredClient, null);}//客户端验证无误后记录日志if (this.logger.isTraceEnabled()) {this.logger.trace("Retrieved registered client");}OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(OAuth2AuthorizationRequest.class.getName());//根据方法上方使用`state`参数从`authorizationService`中查出的授权对象为依据,取出其权限范围,用来做对比Set<String> requestedScopes = authorizationRequest.getScopes();//取出授权同意请求的权限范围Set<String> authorizedScopes = new HashSet<>(authorizationConsentAuthentication.getScopes());//对比两个权限范围,验证授权同意请求中的范围是否在`state`参数查出的权限范围之内if (!requestedScopes.containsAll(authorizedScopes)) {throwError(OAuth2ErrorCodes.INVALID_SCOPE, OAuth2ParameterNames.SCOPE,authorizationConsentAuthentication, registeredClient, authorizationRequest);}//一致则记录日志if (this.logger.isTraceEnabled()) {this.logger.trace("Validated authorization consent request parameters");}//从授权同意记录存储中,根据当前请求的客户端id与认证用户名取出授权记录数据OAuth2AuthorizationConsent currentAuthorizationConsent = this.authorizationConsentService.findById(authorization.getRegisteredClientId(), authorization.getPrincipalName());//如果存在授权同意记录,则取出其权限范围Set<String> currentAuthorizedScopes = currentAuthorizationConsent != null ?currentAuthorizationConsent.getScopes() : Collections.emptySet();//如果授权同意记录中存在授权的范围,则检查请求的范围是否在已授权范围内,并将其添加到最终授权的范围集合中。//这么做的目的,是确保给予客户端的权限中只能包含已经被用户批准的范围if (!currentAuthorizedScopes.isEmpty()) {for (String requestedScope : requestedScopes) {if (currentAuthorizedScopes.contains(requestedScope)) {authorizedScopes.add(requestedScope);}}}//如果授权范围集合不为空且请求的范围包含 openid,则自动批准 openid 范围,因为该范围不需要用户的额外同意if (!authorizedScopes.isEmpty() && requestedScopes.contains(OidcScopes.OPENID)) {// 'openid' scope is auto-approved as it does not require consentauthorizedScopes.add(OidcScopes.OPENID);}//根据是否存在当前的授权同意对象,创建或更新一个授权同意构建器(authorizationConsentBuilder)。OAuth2AuthorizationConsent.Builder authorizationConsentBuilder;//如果存在,则从当前对象创建新的构建器;if (currentAuthorizationConsent != null) {if (this.logger.isTraceEnabled()) {this.logger.trace("Retrieved existing authorization consent");}authorizationConsentBuilder = OAuth2AuthorizationConsent.from(currentAuthorizationConsent);//如果不存在,则根据注册的客户端 ID 和主体名称创建新的授权同意构建器。} else {authorizationConsentBuilder = OAuth2AuthorizationConsent.withId(authorization.getRegisteredClientId(), authorization.getPrincipalName());}//然后,将所有授权的范围添加到构建器中authorizedScopes.forEach(authorizationConsentBuilder::scope);//如果存在自定义授权同意处理器,使用它来处理授权同意请求if (this.authorizationConsentCustomizer != null) {// @formatter:offOAuth2AuthorizationConsentAuthenticationContext authorizationConsentAuthenticationContext =OAuth2AuthorizationConsentAuthenticationContext.with(authorizationConsentAuthentication).authorizationConsent(authorizationConsentBuilder).registeredClient(registeredClient).authorization(authorization).authorizationRequest(authorizationRequest).build();// @formatter:onthis.authorizationConsentCustomizer.accept(authorizationConsentAuthenticationContext);if (this.logger.isTraceEnabled()) {this.logger.trace("Customized authorization consent");}}//处理授权同意被拒绝的情况Set<GrantedAuthority> authorities = new HashSet<>();authorizationConsentBuilder.authorities(authorities::addAll);if (authorities.isEmpty()) {//如果授权同意被拒绝或撤销,移除当前的授权同意和授权,并抛出 `ACCESS_DENIED` 错误。if (currentAuthorizationConsent != null) {this.authorizationConsentService.remove(currentAuthorizationConsent);if (this.logger.isTraceEnabled()) {this.logger.trace("Revoked authorization consent");}}//移除当前的授权同意和授权this.authorizationService.remove(authorization);if (this.logger.isTraceEnabled()) {this.logger.trace("Removed authorization");}//抛出 `ACCESS_DENIED` 错误throwError(OAuth2ErrorCodes.ACCESS_DENIED, OAuth2ParameterNames.CLIENT_ID,authorizationConsentAuthentication, registeredClient, authorizationRequest);}//如果新的授权同意与当前的不相同,则保存新的授权同意信息OAuth2AuthorizationConsent authorizationConsent = authorizationConsentBuilder.build();if (!authorizationConsent.equals(currentAuthorizationConsent)) {this.authorizationConsentService.save(authorizationConsent);if (this.logger.isTraceEnabled()) {this.logger.trace("Saved authorization consent");}}//下面是生成授权码的逻辑//使用权限对象、注册的客户端以及请求的授权范围等信息,来创建一个生成授权码所需的上下文对象OAuth2TokenContext tokenContext = createAuthorizationCodeTokenContext(authorizationConsentAuthentication, registeredClient, authorization, authorizedScopes);//使用authorizationCodeGenerator工具,根据上下文信息来生成一个授权码OAuth2AuthorizationCode authorizationCode = this.authorizationCodeGenerator.generate(tokenContext);//为空则代表生成失败,抛出异常if (authorizationCode == null) {OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,"The token generator failed to generate the authorization code.", ERROR_URI);throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, null);}//生成成功记录日志if (this.logger.isTraceEnabled()) {this.logger.trace("Generated authorization code");}//更新保存在授权服务的客户端授权记录信息,多了授权的范围和授权码,并保存更新后的授权OAuth2Authorization updatedAuthorization = OAuth2Authorization.from(authorization).authorizedScopes(authorizedScopes).token(authorizationCode).attributes(attrs -> {attrs.remove(OAuth2ParameterNames.STATE);}).build();//执行更新保存this.authorizationService.save(updatedAuthorization);//记录日志if (this.logger.isTraceEnabled()) {this.logger.trace("Saved authorization");}//获取请求中的客户端重定向地址String redirectUri = authorizationRequest.getRedirectUri();//如果没有,则从注册客户端的重定向 URI 列表中获取第一个作为默认重定向 URI。if (!StringUtils.hasText(redirectUri)) {redirectUri = registeredClient.getRedirectUris().iterator().next();}//记录日志if (this.logger.isTraceEnabled()) {this.logger.trace("Authenticated authorization consent request");}//创建并返回一个新的对象,其中包含了授权请求的 URI、客户端 ID、认证用户信息、授权码、重定向URI、状态以及请求的范围等信息//返回到OAuth2AuthorizationEndpointFilter过滤器中,由其将授权码重定向到客户端return new OAuth2AuthorizationCodeRequestAuthenticationToken(authorizationRequest.getAuthorizationUri(), registeredClient.getClientId(), principal, authorizationCode,redirectUri, authorizationRequest.getState(), authorizedScopes);
}

总结

流程大致总结

  1. 客户端授权请求记录中是否存在state参数
  2. 验证用户是否登录、身份是否有效,客户端是否已经注册
  3. 检查请求的授权范围,并确保给与的权限范围只包括用户批准的权限
  4. 生成授权码,更新客户端请求授权的信息记录,最后返回带有授权码的权限对象,用于过滤器进行重定向返回给客户端

两个Provider在生成授权码处的区别

经过源码分析,授权码请求认证类OAuth2AuthorizationCodeRequestAuthenticationProvider与授权同意请求认证类
OAuth2AuthorizationConsentAuthenticationConverter的认证处理中,均包含code授权码生成逻辑:

是因为存在客户端已被授权同意情况下,再次请求授权码的情况。当客户端已经被允许授权的情况下,再申请授权码时,会在OAuth2AuthorizationCodeRequestAuthenticationProvider认证后直接返回授权码,不需要再进行一遍授权同意了。




客户端认证过滤器

OAuth2ClientAuthenticationFilter过滤器

在客户端获得授权码code,并携带code发起POST /oauth2/token请求后,请求会先进入OAuth2AuthorizationEndpointFilter,验证客户端身份有效性,验证通过后在由OAuth2TokenEndpointFilter生成token


认证方式

简介

针对POST /oauth2/token请求的客户端认证,spring security oauth2中自带四种认证方式,可以自行扩展

分别为:

  • jwt认证方式
  • client_secret_basic方式
  • client_secret_post方式
  • PKCE方式

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

列举一个使用Controller注册客户端的示例:

@RestController
public class RegisteredController {//注册客户端的保存类,有 数据库(oauth2_registered_client表) 和 内存(Map) 保存两种自带实现,默认内存@Resourceprivate RegisteredClientRepository registeredClientRepository;@GetMapping("/addClient")public String addClient() {RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())// 客户端ID.clientId("test-client")//指定密钥,bcrypt密文,noop明文//.clientSecret("{bcrypt}" + new BCryptPasswordEncoder().encode("secret")).clientSecret("{noop}secret")// 客户端认证方式,这里指定使用`client_secret_basic`方式,即请求头加'Authorization'参数//ClientAuthenticationMethod的常量为各种配置方式的字符串.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC).build();//注册客户端registeredClientRepository.save(registeredClient);return "添加客户端信息成功";}     }    

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

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

public final class ClientAuthenticationMethod implements Serializable {private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;/*** @deprecated Use {@link #CLIENT_SECRET_BASIC} 弃用*/@Deprecatedpublic static final ClientAuthenticationMethod BASIC = new ClientAuthenticationMethod("basic");/*** @since 5.5*/public static final ClientAuthenticationMethod CLIENT_SECRET_BASIC = new ClientAuthenticationMethod("client_secret_basic");/*** @deprecated Use {@link #CLIENT_SECRET_POST} 弃用*/@Deprecatedpublic static final ClientAuthenticationMethod POST = new ClientAuthenticationMethod("post");/*** @since 5.5*/public static final ClientAuthenticationMethod CLIENT_SECRET_POST = new ClientAuthenticationMethod("client_secret_post");/*** @since 5.5*/public static final ClientAuthenticationMethod CLIENT_SECRET_JWT = new ClientAuthenticationMethod("client_secret_jwt");/*** @since 5.5*/public static final ClientAuthenticationMethod PRIVATE_KEY_JWT = new ClientAuthenticationMethod("private_key_jwt");/*** @since 5.2*/public static final ClientAuthenticationMethod NONE = new ClientAuthenticationMethod("none");}

jwt认证方式

对应的是ClientAuthenticationMethod中的client_secret_jwtprivate_key_jwt

客户端会使用加密算法及密钥生成一个JWT字符串,通过/oauth2/token请求传到授权服务中,授权服务再用相同算法及密钥解密进行对比,一致则认证成功(密钥即为注册客户端时指定的密钥clientSecret)。

请求格式

请求方法: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

  • 客户端会使用url编码,向/oauth2/token请求的请求头中添加Authentication Basic信息。

  • 授权服务会从请求头Authorization参数中,取出Basic及其后面的URL编码值,并解码取出密钥部分,跟对应注册客户端信息中的密钥做对比,对比成功则验证通过。

请求格式

请求方法: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方式

对应ClientAuthenticationMethodCLIENT_SECRET_POST

使用此种方式,客户端会直接将密钥放到/oauth2/token请求的请求体中,不经任何加密,授权服务取出密钥,直接与对应已注册客户端信息中的密钥对比,一致则认证成功。

请求格式

请求方法:POST

请求路径:/oauth2/token

请求格式如下:

POST /oauth2/token HTTP/1.1
Host: authorization-server.com
Content-Type: application/x-www-form-urlencodedclient_id=your-client-id&
client_secret=your-client-secret&
grant_type=authorization_code&
code=authorization-code&
redirect_uri=your-redirect-uri

关键点

  • 必要参数
    • client_id:客户端 ID
    • client_secret:客户端密钥
  • 其他参数:可能包括 grant_typecoderedirect_uri 等,如果有的话。

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


PKCE方式

对应ClientAuthenticationMethod 中的 NONE:用于公共客户端认证。

公共客户端通常是在没有客户端密钥的情况下进行认证的,PKCE 的工作原理是通过增加一个动态密钥来防止授权码被劫持。它在 OAuth 2.0 授权码流程的基础上,增加了两个新的参数:code_challengecode_verifier

PKCE大致处理流程

  1. 客户端生成一个随机字符串code_verifier,使用哈希算法(通常是 SHA-256)对code_verifier进行哈希运算,生成code_challenge
  2. 客户端发起授权码请求时,将code_challenge以及其他必要的授权参数(如client_idredirect_uri等)一起发送到授权服务器。
  3. 用户在授权服务器上进行认证时,会保存客户端发来的code_challenge,然后返回授权码给客户端。
  4. 客户端接收到授权码后,将授权码和code_verifier发送到授权服务器,以交换访问令牌。
  5. 授权服务器接收到请求,使用相同的哈希算法对code_verifier进行哈希运算,如果与之前认证时保存的code_challenge一致,则认证成功。

其实就是对比客户端获取授权码之前与之后的code_verifier是否一致,来验证授权码是否被劫持篡改。

转换的请求

请求方法:POST

请求路径:/oauth2/token

请求格式:

POST /oauth2/token HTTP/1.1
Host: authorization-server.com
Content-Type: application/x-www-form-urlencodedclient_id=your-client-id&
code_verifier=your-code-verifier&
grant_type=authorization_code&
code=authorization-code&
redirect_uri=your-redirect-uri

关键点

  • 必要参数
    • client_id:客户端 ID
    • code_verifier:PKCE 流程中的 code_verifier
  • 其他参数:可能包括 grant_typecoderedirect_uri 等。

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



认证过滤器源码解析

OAuth2AuthorizationEndpointFilter过滤器doFilterInternal方法,对客户端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 请求token
  • /oauth2/introspect 获取token的有效信息
  • /oauth2/revoke 撤销token

上面源码的

this.requestMatcher.matches(request)

OAuth2ClientAuthenticationConfigurer初始化时指定匹配规则

@Override
void init(HttpSecurity httpSecurity) {AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(httpSecurity);//规定了匹配的请求this.requestMatcher = new OrRequestMatcher(// 匹配/oauth2/tokennew AntPathRequestMatcher(authorizationServerSettings.getTokenEndpoint(),HttpMethod.POST.name()),// 匹配/oauth2/introspect new AntPathRequestMatcher(authorizationServerSettings.getTokenIntrospectionEndpoint(),HttpMethod.POST.name()),// 匹配/oauth2/revokenew 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进行认证



client_secret_basic认证源码解析

请求转换器

ClientSecretBasicAuthenticationConverter

请求处理流程

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

源码解析

public final class ClientSecretBasicAuthenticationConverter implements AuthenticationConverter {@Nullable@Overridepublic Authentication convert(HttpServletRequest request) {//取出请求头中的Authorization参数值,如果为空返回nullString header = request.getHeader(HttpHeaders.AUTHORIZATION);if (header == null) {return null;}//斜杠小写s正则匹配的是不可见字符,包括空格、制表符、换页符等,//这里就是按空格拆分Authorization参数值String[] parts = header.split("\\s");//如果拆分出来的第一个值在忽略大小写情况下不是Basic,直接结束方法返回null,//从此处看出这个转换器匹配的是请求头Authorization参数值为'Basic ***'、携带未加密用户名密码的if (!parts[0].equalsIgnoreCase("Basic")) {return null;}//拆分完的Authorization参数值如果不是2个,直接抛出invalid_request异常if (parts.length != 2) {throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);}//解析Authorization参数值两段中的第二段,先转为utf-8字节,再用Base64解码,解析失败则抛出invalid_request异常byte[] decodedCredentials;try {decodedCredentials = Base64.getDecoder().decode(parts[1].getBytes(StandardCharsets.UTF_8));} catch (IllegalArgumentException ex) {throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST), ex);}//将解码后的凭证转换为字符串,并按 : 分割成用户名和密码。//检查分割后的数组是否包含用户名和密码两个部分,并且两部分内容都不为空。//如果不满足上面这些条件,抛出 OAuth2AuthenticationException 异常,表示请求无效。String credentialsString = new String(decodedCredentials, StandardCharsets.UTF_8);String[] credentials = credentialsString.split(":", 2);if (credentials.length != 2 ||!StringUtils.hasText(credentials[0]) ||!StringUtils.hasText(credentials[1])) {throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);}//尝试解码用户名和密码部分。如果解码失败,抛出 OAuth2AuthenticationException 异常,表示请求无效。String clientID;String clientSecret;try {clientID = URLDecoder.decode(credentials[0], StandardCharsets.UTF_8.name());clientSecret = URLDecoder.decode(credentials[1], StandardCharsets.UTF_8.name());} catch (Exception ex) {throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST), ex);}//如果解码成功,创建一个新的 OAuth2ClientAuthenticationToken 权限对象,//并将客户端 ID、认证方法(CLIENT_SECRET_BASIC)和客户端密钥作为参数传入。return new OAuth2ClientAuthenticationToken(clientID, ClientAuthenticationMethod.CLIENT_SECRET_BASIC, clientSecret,OAuth2EndpointUtils.getParametersIfMatchesAuthorizationCodeGrantRequest(request));}}

请求验证

ClientSecretAuthenticationProvider

这段代码是ClientSecretAuthenticationProvider类中的authenticate方法,用于处理客户端使用client_secret_basicclient_secret_post方法进行认证的逻辑。以下是逐行解释:

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {// 将传入的Authentication对象转换为OAuth2ClientAuthenticationToken对象OAuth2ClientAuthenticationToken clientAuthentication =(OAuth2ClientAuthenticationToken) authentication;// 检查客户端的认证方法是否为client_secret_basic或client_secret_postif (!ClientAuthenticationMethod.CLIENT_SECRET_BASIC.equals(clientAuthentication.getClientAuthenticationMethod()) &&!ClientAuthenticationMethod.CLIENT_SECRET_POST.equals(clientAuthentication.getClientAuthenticationMethod())) {return null;}// 获取客户端IDString clientId = clientAuthentication.getPrincipal().toString();// 从存储库中查找已注册的客户端信息RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);if (registeredClient == null) {throwInvalidClient(OAuth2ParameterNames.CLIENT_ID);}// 如果启用跟踪日志,则记录已检索到的客户端信息if (this.logger.isTraceEnabled()) {this.logger.trace("Retrieved registered client");}// 检查客户端注册信息中是否包含当前使用的认证方法if (!registeredClient.getClientAuthenticationMethods().contains(clientAuthentication.getClientAuthenticationMethod())) {throwInvalidClient("authentication_method");}// 检查客户端凭据是否为空if (clientAuthentication.getCredentials() == null) {throwInvalidClient("credentials");}// 获取客户端密钥String clientSecret = clientAuthentication.getCredentials().toString();// 验证客户端密钥是否匹配,使用委托模式调用DelegatingPasswordEncoder来进行对比if (!this.passwordEncoder.matches(clientSecret, registeredClient.getClientSecret())) {throwInvalidClient(OAuth2ParameterNames.CLIENT_SECRET);}// 检查客户端密钥是否过期if (registeredClient.getClientSecretExpiresAt() != null &&Instant.now().isAfter(registeredClient.getClientSecretExpiresAt())) {throwInvalidClient("client_secret_expires_at");}// 如果启用跟踪日志,则记录已验证的客户端认证参数if (this.logger.isTraceEnabled()) {this.logger.trace("Validated client authentication parameters");}// 验证保密客户端的“code_verifier”参数(如果可用)this.codeVerifierAuthenticator.authenticateIfAvailable(clientAuthentication, registeredClient);// 如果启用跟踪日志,则记录已认证的客户端密钥if (this.logger.isTraceEnabled()) {this.logger.trace("Authenticated client secret");}// 返回新的OAuth2ClientAuthenticationToken,表示认证成功return new OAuth2ClientAuthenticationToken(registeredClient,clientAuthentication.getClientAuthenticationMethod(), clientAuthentication.getCredentials());
}

源码流程概括

  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 客户端认证。


源码解析

public final class ClientSecretPostAuthenticationConverter implements AuthenticationConverter {@Nullable@Overridepublic Authentication convert(HttpServletRequest request) {MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);// 取出请求中携带的client_id参数值,如果为空返回nullString clientId = parameters.getFirst(OAuth2ParameterNames.CLIENT_ID);if (!StringUtils.hasText(clientId)) {return null;}// client_id参数值只能是1个,否则抛出invalid_request异常if (parameters.get(OAuth2ParameterNames.CLIENT_ID).size() != 1) {throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);}// 取出请求中携带的client_secret参数值,如果为空返回nullString clientSecret = parameters.getFirst(OAuth2ParameterNames.CLIENT_SECRET);if (!StringUtils.hasText(clientSecret)) {return null;}// client_secret参数值只能是1个,否则抛出invalid_request异常if (parameters.get(OAuth2ParameterNames.CLIENT_SECRET).size() != 1) {throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);}// 获取其他请求参数,这些参数必须匹配授权码授权请求的格式,并排除 client_id 和 client_secret 参数Map<String, Object> additionalParameters = OAuth2EndpointUtils.getParametersIfMatchesAuthorizationCodeGrantRequest(request,OAuth2ParameterNames.CLIENT_ID,OAuth2ParameterNames.CLIENT_SECRET);// 创建权限对象并返回,其中包含客户端 ID、认证方法(CLIENT_SECRET_POST)、客户端密钥和额外的参数。return new OAuth2ClientAuthenticationToken(clientId, ClientAuthenticationMethod.CLIENT_SECRET_POST, clientSecret,additionalParameters);}}

认证器

见上面的ClientSecretAuthenticationProvider

client_secret_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

OAuth2AuthorizationCodeRequestAuthenticationConverterconvert方法,将PKCE参数code_challengecode_challenge_method取出并添加到创建的认证对象中:

public final class PublicClientAuthenticationConverter implements AuthenticationConverter {@Nullable@Overridepublic Authentication convert(HttpServletRequest request) {// 请求必须携带code_verifier参数且不为null;// 请求的grant_type参数值必须是'authorization_code',且code参数不能为空。// 即:检查请求是否匹配 PKCE 令牌请求。如果请求不匹配,则返回 null,表示无法进行转换。if (!OAuth2EndpointUtils.matchesPkceTokenRequest(request)) {return null;}//获取请求中的所有参数及其值MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);//获取 client_id 参数,并检查它是否为空。//如果为空或者 client_id 参数的值不唯一,则抛出 invalid_request异常,表示请求无效String clientId = parameters.getFirst(OAuth2ParameterNames.CLIENT_ID);if (!StringUtils.hasText(clientId) ||parameters.get(OAuth2ParameterNames.CLIENT_ID).size() != 1) {throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);}// code_verifier必须不为空且必须只有1个值if (parameters.get(PkceParameterNames.CODE_VERIFIER).size() != 1) {throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);}// 从请求中移除client_id//从参数列表中移除client_id参数目的是为了确保在创建 OAuth2ClientAuthenticationToken 对象时不会包含此参数。parameters.remove(OAuth2ParameterNames.CLIENT_ID);// 创建权限对象并返回,其中包含客户端 ID、认证方法(ClientAuthenticationMethod.NONE)、客户端密钥(此处为 null)和额外的参数return new OAuth2ClientAuthenticationToken(clientId, ClientAuthenticationMethod.NONE, null,new HashMap<>(parameters.toSingleValueMap()));}
}

认证器

认证类:PublicClientAuthenticationProvider

public final class PublicClientAuthenticationProvider implements AuthenticationProvider {@Overridepublic 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) {//获取客户端身份验证请求中的附加参数,并检查该请求是否为授权码类型。如果不是,返回 falseMap<String, Object> parameters = clientAuthentication.getAdditionalParameters();if (!authorizationCodeGrant(parameters)) {return false;}//使用附加参数中的授权码从 authorizationService 查找相应的授权信息。如果找不到,抛出 INVALID_GRANT 错误//这里的authorizationService对应之前在OAuth2AuthorizationCodeRequestAuthenticationProvider中保存的客户端授权记录,里面存有客户端请求授权码时传过来的code_challengeOAuth2Authorization 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,记录日志并返回 falseString 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_verifierString 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_verifierVif (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认证结束。



JWT认证源码解析

转换器

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@Overridepublic Authentication convert(HttpServletRequest request) {//如果请求中取不到client_assertion_type或client_assertion参数,转换方法返回空if (request.getParameter(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE) == null ||request.getParameter(OAuth2ParameterNames.CLIENT_ASSERTION) == null) {return null;}//获取请求中的所有参数,存入mapMultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);// 请求必须带有client_assertion_type参数,且其值只能是一个,否则抛出invalid_request异常String clientAssertionType = parameters.getFirst(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE);if (parameters.get(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE).size() != 1) {throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);}// 请求中client_assertion_type属性的值如果不是'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'就返回nullif (!JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD.getValue().equals(clientAssertionType)) {return null;}// 请求必须带有client_assertion参数,且其值只能是一个,否则抛出invalid_request异常String jwtAssertion = parameters.getFirst(OAuth2ParameterNames.CLIENT_ASSERTION);if (parameters.get(OAuth2ParameterNames.CLIENT_ASSERTION).size() != 1) {throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);}// 如果请求中携带了client_id参数,其值必须是一个,否则抛出invalid_request异常String clientId = parameters.getFirst(OAuth2ParameterNames.CLIENT_ID);if (!StringUtils.hasText(clientId) ||parameters.get(OAuth2ParameterNames.CLIENT_ID).size() != 1) {throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);}// 获取请求中除了client_assertion_type、client_assertion、client_id之外的参数值存入additionalParametersMap<String, Object> additionalParameters = OAuth2EndpointUtils.getParametersIfMatchesAuthorizationCodeGrantRequest(request,OAuth2ParameterNames.CLIENT_ASSERTION_TYPE,OAuth2ParameterNames.CLIENT_ASSERTION,OAuth2ParameterNames.CLIENT_ID);// 结合验证过的请求参数创建权限对象并返回return new OAuth2ClientAuthenticationToken(clientId, JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD,jwtAssertion, additionalParameters);}}

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

OAuth2ClientAuthenticationFilter过滤器的doFilterInternal方法中,如下代码会通过委托模式调用转换器来获取认证对象

Authentication authenticationRequest = this.authenticationConverter.convert(request);

委托模式的实现类DelegatingAuthenticationConverter获取实际转换器并返回认证对象

@Nullable
@Override
public Authentication convert(HttpServletRequest request) {Assert.notNull(request, "request cannot be null");//循环所有的converter实现,那个能转换成功,就返回那个成功的结果for (AuthenticationConverter converter : this.converters) {Authentication authentication = converter.convert(request);if (authentication != null) {return authentication;}}return null;
}

通过上面源码分析,如果请求包含client_assertion_typeclient_assertion参数,则会被JwtClientAssertionAuthenticationConverter转换成功并返回认证对象OAuth2ClientAuthenticationToken,交由Provider进行验证


认证器

认证类:JwtClientAssertionAuthenticationProvider

如下是JwtClientAssertionAuthenticationProviderauthenticate方法。该方法用于验证OAuth 2.0客户端的JWT断言认证(client assertion authentication)。以下是代码的逐行解释:

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {// 将传入的Authentication对象转换为OAuth2ClientAuthenticationToken类型OAuth2ClientAuthenticationToken clientAuthentication = (OAuth2ClientAuthenticationToken) authentication;// 如果客户端的认证方法不是JWT客户端断言认证,则返回nullif (!JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD.equals(clientAuthentication.getClientAuthenticationMethod())) {return null;}// 获取客户端IDString clientId = clientAuthentication.getPrincipal().toString();// 根据客户端ID查找注册的客户端RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);if (registeredClient == null) {// 如果找不到注册的客户端,则抛出异常throwInvalidClient(OAuth2ParameterNames.CLIENT_ID);}// 如果日志级别为trace,则记录日志if (this.logger.isTraceEnabled()) {this.logger.trace("Retrieved registered client");}// 检查客户端是否支持PRIVATE_KEY_JWT或CLIENT_SECRET_JWT认证方法if (!registeredClient.getClientAuthenticationMethods().contains(ClientAuthenticationMethod.PRIVATE_KEY_JWT) &&!registeredClient.getClientAuthenticationMethods().contains(ClientAuthenticationMethod.CLIENT_SECRET_JWT)) {// 如果不支持,则抛出异常throwInvalidClient("authentication_method");}// 检查客户端凭据是否为空if (clientAuthentication.getCredentials() == null) {// 如果为空,则抛出异常throwInvalidClient("credentials");}// 初始化Jwt对象Jwt jwtAssertion = null;// 创建JwtDecoder对象,已通过构造方法指定为JwtClientAssertionDecoderFactoryJwtDecoder jwtDecoder = this.jwtDecoderFactory.createDecoder(registeredClient);try {// 使用JwtDecoder解码客户端凭据jwtAssertion = jwtDecoder.decode(clientAuthentication.getCredentials().toString());} catch (JwtException ex) {// 如果解码失败,则抛出异常throwInvalidClient(OAuth2ParameterNames.CLIENT_ASSERTION, ex);}// 如果日志级别为trace,则记录日志if (this.logger.isTraceEnabled()) {this.logger.trace("Validated client authentication parameters");}// 验证机密客户端的"code_verifier"参数,如果可用this.codeVerifierAuthenticator.authenticateIfAvailable(clientAuthentication, registeredClient);// 确定客户端认证方法ClientAuthenticationMethod clientAuthenticationMethod =registeredClient.getClientSettings().getTokenEndpointAuthenticationSigningAlgorithm() instanceof SignatureAlgorithm ?ClientAuthenticationMethod.PRIVATE_KEY_JWT :ClientAuthenticationMethod.CLIENT_SECRET_JWT;// 如果日志级别为trace,则记录日志if (this.logger.isTraceEnabled()) {this.logger.trace("Authenticated client assertion");}// 返回新的OAuth2ClientAuthenticationToken对象,其中包含已验证的客户端和JWT断言return new OAuth2ClientAuthenticationToken(registeredClient, clientAuthenticationMethod, jwtAssertion);
}

代码功能概述

  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 URLString jwkSetUrl = registeredClient.getClientSettings().getJwkSetUrl();// 如果JWK Set URL为空,则抛出异常if (!StringUtils.hasText(jwkSetUrl)) {OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT,"Failed to find a Signature Verifier for Client: '"+ registeredClient.getId()+ "'. Check to ensure you have configured the JWK Set URL.",JWT_CLIENT_AUTHENTICATION_ERROR_URI);throw new OAuth2AuthenticationException(oauth2Error);}// 使用JWK Set URL和签名算法创建并返回NimbusJwtDecoderreturn NimbusJwtDecoder.withJwkSetUri(jwkSetUrl).jwsAlgorithm((SignatureAlgorithm) jwsAlgorithm).build();}// 如果JWS算法是MAC算法(对称加密)if (jwsAlgorithm instanceof MacAlgorithm) {// 获取客户端密钥String clientSecret = registeredClient.getClientSecret();// 如果客户端密钥为空,则抛出异常if (!StringUtils.hasText(clientSecret)) {OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT,"Failed to find a Signature Verifier for Client: '"+ registeredClient.getId()+ "'. Check to ensure you have configured the client secret.",JWT_CLIENT_AUTHENTICATION_ERROR_URI);throw new OAuth2AuthenticationException(oauth2Error);}// 创建SecretKeySpec,用于对称加密SecretKeySpec secretKeySpec = new SecretKeySpec(clientSecret.getBytes(StandardCharsets.UTF_8),JCA_ALGORITHM_MAPPINGS.get(jwsAlgorithm));// 使用客户端密钥和MAC算法创建并返回NimbusJwtDecoderreturn NimbusJwtDecoder.withSecretKey(secretKeySpec).macAlgorithm((MacAlgorithm) jwsAlgorithm).build();}// 如果JWS算法既不是签名算法也不是MAC算法,则抛出异常OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT,"Failed to find a Signature Verifier for Client: '"+ registeredClient.getId()+ "'. Check to ensure you have configured a valid JWS Algorithm: '" + jwsAlgorithm + "'.",JWT_CLIENT_AUTHENTICATION_ERROR_URI);throw new OAuth2AuthenticationException(oauth2Error);
}

关键点解释

  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解析





token请求过滤器

OAuth2TokenEndpointFilter 是一个过滤器,用于处理 /oauth2/token 端点上的 OAuth2 令牌请求。它的主要作用是验证请求、转换请求为身份验证对象并通过身份验证管理器进行身份验证。

token请求过滤器处理的请求

1.授权码模式获取令牌请求

OAuth 2.0 授权码令牌请求是由客户端发起的,携带授权码向授权服务获取token的请求。请求通常包含以下内容:

  • HTTP 方法:POST

  • 路径:授权服务器的授权端点(默认 /oauth2/token

  • 请求参数:

    • grant_type: 授权模式。
    • code: 授权码的值。
    • redirect_uri: 用户授权后重定向的 URI。
    • client_id: 客户端id。
    • client_secret: 客户端密钥。

请求示例

POST /oauth2/token HTTP/1.1
Host: authorization-server.com
Content-Type: application/x-www-form-urlencodedgrant_type=authorization_code
code=AUTHORIZATION_CODE
redirect_uri=REDIRECT_URI
client_id=CLIENT_ID
client_secret=CLIENT_SECRET

2.令牌刷新请求

OAuth 2.0 授权码令牌刷新请求是由客户端发起的,用于刷新客户端令牌有效期的请求。请求通常包含以下内容:

  • HTTP 方法:POST

  • 路径:授权服务器的授权端点(默认 /oauth2/token

  • 请求参数:

    • grant_type: 授权模式,值固定为refresh_token。
    • refresh_token: 客户端用授权码换取token时,授权服务返回响应中的refresh_token参数值。
    • scope: 权限范围。

请求示例

POST /oauth2/token HTTP/1.1
Host: localhost:9000
Content-Type: application/x-www-form-urlencodedgrant_type=refresh_token
refresh_token=9c27dd65-9a77-4e5c-bd79-efc0f5fa4dbb
scope=read

3.客户端凭证模式令牌请求

在 Spring Security OAuth2 中,client_credentials 授权模式的请求流程与授权码模式有很大的不同,因为 client_credentials 授权模式主要用于服务器到服务器的通信,不涉及用户的浏览器重定向。

Client Credentials 授权模式的请求流程

  1. 客户端请求访问受保护的资源

    • 客户端应用程序直接请求受保护的资源,而不涉及用户的浏览器。
  2. 检查现有的访问令牌

    • 客户端应用程序检查是否已经拥有有效的访问令牌。如果有,则使用该令牌访问资源。
  3. 请求访问令牌

    • 如果客户端没有有效的访问令牌或令牌已过期,则客户端应用程序向授权服务器请求新的访问令牌。请求路径通常是 /oauth2/token
    • 这个请求需要包含客户端的凭证(client_idclient_secret)。
  4. 授权服务器颁发访问令牌

    • 授权服务器验证客户端凭证。如果验证通过,授权服务器会颁发一个新的访问令牌。
  5. 使用访问令牌访问受保护的资源

    • 客户端应用程序使用获得的访问令牌来访问受保护的资源。

请求示例

POST /oauth2/token HTTP/1.1
Content-Type: application/x-www-form-urlencodedgrant_type=client_credentials
scope=read
  • HTTP 方法:POST

  • 路径:授权服务器的授权端点(默认 /oauth2/token

  • 请求参数:

    • grant_type: 授权模式,值为client_credentials,代表使用客户端凭证模式。
    • scope: 权限范围。

过滤器源码解析

OAuth2TokenEndpointFilter令牌请求过滤器中的逻辑实现与OAuth2AuthorizationEndpointFilter授权码请求过滤器的实现思路基本相同,都是:构造方法添加请求匹配器限定请求匹配规则、添加请求转换器限定可以转换的请求,然后根据委托模式依据转换后不同类型的权限对象,来找到具体的验证者Provider进行处理

构造方法

public OAuth2TokenEndpointFilter(AuthenticationManager authenticationManager, String tokenEndpointUri) {Assert.notNull(authenticationManager, "authenticationManager cannot be null");Assert.hasText(tokenEndpointUri, "tokenEndpointUri cannot be empty");this.authenticationManager = authenticationManager;// 匹配`/oauth2/token` POST请求this.tokenEndpointMatcher = new AntPathRequestMatcher(tokenEndpointUri, HttpMethod.POST.name());//添加转换器this.authenticationConverter = new DelegatingAuthenticationConverter(Arrays.asList(//针对携带授权码的请求转换new OAuth2AuthorizationCodeAuthenticationConverter(),//针对刷新token的请求转换new OAuth2RefreshTokenAuthenticationConverter(),//针对客户端凭证模式请求token的请求转换new OAuth2ClientCredentialsAuthenticationConverter()));
}

doFilterInternal方法

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)throws ServletException, IOException {//检查请求的路径是否匹配 `/oauth2/token` POSTif (!this.tokenEndpointMatcher.matches(request)) {filterChain.doFilter(request, response);return;}try {//检查`grant_type`参数,不能为空且只能有一个值,否则抛出`INVALID_REQUEST`错误。String[] grantTypes = request.getParameterValues(OAuth2ParameterNames.GRANT_TYPE);if (grantTypes == null || grantTypes.length != 1) {throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.GRANT_TYPE);}//使用转换器将请求转为权限对象Authentication authorizationGrantAuthentication = this.authenticationConverter.convert(request);if (authorizationGrantAuthentication == null) {throwError(OAuth2ErrorCodes.UNSUPPORTED_GRANT_TYPE, OAuth2ParameterNames.GRANT_TYPE);}//如果转换成功,并且身份验证对象是`AbstractAuthenticationToken`的实例,则设置请求的详细信息。if (authorizationGrantAuthentication instanceof AbstractAuthenticationToken) {((AbstractAuthenticationToken) authorizationGrantAuthentication).setDetails(this.authenticationDetailsSource.buildDetails(request));}//通过`authenticationManager`进行身份验证OAuth2AccessTokenAuthenticationToken accessTokenAuthentication =(OAuth2AccessTokenAuthenticationToken) this.authenticationManager.authenticate(authorizationGrantAuthentication);//进行身份验证成功处理,即生成token并返回给客户端this.authenticationSuccessHandler.onAuthenticationSuccess(request, response, accessTokenAuthentication);} catch (OAuth2AuthenticationException ex) {//如果身份验证失败,捕获 `OAuth2AuthenticationException` 异常,清除安全上下文,//并调用 `authenticationFailureHandler` 处理失败的身份验证结果SecurityContextHolder.clearContext();if (this.logger.isTraceEnabled()) {this.logger.trace(LogMessage.format("Token request failed: %s", ex.getError()), ex);}this.authenticationFailureHandler.onAuthenticationFailure(request, response, ex);}
}

下面讲解用授权码换取token的客户端请求转换过程,以及验证过程的源码解析


授权码验证请求转换源码解析

转换器为OAuth2AuthorizationCodeAuthenticationConverter

OAuth2AuthorizationCodeAuthenticationConverter 是一个实现了 AuthenticationConverter 接口的类,这个转换器主要用于将包含授权码的 OAuth 2.0 令牌请求转换为 OAuth2AuthorizationCodeAuthenticationToken 对象。

它验证请求中的关键参数(如 grant_typecoderedirect_uri),并将这些参数以及附加参数打包到一个认证对象中,以便后续处理流程能够使用。

以下是 OAuth2AuthorizationCodeAuthenticationConverter 的源码解析:

public final class OAuth2AuthorizationCodeAuthenticationConverter implements AuthenticationConverter {@Nullable@Overridepublic Authentication convert(HttpServletRequest request) {// grant_type (REQUIRED)String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);// 检查请求中的 `grant_type` 参数是否为 `authorization_code`,如果不是则返回 `null`if (!AuthorizationGrantType.AUTHORIZATION_CODE.getValue().equals(grantType)) {return null;}// 从上下文获取认证信息(客户端认证)Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();// 获取所有请求参数存入mapMultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);// code (REQUIRED)String code = parameters.getFirst(OAuth2ParameterNames.CODE);// 检查 `code` 参数是否存在且仅有一个值,如果不符合条件则抛出异常if (!StringUtils.hasText(code) ||parameters.get(OAuth2ParameterNames.CODE).size() != 1) {OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST,OAuth2ParameterNames.CODE,OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);}// 检查 `redirect_uri` 参数是否存在且仅有一个值,只有当 `redirect_uri` 在请求中存在时才进行检查String redirectUri = parameters.getFirst(OAuth2ParameterNames.REDIRECT_URI);if (StringUtils.hasText(redirectUri) &&parameters.get(OAuth2ParameterNames.REDIRECT_URI).size() != 1) {OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST,OAuth2ParameterNames.REDIRECT_URI,OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);}// 处理附加参数//将其他的请求参数(除了grant_type、client_id、code和redirect_uri)存储在map中。Map<String, Object> additionalParameters = new HashMap<>();parameters.forEach((key, value) -> {if (!key.equals(OAuth2ParameterNames.GRANT_TYPE) &&!key.equals(OAuth2ParameterNames.CLIENT_ID) &&!key.equals(OAuth2ParameterNames.CODE) &&!key.equals(OAuth2ParameterNames.REDIRECT_URI)) {additionalParameters.put(key, value.get(0));}});// 创建并返回一个包含授权码、客户端认证信息、重定向 URI 和附加参数的对象回过滤器return new OAuth2AuthorizationCodeAuthenticationToken(code, clientPrincipal, redirectUri, additionalParameters);}}

授权码验证请求认证源码解析

OAuth2AuthorizationCodeAuthenticationProvider会对OAuth2AuthorizationCodeAuthenticationConverter转换过来的对象进行认证

下面是OAuth2AuthorizationCodeAuthenticationProvider的源码解释(org.springframework.security.oauth2.server.authorization.authentication包下),针对OAuth2AuthorizationCodeAuthenticationProviderauthenticate方法,分段描述其大致认证流程和token生成过程。

认证流程

进入OAuth2AuthorizationCodeAuthenticationProviderauthenticate方法进行认证,以下是其中的核心源码解析

  1. 转换认证对象:

    OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthentication =(OAuth2AuthorizationCodeAuthenticationToken) authentication;
    

    将传入的authentication对象转换为OAuth2AuthorizationCodeAuthenticationToken类型。

  2. 获取已认证的客户端:

    OAuth2ClientAuthenticationToken clientPrincipal =getAuthenticatedClientElseThrowInvalidClient(authorizationCodeAuthentication);
    RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
    

    调用getAuthenticatedClientElseThrowInvalidClient方法,从转换后的请求对象中获取已认证的客户端信息,存入RegisteredClient对象。

  3. 验证授权码:

    OAuth2Authorization authorization = this.authorizationService.findByToken(authorizationCodeAuthentication.getCode(), AUTHORIZATION_CODE_TOKEN_TYPE);
    if (authorization == null) {throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
    }
    

    使用请求对象中携带的授权码,从authorizationService中获取对应的OAuth2Authorization对象,如果找不到则抛出INVALID_GRANT异常。如果使用JDBC实现,这里会去oauth2_authorization表中进行查询。

    这里实际就是去验证请求中的授权码是否在授权服务中存在,即授权服务是否生成过这个授权码

  4. 验证授权请求和客户端信息:

    // 从授权服务的授权记录authorizationService中获取的授权码code
    OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode =authorization.getToken(OAuth2AuthorizationCode.class);//将 从授权服务的授权记录authorizationService中获取的授权数据 转为authorizationRequest请求对象
    OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(OAuth2AuthorizationRequest.class.getName());//对比请求的ID和授权记录的客户端ID是否匹配
    if (!registeredClient.getClientId().equals(authorizationRequest.getClientId())) {//如果授权记录中的授权码已被其他客户端使用,则将其无效,并抛出异常if (!authorizationCode.isInvalidated()) {    authorization = OAuth2AuthenticationProviderUtils.invalidate(authorization, authorizationCode.getToken());this.authorizationService.save(authorization);}throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
    }//对比请求和授权记录客户端的重定向URI是否匹配
    if (StringUtils.hasText(authorizationRequest.getRedirectUri()) &&				!authorizationRequest.getRedirectUri().equals(authorizationCodeAuthentication.getRedirectUri())) {throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
    }//检查授权记录中的授权码是否仍然有效
    if (!authorizationCode.isActive()) {throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
    }
    

    验证请求客户端ID和授权记录的客户端ID是否匹配,以及重定向URI是否匹配,授权码是否仍然有效,否则抛出INVALID_GRANT异常。

  5. 构建Token上下文:

    tokenContext 是一个封装了生成令牌所需的上下文信息的对象。在OAuth2流程中,令牌生成需要了解当前的客户端、用户、授权信息、授权范围等信息。DefaultOAuth2TokenContext 是一个用于构建这些上下文信息的类。

    DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()//当前的客户端信息 .registeredClient(registeredClient)  //当前的用户信息  .principal(authorization.getAttribute(Principal.class.getName()))//授权服务器的上下文信息.authorizationServerContext(AuthorizationServerContextHolder.getContext())  //当前的授权信息.authorization(authorization)   //授权的范围(scopes),即用户授权的访问权限。.authorizedScopes(authorization.getAuthorizedScopes())   //指定授权类型为授权码.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) //当前的授权码认证请求.authorizationGrant(authorizationCodeAuthentication);//创建一个授权构建器,后面会通过令牌、刷新令牌、id令牌等创建一个授权对象,保存到授权服务的授权记录(OAuth2AuthorizationService)中
    OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.from(authorization);
    

    registeredClient:

    • registeredClient 表示当前的客户端信息,通过clientPrincipal.getRegisteredClient()获取。它包含了客户端ID、授权类型等信息。

    principal:

    • authorization.getAttribute(Principal.class.getName()) 获取当前的用户信息(Principal)。这是一个表示认证用户的对象。

    authorizationServerContext:

    • AuthorizationServerContextHolder.getContext() 获取授权服务器的上下文信息,包括服务器的配置和状态。

    authorization:

    • authorization 表示当前的授权信息,通过之前检索到的OAuth2Authorization对象获得。它包含了授权码、用户授权的范围等信息。

    authorizedScopes:

    • authorization.getAuthorizedScopes() 获取授权的范围(scopes),即用户授权的访问权限。

    authorizationGrantType:

    • AuthorizationGrantType.AUTHORIZATION_CODE 指定授权类型为授权码(Authorization Code)。

    authorizationGrant:

    • authorizationCodeAuthentication 表示当前的授权码认证请求。

    以上这些信息被组合在一起,用于生成访问令牌。


Token生成过程

生成访问令牌:

//构建token上下文
OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build();
//生成token
OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);if (generatedAccessToken == null) {OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,"The token generator failed to generate the access token.", ERROR_URI);throw new OAuth2AuthenticationException(error);
}//创建访问令牌对象
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());//存储令牌
if (generatedAccessToken instanceof ClaimAccessor) {authorizationBuilder.token(accessToken, (metadata) ->metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims()));} else {authorizationBuilder.accessToken(accessToken);}

构建token上下文:

  • 使用tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build() 构建tokenContext,指定要生成的令牌类型为访问令牌(ACCESS_TOKEN)。

生成token:

  • this.tokenGenerator.generate(tokenContext) 使用 tokenGenerator 根据上下文信息生成访问令牌。可通过OAuth2AuthorizationServerConfigurer进行配置使用不同的token生成策略,默认使用DelegatingOAuth2TokenGenerator 来处理令牌的生成。这里利用委托模式生成access_token。(关于委托模式的令牌生成,文章后面介绍)
  • tokenGenerator 是一个负责生成不同类型令牌的组件,可能是一个实现了OAuth2TokenGenerator接口的类。
  • 如果生成失败,则抛出OAuth2AuthenticationException异常。

创建访问令牌对象:

  • 使用生成的generatedAccessToken的值创建一个OAuth2AccessToken对象。
  • OAuth2AccessToken 包含了令牌类型(TokenType.BEARER)、令牌值、签发时间、过期时间以及授权范围。

存储令牌:

  • 如果generatedAccessToken实现了ClaimAccessor接口,则将令牌的声明(claims)存储在授权信息中。
  • 否则,直接将访问令牌存储在authorizationBuilder授权构建器中。

生成刷新令牌:

OAuth2RefreshToken refreshToken = null;//检查客户端是否包含 REFRESH_TOKEN 授权类型。只有在客户端配置了 REFRESH_TOKEN 授权类型时才会生成刷新令牌。
//检查客户端认证方法是否不是 NONE。公共客户端(没有认证方法)不会收到刷新令牌
if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN) &&    !clientPrincipal.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE)) 
{//通过上面两个检查后,使用 tokenContextBuilder 构建一个新的 TokenContext 实例,并设置 tokenType 为 REFRESH_TOKEN。	//TokenContext 是一个上下文对象,包含生成令牌所需的所有信息。tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build();//默认情况下,使用委托模式生成刷新令牌OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext);//检查 generatedRefreshToken 是否是 OAuth2RefreshToken 实例。如果不是,抛出 OAuth2AuthenticationException 异常。if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) {OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,"The token generator failed to generate the refresh token.", ERROR_URI);throw new OAuth2AuthenticationException(error);}//如果日志记录级别设置为 TRACE,记录生成刷新令牌的日志。if (this.logger.isTraceEnabled()) {this.logger.trace("Generated refresh token");}//将生成的 generatedRefreshToken 转换为 OAuth2RefreshToken。refreshToken = (OAuth2RefreshToken) generatedRefreshToken;//使用授权构建器authorizationBuilder将刷新令牌添加到 OAuth2Authorization 对象中。//默认情况下,此时authorizationBuilder已包含`access_token`和`refresh_token`,所以这两个都会返回authorizationBuilder.refreshToken(refreshToken);
}

生成ID令牌(如果需要):

idToken主要用与客户端验证用户的身份,大致概括:

  1. 授权请求

    • 客户端向授权服务器发起包含 openid scope 的授权请求。
    • 用户通过授权服务器认证并同意授权。
  2. 授权码交换

    • 授权服务器返回授权码给客户端。
    • 客户端使用授权码向授权服务器请求访问令牌和 ID Token。
  3. 身份验证和用户信息获取

    • 客户端使用 ID Token 验证用户身份。
    • 客户端可以使用访问令牌访问用户信息端点获取更多用户信息。
  4. 资源访问

    • 客户端使用访问令牌访问资源服务器上的受保护资源。
    • 资源服务器验证访问令牌的有效性,并允许或拒绝访问。

源码分析:

OidcIdToken idToken;
//检查授权请求的范围是否包含 openid。如果包含,表示客户端请求了 OpenID Connect (OIDC) 的功能,需要生成 ID Token。
if (authorizationRequest.getScopes().contains(OidcScopes.OPENID)) {//使用 tokenContextBuilder 构建一个新的 TokenContext上下文 实例,并设置类型为 ID_TOKEN。//将 authorization 对象构建并传入上下文。这一步使得 ID Token 的生成器可以访问授权信息(包括访问令牌和刷新令牌,如果已经生成)。//调用 build() 方法生成 TokenContext 对象。tokenContext = tokenContextBuilder.tokenType(ID_TOKEN_TOKEN_TYPE).authorization(authorizationBuilder.build()).build();//在默认实现中,使用 DelegatingOAuth2TokenGenerator 来委派具体的令牌生成任务。这个生成器会根据 TokenContext 中的 tokenType 来选择适当的令牌生成器,这里默认使用JwtGenerator OAuth2Token generatedIdToken = this.tokenGenerator.generate(tokenContext);//检查 generatedIdToken 是否是 Jwt 实例。ID Token 通常是 JWT 格式的。如果生成的令牌不是 Jwt 类型,抛出 OAuth2AuthenticationException 异常。if (!(generatedIdToken instanceof Jwt)) {OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,"The token generator failed to generate the ID token.", ERROR_URI);throw new OAuth2AuthenticationException(error);}//如果日志记录级别设置为 TRACE,记录生成 ID Token 的日志。if (this.logger.isTraceEnabled()) {this.logger.trace("Generated id token");}//使用生成的 Jwt 令牌创建 OidcIdToken 对象,设置令牌的值、签发时间、过期时间和声明(claims)。idToken = new OidcIdToken(generatedIdToken.getTokenValue(), generatedIdToken.getIssuedAt(),generatedIdToken.getExpiresAt(), ((Jwt) generatedIdToken).getClaims());//将生成的 OidcIdToken 添加到 authorizationBuilder 中,并附加声明(claims)元数据。authorizationBuilder.token(idToken, (metadata) ->metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, idToken.getClaims()));
} else {//如果授权请求的范围不包含 openid,则不生成 ID Token,将 idToken 设置为 null。idToken = null;
}

最后步骤

  1. 无效化授权码:

    保证授权码只能用一次

    authorization = OAuth2AuthenticationProviderUtils.invalidate(authorization, authorizationCode.getToken());
    this.authorizationService.save(authorization);
    
  2. 返回认证结果:

    Map<String, Object> additionalParameters = Collections.emptyMap();
    if (idToken != null) {additionalParameters = new HashMap<>();additionalParameters.put(OidcParameterNames.ID_TOKEN, idToken.getTokenValue());
    }
    return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken, additionalParameters);
    

通过上述步骤,OAuth2的认证过程完成,并生成相应的访问令牌、刷新令牌和ID令牌(如果需要),最后将这些信息打包成OAuth2AccessTokenAuthenticationToken对象返回。



令牌验证与撤销

/oauth2/introspect令牌验证请求

/oauth2/introspect 是 OAuth 2.0 中定义的一个端点,用于客户端或资源服务器验证和获取关于访问令牌(Access Token)或刷新令牌(Refresh Token)的元数据信息。

/oauth2/introspect 端点是 OAuth 2.0 Token Introspection 规范的一部分。该端点允许资源服务器或其他信任方查询授权服务器,以确定令牌的状态和其他相关信息。这个请求主要用于验证一个令牌是否有效,并获取关于该令牌的附加信息,例如发行时间、到期时间、作用域等。

/oauth2/introspect 端点提供了一种机制,使资源服务器能够验证和获取访问令牌的详细信息。这对于分布式系统中令牌的验证和管理尤为重要,确保只有有效且授权的令牌能够访问受保护的资源。

请求示例

一个典型的 Token Introspection 请求是通过 POST 方法发送的,包含一个令牌:

POST /oauth2/introspect HTTP/1.1
Host: authorization-server.com
Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64(client_id:client_secret)token=access_token,就是token的值

在这个请求中:

  • token 是要检查的访问令牌或刷新令牌。

  • Authorization 头部包含客户端凭证,用于认证客户端身份。

响应示例

授权服务器会返回一个包含令牌状态和其他元数据的 JSON 响应:

{"active": true,"client_id": "client_id","username": "user","scope": "read write","sub": "subject","aud": "audience","iss": "issuer","exp": 1560234158,"iat": 1560230558,"nbf": 1560230558
}

在这个响应中:

  • active 表示令牌是否有效。

  • client_id 是令牌关联的客户端 ID。

  • username 是与令牌关联的资源所有者(用户)。

  • scope 是令牌的作用域。

  • exp 是令牌的过期时间。

  • 其他字段提供关于令牌的附加信息。

与令牌之间的关联

/oauth2/introspect 端点主要用于以下场景:

  1. 资源服务器验证令牌
    资源服务器接收到客户端的请求,携带一个访问令牌。资源服务器使用 /oauth2/introspect 端点来验证该令牌是否有效,以及获取与该令牌相关的信息。这有助于资源服务器确定是否允许访问资源。

  2. 获取令牌元数据
    客户端或资源服务器可以通过 introspect 端点获取令牌的详细信息,如令牌的有效期、作用域、关联的用户等。这样可以根据这些信息做出相应的访问控制决策。

工作流程示例

  1. 客户端获取访问令牌
    客户端通过授权流程(例如授权码流程、客户端凭证流程等)从授权服务器获取访问令牌。

  2. 客户端请求资源服务器
    客户端使用该访问令牌向资源服务器请求受保护资源。

  3. 资源服务器验证令牌
    资源服务器接收到请求后,使用 /oauth2/introspect 端点验证令牌的有效性和获取元数据。

  4. 返回结果
    如果令牌有效且权限足够,资源服务器返回受保护资源给客户端;否则,返回相应的错误信息。


源码解析

OAuth2TokenIntrospectionEndpointFilter 是一个 Spring Security 过滤器,用于处理 /oauth2/introspect 请求。其主要功能是验证和解析客户端发送的令牌,并返回相应的元数据信息或错误响应。

其运行流程与前面介绍的过滤器一致,先将请求转为权限对象,在根据权限对象进行验证,最后返回元数据信息结果。下面是对该过滤器处理流程的详细讲解:

过滤器类定义

过滤器类继承自 OncePerRequestFilter,确保在每个请求中只执行一次过滤操作。

public final class OAuth2TokenIntrospectionEndpointFilter extends OncePerRequestFilter {

成员变量

private static final String DEFAULT_TOKEN_INTROSPECTION_ENDPOINT_URI = "/oauth2/introspect";private final AuthenticationManager authenticationManager;
private final RequestMatcher tokenIntrospectionEndpointMatcher;
private AuthenticationConverter authenticationConverter;
private final HttpMessageConverter<OAuth2TokenIntrospection> tokenIntrospectionHttpResponseConverter =new OAuth2TokenIntrospectionHttpMessageConverter();
private final HttpMessageConverter<OAuth2Error> errorHttpResponseConverter = new OAuth2ErrorHttpMessageConverter();
private AuthenticationSuccessHandler authenticationSuccessHandler = this::sendIntrospectionResponse;
private AuthenticationFailureHandler authenticationFailureHandler = this::sendErrorResponse;
  1. authenticationManager:用于处理认证请求的管理器。
  2. tokenIntrospectionEndpointMatcher:用于匹配 introspection 请求的路径和方法(默认匹配 /oauth2/introspect 和 POST 请求)。
  3. authenticationConverter:将 HTTP 请求转换为 Authentication 对象的转换器。
  4. tokenIntrospectionHttpResponseConverter:将 OAuth2TokenIntrospection 对象转换为 HTTP 响应的消息转换器。
  5. errorHttpResponseConverter:将 OAuth2Error 对象转换为 HTTP 错误响应的消息转换器。
  6. authenticationSuccessHandlerauthenticationFailureHandler:处理认证成功和失败的处理器。

构造方法

public OAuth2TokenIntrospectionEndpointFilter(AuthenticationManager authenticationManager) {this(authenticationManager, DEFAULT_TOKEN_INTROSPECTION_ENDPOINT_URI);
}public OAuth2TokenIntrospectionEndpointFilter(AuthenticationManager authenticationManager,String tokenIntrospectionEndpointUri) {Assert.notNull(authenticationManager, "authenticationManager cannot be null");Assert.hasText(tokenIntrospectionEndpointUri, "tokenIntrospectionEndpointUri cannot be empty");this.authenticationManager = authenticationManager;this.tokenIntrospectionEndpointMatcher = new AntPathRequestMatcher(tokenIntrospectionEndpointUri, HttpMethod.POST.name());this.authenticationConverter = new OAuth2TokenIntrospectionAuthenticationConverter();
}

构造方法初始化 authenticationManagertokenIntrospectionEndpointMatcher,并设置默认的 authenticationConverter

doFilterInternal 方法

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)throws ServletException, IOException {if (!this.tokenIntrospectionEndpointMatcher.matches(request)) {filterChain.doFilter(request, response);return;}try {Authentication tokenIntrospectionAuthentication = this.authenticationConverter.convert(request);Authentication tokenIntrospectionAuthenticationResult =this.authenticationManager.authenticate(tokenIntrospectionAuthentication);this.authenticationSuccessHandler.onAuthenticationSuccess(request, response, tokenIntrospectionAuthenticationResult);} catch (OAuth2AuthenticationException ex) {SecurityContextHolder.clearContext();if (this.logger.isTraceEnabled()) {this.logger.trace(LogMessage.format("Token introspection request failed: %s", ex.getError()), ex);}this.authenticationFailureHandler.onAuthenticationFailure(request, response, ex);}
}

处理流程:

  1. 请求匹配

    • 使用 tokenIntrospectionEndpointMatcher 检查请求是否匹配 /oauth2/introspect 和 POST 方法。
    • 如果不匹配,调用 filterChain.doFilter 继续处理请求链。
  2. 请求转换

    • 使用 authenticationConverter 将请求转换为 Authentication 对象(即 OAuth2TokenIntrospectionAuthenticationToken)。
  3. 令牌认证

    • 使用 authenticationManager 对转换后的 Authentication 对象进行认证。
    • 如果认证成功,调用 authenticationSuccessHandler.onAuthenticationSuccess 方法处理成功响应。
    • 如果认证失败,捕获 OAuth2AuthenticationException 异常,清除安全上下文,并调用 authenticationFailureHandler.onAuthenticationFailure 方法处理错误响应。

在构造方法中,指定了转换器OAuth2TokenIntrospectionAuthenticationConverter,因为只有一个转换器,所以对应进行验证的权限提供者也只有一个,即OAuth2TokenIntrospectionAuthenticationProvider,下面是其验证方法源码及验证过程讲解:

public Authentication authenticate(Authentication authentication) throws AuthenticationException {//首先,将传入的 Authentication 对象转换为 OAuth2TokenIntrospectionAuthenticationToken。OAuth2TokenIntrospectionAuthenticationToken tokenIntrospectionAuthentication =(OAuth2TokenIntrospectionAuthenticationToken) authentication;//然后,通过 getAuthenticatedClientElseThrowInvalidClient 方法获取经过认证的客户端身份信息。	OAuth2ClientAuthenticationToken clientPrincipal =getAuthenticatedClientElseThrowInvalidClient(tokenIntrospectionAuthentication);//使用 authorizationService 查找与令牌对应的 OAuth2Authorization 对象。如果未找到对应的授权信息,记录日志并返回原始的 tokenIntrospectionAuthentication 对象。	OAuth2Authorization authorization = this.authorizationService.findByToken(tokenIntrospectionAuthentication.getToken(), null);	if (authorization == null) {	if (this.logger.isTraceEnabled()) {this.logger.trace("Did not authenticate token introspection request since token was not found");}return tokenIntrospectionAuthentication;}if (this.logger.isTraceEnabled()) {this.logger.trace("Retrieved authorization with token");}//检查令牌是否处于活跃状态。如果令牌不活跃,记录日志并返回一个包含空元数据的 OAuth2TokenIntrospectionAuthenticationToken 对象。	OAuth2Authorization.Token<OAuth2Token> authorizedToken =authorization.getToken(tokenIntrospectionAuthentication.getToken());if (!authorizedToken.isActive()) {if (this.logger.isTraceEnabled()) {this.logger.trace("Did not introspect token since not active");}return new OAuth2TokenIntrospectionAuthenticationToken(tokenIntrospectionAuthentication.getToken(),clientPrincipal, OAuth2TokenIntrospection.builder().build());}//查找与授权信息对应的注册客户端。	RegisteredClient authorizedClient = this.registeredClientRepository.findById(authorization.getRegisteredClientId());//使用 withActiveTokenClaims 方法构建包含令牌元数据信息的 OAuth2TokenIntrospection 对象。OAuth2TokenIntrospection tokenClaims = withActiveTokenClaims(authorizedToken, authorizedClient);//记录日志,表明令牌内省请求已通过认证。if (this.logger.isTraceEnabled()) {this.logger.trace("Authenticated token introspection request");}//返回一个包含令牌值、客户端身份信息和令牌元数据信息的 OAuth2TokenIntrospectionAuthenticationToken 对象。	return new OAuth2TokenIntrospectionAuthenticationToken(authorizedToken.getToken().getTokenValue(),clientPrincipal, tokenClaims);
}

OAuth2TokenIntrospectionAuthenticationProvider 的验证过程包括以下步骤:

  1. 将传入的 Authentication 对象转换为 OAuth2TokenIntrospectionAuthenticationToken
  2. 获取经过认证的客户端身份信息。
  3. 使用令牌查找对应的授权信息。
  4. 检查令牌是否处于活跃状态。
  5. 构建令牌元数据信息。
  6. 记录日志并返回包含验证结果的 OAuth2TokenIntrospectionAuthenticationToken 对象。

通过这些步骤,该提供者确保了对令牌内省请求的正确处理,返回相应的元数据信息或错误响应。

处理认证成功响应

private void sendIntrospectionResponse(HttpServletRequest request, HttpServletResponse response,Authentication authentication) throws IOException {OAuth2TokenIntrospectionAuthenticationToken tokenIntrospectionAuthentication =(OAuth2TokenIntrospectionAuthenticationToken) authentication;OAuth2TokenIntrospection tokenClaims = tokenIntrospectionAuthentication.getTokenClaims();ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);this.tokenIntrospectionHttpResponseConverter.write(tokenClaims, null, httpResponse);
}

处理流程:

  1. Authentication 对象转换为 OAuth2TokenIntrospectionAuthenticationToken
  2. 获取 tokenClaims,即令牌的元数据信息。
  3. 使用 tokenIntrospectionHttpResponseConvertertokenClaims 写入 HTTP 响应。

处理认证失败响应

private void sendErrorResponse(HttpServletRequest request, HttpServletResponse response,AuthenticationException exception) throws IOException {OAuth2Error error = ((OAuth2AuthenticationException) exception).getError();ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);httpResponse.setStatusCode(HttpStatus.BAD_REQUEST);this.errorHttpResponseConverter.write(error, null, httpResponse);
}

处理流程

  1. AuthenticationException 中获取 OAuth2Error 对象。
  2. 设置 HTTP 响应状态为 BAD_REQUEST(400)。
  3. 使用 errorHttpResponseConverterOAuth2Error 写入 HTTP 响应。


/oauth2/revoke撤销令牌请求

请求作用

在 OAuth 2.0 安全框架中,/oauth2/revoke 是一个用于撤销(revoke)访问令牌或刷新令牌的请求端点。其主要用途是允许客户端或资源所有者明确地使某个令牌失效,从而使其不再能被使用访问受保护的资源。

/oauth2/revoke 请求是 OAuth 2.0 中的重要端点,用于安全撤销访问令牌和刷新令牌。通过它,客户端可以确保在不需要时安全地使令牌失效,提升整个系统的安全性。

主要用途

  • 提升安全性:当访问令牌或刷新令牌泄露时,能够立即使其失效,从而防止未经授权的访问。

  • 终止会话:当用户在某个客户端应用上登出时,可以使用撤销端点使当前令牌失效,确保用户会话的终止。

  • 减少权限:当客户端应用不再需要某个令牌的权限时,可以主动撤销它,减少潜在的滥用风险。

请求和响应格式

请求

/oauth2/revoke 端点通常通过 POST 请求进行访问。请求的主要参数包括:

  • token: 必需参数,要被撤销的访问令牌或刷新令牌。
  • token_type_hint: 可选参数,指示令牌的类型(如 access_tokenrefresh_token),帮助服务器更快地查找到令牌。

示例请求:

POST /oauth2/revoke HTTP/1.1
Host: authorization-server.com
Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64(client_id:client_secret)token=example_token&token_type_hint=access_token

响应

成功的撤销请求通常返回 200 状态码,并且没有响应体。即使提供了无效或不存在的令牌,通常也会返回 200 状态码,以避免泄露令牌信息。

撤销流程

  1. 验证客户端:授权服务器首先验证发起撤销请求的客户端的身份。通常需要客户端凭据(如 client_id 和 client_secret)。
  2. 查找令牌:服务器查找请求中指定的令牌。如果提供了 token_type_hint,会使用该提示来加速查找。
  3. 撤销令牌:如果找到该令牌,服务器会将其标记为已撤销。对于访问令牌,这通常意味着删除或标记令牌为无效;对于刷新令牌,可能涉及删除相关的所有访问令牌。
  4. 响应请求:服务器返回 200 状态码,指示撤销请求已被处理。

安全性注意事项

  • 客户端验证:确保只有授权客户端能够撤销其发行的令牌。
  • 速率限制:防止滥用撤销端点进行拒绝服务(DoS)攻击。
  • 最小权限:尽量限制令牌的权限和有效期,以减少被滥用的风险。

源码解析

/oauth2/revoke 请求是由OAuth2TokenRevocationEndpointFilter过滤器进行处理的

OAuth2TokenRevocationEndpointFilter 过滤器的主要作用是处理 /oauth2/revoke 请求,验证请求的有效性,并根据认证结果返回适当的响应。通过匹配请求路径、转换请求、进行认证、处理认证结果,这个过滤器确保了令牌撤销操作的安全性和有效性。

OAuth2TokenRevocationEndpointFilter 是一个用于处理 OAuth 2.0 令牌撤销请求的过滤器。它负责从 /oauth2/revoke 端点接收撤销令牌的请求,验证请求,并处理撤销操作。以下是这个过滤器的原理和请求处理流程的详细讲解。

原理
  1. 匹配请求:过滤器首先会检查请求是否匹配预期的撤销令牌端点 /oauth2/revoke

  2. 转换请求:将 HTTP 请求转换为 Authentication 对象,以便进行认证处理。

  3. 认证请求:通过 AuthenticationManager 进行认证。

  4. 处理结果:根据认证结果,调用相应的成功或失败处理器。

请求处理流程

初始化

过滤器在初始化时设置了以下几个关键组件:

  • authenticationManager:处理认证的核心组件。
  • tokenRevocationEndpointMatcher:用于匹配撤销令牌的请求路径和方法。
  • authenticationConverter:将 HTTP 请求转换为 Authentication 对象的转换器。
  • authenticationSuccessHandlerauthenticationFailureHandler:分别处理成功和失败的认证结果。
public OAuth2TokenRevocationEndpointFilter(AuthenticationManager authenticationManager) {this(authenticationManager, DEFAULT_TOKEN_REVOCATION_ENDPOINT_URI);
}public OAuth2TokenRevocationEndpointFilter(AuthenticationManager authenticationManager, String tokenRevocationEndpointUri) {Assert.notNull(authenticationManager, "authenticationManager cannot be null");Assert.hasText(tokenRevocationEndpointUri, "tokenRevocationEndpointUri cannot be empty");this.authenticationManager = authenticationManager;this.tokenRevocationEndpointMatcher = new AntPathRequestMatcher(tokenRevocationEndpointUri, HttpMethod.POST.name());this.authenticationConverter = new OAuth2TokenRevocationAuthenticationConverter();
}

处理请求

doFilterInternal 方法是过滤器处理请求的核心部分:

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)throws ServletException, IOException {// 检查请求是否匹配撤销令牌端点if (!this.tokenRevocationEndpointMatcher.matches(request)) {filterChain.doFilter(request, response);return;}try {// 转换请求为 Authentication 对象Authentication tokenRevocationAuthentication = this.authenticationConverter.convert(request);// 进行认证处理Authentication tokenRevocationAuthenticationResult = this.authenticationManager.authenticate(tokenRevocationAuthentication);// 认证成功处理this.authenticationSuccessHandler.onAuthenticationSuccess(request, response, tokenRevocationAuthenticationResult);} catch (OAuth2AuthenticationException ex) {SecurityContextHolder.clearContext();if (this.logger.isTraceEnabled()) {this.logger.trace(LogMessage.format("Token revocation request failed: %s", ex.getError()), ex);}// 认证失败处理this.authenticationFailureHandler.onAuthenticationFailure(request, response, ex);}
}

检查请求匹配

首先检查请求是否匹配撤销令牌端点。如果不匹配,继续过滤链:

if (!this.tokenRevocationEndpointMatcher.matches(request)) {filterChain.doFilter(request, response);return;
}

转换请求为 Authentication 对象

使用 authenticationConverter 将 HTTP 请求转换为 Authentication 对象:

Authentication tokenRevocationAuthentication = this.authenticationConverter.convert(request);

进行认证处理

调用 authenticationManager 进行认证:

Authentication tokenRevocationAuthenticationResult = this.authenticationManager.authenticate(tokenRevocationAuthentication);

因为此过滤器中只定义了一个转换器OAuth2TokenRevocationAuthenticationConverter,所以验证其转换后的权限对象的是OAuth2TokenRevocationAuthenticationProvider,下面是其验证方法的源码解释:

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {//首先,将传入的 Authentication 对象转换为 OAuth2TokenRevocationAuthenticationToken 对象OAuth2TokenRevocationAuthenticationToken tokenRevocationAuthentication =(OAuth2TokenRevocationAuthenticationToken) authentication;//通过 getAuthenticatedClientElseThrowInvalidClient 方法获取认证的客户端//该方法会确保客户端是合法且已经通过认证的。如果客户端不合法,则会抛出 OAuth2AuthenticationException 异常。OAuth2ClientAuthenticationToken clientPrincipal =getAuthenticatedClientElseThrowInvalidClient(tokenRevocationAuthentication);RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();//通过令牌查找对应的 OAuth2Authorization 对象OAuth2Authorization authorization = this.authorizationService.findByToken(tokenRevocationAuthentication.getToken(), null);//如果找不到对应的授权信息,直接返回 tokenRevocationAuthentication 对象。if (authorization == null) {if (this.logger.isTraceEnabled()) {this.logger.trace("Did not authenticate token revocation request since token was not found");}return tokenRevocationAuthentication;}//检查客户端 ID 是否与授权信息中的客户端 ID 匹配,如果不匹配,抛出 OAuth2AuthenticationException 异常,表示客户端无效if (!registeredClient.getId().equals(authorization.getRegisteredClientId())) {throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);}//获取授权信息中的令牌,并将其设置为无效OAuth2Authorization.Token<OAuth2Token> token = authorization.getToken(tokenRevocationAuthentication.getToken());//invalidate方法会将授权信息中的令牌设置为无效状态,并保存修改后的授权信息。authorization = OAuth2AuthenticationProviderUtils.invalidate(authorization, token.getToken());this.authorizationService.save(authorization);//记录日志信息,表明撤销令牌操作已经成功执行if (this.logger.isTraceEnabled()) {this.logger.trace("Saved authorization with revoked token");this.logger.trace("Authenticated token revocation request");}//最后,创建一个新的 OAuth2TokenRevocationAuthenticationToken 对象,并将其返回return new OAuth2TokenRevocationAuthenticationToken(token.getToken(), clientPrincipal);
}

处理认证结果

根据认证结果调用相应的成功或失败处理器:

this.authenticationSuccessHandler.onAuthenticationSuccess(request, response, tokenRevocationAuthenticationResult);

如果认证失败,捕获 OAuth2AuthenticationException 异常,清除安全上下文,并调用失败处理器:

} catch (OAuth2AuthenticationException ex) {SecurityContextHolder.clearContext();if (this.logger.isTraceEnabled()) {this.logger.trace(LogMessage.format("Token revocation request failed: %s", ex.getError()), ex);}this.authenticationFailureHandler.onAuthenticationFailure(request, response, ex);
}

处理成功和失败的响应

成功处理器和失败处理器分别发送适当的 HTTP 响应:

成功处理器

private void sendRevocationSuccessResponse(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {response.setStatus(HttpStatus.OK.value());
}

失败处理器

private void sendErrorResponse(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException {OAuth2Error error = ((OAuth2AuthenticationException) exception).getError();ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);httpResponse.setStatusCode(HttpStatus.BAD_REQUEST);this.errorHttpResponseConverter.write(error, null, httpResponse);
}



默认的令牌生成策略

授权服务中会使用OAuth2TokenGenerator来生成令牌,在如下Provider中:

  • OAuth2AuthorizationCodeAuthenticationProvider
  • OAuth2RefreshTokenAuthenticationProvider
  • OAuth2ClientCredentialsAuthenticationProvider
  • OidcClientRegistrationAuthenticationProvider

上面的Provider内大都使用:

this.tokenGenerator.generate(tokenContext)

进行代码生成。


this.tokenGenerator.generate(tokenContext) 对应了 OAuth2AuthorizationServerConfigurer 中的如下代码配置:

public OAuth2AuthorizationServerConfigurer tokenGenerator(OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) {Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");getBuilder().setSharedObject(OAuth2TokenGenerator.class, tokenGenerator);return this;
}

在这里,tokenGenerator 是一个实现了 OAuth2TokenGenerator 接口的组件,用于生成 OAuth2 令牌。在配置过程中,可以通过 OAuth2AuthorizationServerConfigurertokenGenerator 方法来设置自定义的 OAuth2TokenGenerator 实现。

默认情况下的实现

在默认情况下,Spring Authorization Server 提供了一个默认的 OAuth2TokenGenerator 实现,即 DelegatingOAuth2TokenGeneratorDelegatingOAuth2TokenGenerator 是一个委派模式的生成器,它会将令牌生成的任务委派给多个具体的令牌生成器。通常,它包含以下几个具体的生成器:

  1. JwtGenerator

    用于生成 JWT(JSON Web Token)类型的访问令牌和 ID 令牌。

  2. OAuth2AccessTokenGenerator

    用于生成常规的 OAuth2 访问令牌。

  3. OAuth2RefreshTokenGenerator

    用于生成 OAuth2 刷新令牌。

配置默认的 OAuth2TokenGenerator

在没有自定义配置的情况下,Spring Authorization Server 会自动配置上述默认的 OAuth2TokenGenerator 实现。这意味着,在默认配置下,this.tokenGenerator.generate(tokenContext) 会使用 DelegatingOAuth2TokenGenerator 来生成令牌,并根据上下文信息选择合适的具体生成器(如 JwtGeneratorOAuth2AccessTokenGeneratorOAuth2RefreshTokenGenerator)。


默认的配置示例

假设我们没有自定义任何 OAuth2TokenGenerator,Spring Authorization Server 的默认配置可能如下:

@Configuration
public class AuthorizationServerConfig extends OAuth2AuthorizationServerConfigurerAdapter {@Overridepublic void configure(OAuth2AuthorizationServerConfigurer authorizationServerConfigurer) {// 默认情况下,Spring Authorization Server 会配置 `DelegatingOAuth2TokenGenerator`// 不需要额外配置 tokenGenerator}
}

在这种默认配置下,OAuth2AuthorizationCodeAuthenticationProvider 中的 this.tokenGenerator.generate(tokenContext) 会使用默认的 DelegatingOAuth2TokenGenerator 来处理令牌的生成。


详细解释 DelegatingOAuth2TokenGenerator

DelegatingOAuth2TokenGenerator 会根据 tokenContext 中的 tokenType 来决定使用哪个具体的生成器:

  • 如果 tokenTypeOAuth2TokenType.ACCESS_TOKEN,它会使用 OAuth2AccessTokenGenerator 来生成访问令牌。
  • 如果 tokenTypeOAuth2TokenType.REFRESH_TOKEN,它会使用 OAuth2RefreshTokenGenerator 来生成刷新令牌。
  • 如果 tokenTypeID_TOKEN_TOKEN_TYPE,它会使用 JwtGenerator 来生成 ID 令牌。

OAuth2AuthorizationCodeAuthenticationProvider的认证方法中,当grant_typecode时,会默认指定 tokenTypeOAuth2TokenType.ACCESS_TOKEN

想要生成刷新令牌时,指定grant_typerefresh_token即可。

如果需要生成 ID 令牌,需要在 OpenID Connect 的上下文中明确指定 tokenTypeID_TOKEN_TOKEN_TYPE

自定义 OAuth2TokenGenerator

如果需要自定义 OAuth2TokenGenerator,可以通过 OAuth2AuthorizationServerConfigurertokenGenerator 方法进行配置。例如:

@Configuration
public class AuthorizationServerConfig extends OAuth2AuthorizationServerConfigurerAdapter {@Overridepublic void configure(OAuth2AuthorizationServerConfigurer authorizationServerConfigurer) {OAuth2TokenGenerator<OAuth2Token> customTokenGenerator = new CustomTokenGenerator();authorizationServerConfigurer.tokenGenerator(customTokenGenerator);}
}

在上述配置中,CustomTokenGenerator 是一个自定义的 OAuth2TokenGenerator 实现,它将覆盖默认的 DelegatingOAuth2TokenGenerator

总结来说,默认情况下,Spring Authorization Server 会使用 DelegatingOAuth2TokenGenerator 作为 OAuth2TokenGenerator 的默认实现,负责生成不同类型的令牌。你也可以通过 OAuth2AuthorizationServerConfigurer 来自定义 OAuth2TokenGenerator 的实现。




令牌认证与JWKS

关键过滤器NimbusJwkSetEndpointFilter

NimbusJwkSetEndpointFilter 是 Spring Security OAuth 2.0 授权服务器框架中的一个过滤器,位于包 org.springframework.security.oauth2.server.authorization.web 下。它的主要作用是公开 OAuth 2.0 授权服务器的 JSON Web Key Set (JWKS) 端点。

主要作用:

  1. 公开 JWKS 端点
    NimbusJwkSetEndpointFilter 负责公开一个 JWKS 端点,通常位于路径 /oauth2/jwks。这个端点用于公开授权服务器的公钥,这些公钥可以被 OAuth 2.0 客户端和资源服务器用来验证来自授权服务器的 JSON Web Token (JWT) 的签名。

  2. 提供公钥信息
    该过滤器会返回一个包含授权服务器用于签名 JWT 的公钥集合的 JSON 响应。这些公钥是使用 JSON Web Key (JWK) 格式表示的,客户端和资源服务器可以从中提取并使用相应的公钥来验证签名。

  3. 与 Nimbus 实现集成
    该过滤器通常与 Nimbus JOSE + JWT 库集成,用于处理 JWT 的签名和验证。Nimbus 是一个广泛使用的开源库,用于处理 JOSE (JSON Object Signing and Encryption) 和 JWT。

  4. 保证安全性
    通过公开的 JWKS 端点,授权服务器可以安全地与其他服务共享公钥,而无需直接暴露私钥。客户端和资源服务器通过访问该端点,可以获取最新的公钥,用于验证从授权服务器收到的 JWT 签名。

工作流程:

  1. JWT 签名和验证
    授权服务器生成的 JWT 通常是使用私钥进行签名的。客户端和资源服务器需要知道相应的公钥才能验证 JWT 的签名,确保 JWT 是由授权服务器签发的,并且未被篡改。

  2. JWKS 端点的作用
    NimbusJwkSetEndpointFilter 处理到 JWKS 端点的 HTTP 请求,当收到请求时,它返回授权服务器的公钥集合。这个公钥集合以 JSON Web Key Set (JWKS) 格式表示,包含多个 JSON Web Key (JWK) 对象。

  3. 响应结构
    该过滤器生成的响应是一个 JSON 对象,包含一个 keys 字段,表示多个 JWK 对象。例如:

    {"keys": [{"kty": "RSA","kid": "key-id","use": "sig","alg": "RS256","n": "...","e": "..."}// 可能还有其他密钥]
    }
    

典型的使用场景:

  • OAuth 2.0/OpenID Connect 认证和授权:当你在使用 Spring Authorization Server 实现 OAuth 2.0 或 OpenID Connect (OIDC) 认证时,NimbusJwkSetEndpointFilter 用于提供必要的公钥,客户端和资源服务器可以通过 JWKS 端点来获取这些公钥,以验证 JWT 签名。

  • 微服务架构:在一个微服务架构中,多个服务可能需要验证 JWT 令牌的签名。NimbusJwkSetEndpointFilter 提供了一个集中的位置来公开公钥,所有服务可以通过该端点获取公钥,而无需直接接触私钥。

总结来说,NimbusJwkSetEndpointFilter 通过公开 JWKS 端点,为 OAuth 2.0 和 OpenID Connect 实现中的 JWT 签名验证提供了一个安全、标准化的解决方案。


JWKS配置

源码中,通过OAuth2AuthorizationServerConfigurer进行配置,下面是配置JWKSource的源码方法

@Override
public void configure(HttpSecurity httpSecurity) {this.configurers.values().forEach(configurer -> configurer.configure(httpSecurity));AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(httpSecurity);AuthorizationServerContextFilter authorizationServerContextFilter = new AuthorizationServerContextFilter(authorizationServerSettings);httpSecurity.addFilterAfter(postProcess(authorizationServerContextFilter), SecurityContextHolderFilter.class);//获取并配置JWSJWKSource<com.nimbusds.jose.proc.SecurityContext> jwkSource = OAuth2ConfigurerUtils.getJwkSource(httpSecurity);if (jwkSource != null) {//通过构造方法构建NimbusJwkSetEndpointFilter,为其指定JWKSourceNimbusJwkSetEndpointFilter jwkSetEndpointFilter = new NimbusJwkSetEndpointFilter(jwkSource, authorizationServerSettings.getJwkSetEndpoint());httpSecurity.addFilterBefore(postProcess(jwkSetEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);}}

1. 获取JWKSource

通过上面源码方法中的OAuth2ConfigurerUtils.getJwkSource(httpSecurity)方法获取

其实是从HttpSecurity中获取JWKSource的:

static JWKSource<SecurityContext> getJwkSource(HttpSecurity httpSecurity) {JWKSource<SecurityContext> jwkSource = httpSecurity.getSharedObject(JWKSource.class);if (jwkSource == null) {ResolvableType type = ResolvableType.forClassWithGenerics(JWKSource.class, SecurityContext.class);jwkSource = getOptionalBean(httpSecurity, type);if (jwkSource != null) {httpSecurity.setSharedObject(JWKSource.class, jwkSource);}}return jwkSource;
}
  • getSharedObject(JWKSource.class) 是为了从 HttpSecurity 中获取一个 JWKSource<SecurityContext> 对象。如果 JWKSource 之前已经被配置并存储在 HttpSecurity 的共享对象池中,它会直接返回这个对象。
  • 如果 JWKSource 还没有被设置,则会尝试通过 getOptionalBean 方法从 Spring 容器中获取一个 JWKSource 的 Bean,并将其设置到共享对象池中,以便后续的配置或过滤器可以使用。

在 Spring Security 中,HttpSecurity.getSharedObject(Class<T> sharedType) 方法用于从 HttpSecurity 的共享对象池中获取特定类型的对象。在配置过程中,Spring Security 允许多个配置类或过滤器共享一些公共对象,这些对象可以通过 getSharedObject 方法来获取。

2. JWKSource 是何时以及如何被赋予 HttpSecurity

JWKSource 是在以下情况下被赋予 HttpSecurity 的:

  1. 首先检查共享对象池:如上所述,代码首先使用 getSharedObject(JWKSource.class) 检查 HttpSecurity 是否已经包含了一个 JWKSource 对象。

  2. 尝试从 Spring 容器获取:如果共享对象池中没有找到 JWKSource,则通过 getOptionalBean(httpSecurity, type) 尝试从 Spring 的 ApplicationContext 中获取 JWKSource Bean。

  3. 设置共享对象:如果成功获取到 JWKSource Bean,它会使用 httpSecurity.setSharedObject(JWKSource.class, jwkSource) 将这个对象存储到 HttpSecurity 的共享对象池中,以便其他配置类或过滤器可以访问。


自定义JWKSource示例

以下是使用非对称密钥和对称密钥分别配置 JWKSource<SecurityContext> 的示例代码。

1. 使用非对称密钥的 JWKSource 配置

使用非对称密钥时,通常会使用 RSA 或 EC 密钥对进行签名或加密。以下示例展示了如何配置一个基于 RSA 密钥对的 JWKSource<SecurityContext>

import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.proc.SecurityContext;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.jwk.source.JWKSelector;
import com.nimbusds.jose.jwk.gen.RSAKeyGenerator;import java.security.KeyPair;
import java.security.KeyPairGenerator;@Bean
public JWKSource<SecurityContext> jwkSource() throws Exception {// 生成RSA密钥对KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");keyPairGenerator.initialize(2048);KeyPair keyPair = keyPairGenerator.generateKeyPair();// 构建JWKRSAKey rsaKey = new RSAKey.Builder(keyPair.getPublic()).privateKey(keyPair.getPrivate()).keyID("rsa-key").build();// 构建JWKSetJWKSet jwkSet = new JWKSet(rsaKey);// 返回JWKSource实例return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}

2. 使用对称密钥的 JWKSource 配置

使用对称密钥时,通常会使用 HMAC 或 AES 进行签名或加密。以下示例展示了如何配置一个基于对称密钥的 JWKSource<SecurityContext>

import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.OctetSequenceKey;
import com.nimbusds.jose.proc.SecurityContext;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.jwk.source.JWKSelector;
import java.util.Base64;@Bean
public JWKSource<SecurityContext> jwkSource() {// 生成对称密钥 (256位)byte[] secret = Base64.getDecoder().decode("YourSecretBase64EncodedKey==");// 构建JWKOctetSequenceKey octetSequenceKey = new OctetSequenceKey.Builder(secret).keyID("hmac-key").build();// 构建JWKSetJWKSet jwkSet = new JWKSet(octetSequenceKey);// 返回JWKSource实例return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}

解释

  • 非对称密钥: 使用 RSA 密钥对来生成 JWK。RSAKey 代表 RSA 密钥对,包含公钥和私钥。这个配置适用于需要使用公钥加密和私钥解密的场景,比如签名验证。

  • 对称密钥: 使用一个随机生成的对称密钥来生成 JWK。OctetSequenceKey 代表对称密钥。这个配置适用于 HMAC 签名或 AES 加密的场景。

这两种配置方式在使用 Spring Security 的 OAuth2.0 或 JWT 时非常常见,分别适用于不同的安全需求。


http://www.ppmy.cn/embedded/99571.html

相关文章

鸿蒙内核源码分析(物理内存篇) | 怎么管理物理内存

如何初始化物理内存? 鸿蒙内核物理内存采用了段页式管理&#xff0c;先看两个主要结构体.结构体的每个成员变量的含义都已经注解出来&#xff0c;请结合源码理解. #define VM_LIST_ORDER_MAX 9 //伙伴算法分组数量&#xff0c;从 2^0&#xff0c;2^1&#xff0c;...&a…

开学季必备好物:南卡Pro5骨传导耳机助你轻松应对新学期挑战

开学季的到来&#xff0c;总是伴随着新的挑战和机遇。对于莘莘学子而言&#xff0c;这是一个重新出发的时刻&#xff0c;装备自然也要更新换代&#xff0c;以迎接新学期的学习和生活。在众多开学必备好物中&#xff0c;一款高性能的耳机无疑能成为你学习、休闲的好帮手。今天&a…

网络安全在2024好入行吗?

前言 024年的今天&#xff0c;慎重进入网安行业吧&#xff0c;目前来说信息安全方向的就业对于学历的容忍度比软件开发要大得多&#xff0c;还有很多高中被挖过来的大佬。 理由很简单&#xff0c;目前来说&#xff0c;信息安全的圈子人少&#xff0c;985、211院校很多都才建立…

rabbitMQ安装与简单demo

安装 mac安装有了brew很方便&#xff0c;windows的可参考 win10 安装rabbitMQ详细步骤 brew install rabbitmq启动 brew services start rabbitmq关闭 brew services stop rabbitmq出了问题之后可以重启一下 brew services restart rabbitmqsome issue 某些库下载超时 比…

如何去除抖音视频水印,还原视频的3种方法

抖音等短视频平台已经成为人们获取信息和娱乐的重要渠道。然而&#xff0c;视频上的水印往往会影响到观看体验&#xff0c;甚至在某些情况下限制了视频的分享和使用。本文将介绍三种去除抖音视频水印的方法&#xff0c;帮助用户还原视频的原始面貌。 工具一&#xff1a;奈斯水…

浮毛难清除、异味难消散?选到不好的宠物空气净化器会有什么危害

近年来&#xff0c;不少人家里都养了宠物&#xff0c;有些是猫、有些是狗&#xff0c;甚至有些是兔子&#xff0c;不少人希望能通过它们抒发心中的郁闷&#xff0c;成为自己的搭子。这些宠物在能带来欢乐的同时也会带来一些小烦恼&#xff0c;比如宠物的浮毛、异味都困扰着我们…

ubuntu x86_64系统上安装运行aarch系统的虚拟机

安装qemu-system-aarch64 创建sda.qcow2 虚拟磁盘 运行命令启动虚拟机 sudo qemu-system-aarch64 -M virt-4.0 -m 4G -cpu cortex-a57 -bios /usr/share/qemu-efi-aarch64/QEMU_EFI.fd -cdrom ~/下载/openEuler-24.03-LTS-aarch64-dvd.iso -drive ifnone,filesda.qcow2,idhd0…

C++的模板简介

文章目录 一、前言二、函数模板&#xff08;Function Template&#xff09;三、类模板&#xff08;Class Template&#xff09;四、变参模板&#xff08;Variadic Template&#xff09;五、模板的递归与元编程六、模板的局限与陷阱七、常用模板的实例八、C20 的概念&#xff08…