本文为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授权码流程示意
请求流程:
-
客户端发起授权码请求:用户通过客户端的前端(例如网页)触发请求(点击按钮或扫码登录等方式)。
GET /oauth2/authorize?response_type=code&client_id=client_id_example&redirect_uri=https://client.example.com/callback&scope=read&state=xyz
-
客户端将用户重定向到授权服务器:授权服务器向用户展示登录页面和授权同意页面。
-
用户同意授权并提交同意请求:用户同意后提交表单。
POST /oauth2/authorize Content-Type: application/x-www-form-urlencodedclient_id=client_id_example&state=xyz&scope=read
-
授权服务器重定向回客户端:如果用户同意授权,授权服务器会生成授权码,并携带授权码重定向用户浏览器到客户端指定的 地址(客户端请求中的
redirect_uri
参数)。HTTP/1.1 302 Found Location: https://client.example.com/callback?code=authorization_code_example&state=xyz
-
客户端携带授权码请求令牌:客户端得到授权服务返回的code后,携带code去换取token,但在获取token令牌之前,会由授权服务先进行客户端认证,认证通过后在生成token返回:
请求头除了Host与Content-Type,多了一个
Authorization
:值为
Basic Y2xpZW50SWQ6Y2xpZW50U2VjcmV0
,表示客户端使用 Basic (client_secret_basic)方式进行认证,其中Y2xpZW50SWQ6Y2xpZW50U2VjcmV0
是clientId: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
请求源码处理流程
- 客户端先获取授权码,发起
/oauth2/authorize
请求(GET),进入授权服务OAuth2AuthorizationEndpointFilter
过滤器的doFilterInternal
方法进行处理。 OAuth2AuthorizationEndpointFilter
过滤器使用转换器OAuth2AuthorizationCodeRequestAuthenticationConverter
将请求转换为权限对象OAuth2AuthorizationCodeRequestAuthenticationToken
。- 转换后进行验证,过滤器调用
OAuth2AuthorizationCodeRequestAuthenticationProvider
的authenticate
方法对上一步得到的权限对象OAuth2AuthorizationCodeRequestAuthenticationToken
进行验证,如果用户未登录,则重定向到登录页面进行登录,登录后重新发起/oauth2/authorize
(使用过滤器缓存实现的)。 - 重新进行1、2步的过程。
- 请求回到权限验证器
OAuth2AuthorizationCodeRequestAuthenticationProvider
的authenticate
方法中,验证已登录后,检查权限对象是否已授权(数据库实现使用oauth2_authorization_consent表),如果未授权,重定向到授权页面进行授权 - 前台点击授权按钮,重新发起
/oauth2/authorize
(这次是POST请求)。这次会将请求转换为OAuth2AuthorizationConsentAuthenticationToken
权限对象,因为转换条件是请求为POST、且请求路径为/oauth2/authorize
(源码的判断条件) - 进入权限提供者
OAuth2AuthorizationConsentAuthenticationProvider
的authenticate
方法进行授权验证,检查oauth2_authorization表存在授权请求记录,且oauth2_registered_client表中查clientid
信息无误,则进行授权,向oauth2_authorization_consent表插入授权记录,并生成code
授权码,调用/oauth2/authorize
请求携带的重定向地址返回授权码。
下面结合此流程进行源码分析
授权码请求过滤器
简介
OAuth2AuthorizationEndpointFilter
OAuth2AuthorizationEndpointFilter
过滤器在 Spring Security OAuth2 中扮演了重要的角色,用于处理 OAuth 2.0 授权码请求(/oauth2/authorize
路径),并启动授权流程。大致处理:
- 处理授权请求:拦截到达授权端点的客户端请求,验证请求的有效性,并根据请求的参数和状态决定后续操作。
- 启动授权流程:引导用户进行认证和授权,如果用户已登录且授予了相关权限,则生成授权码,并返回给客户端
处理的两种请求
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_challenge
和code_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
会调用ProviderManager
的authenticate
方法,遍历AuthenticationProvider
的所有的实现类,看哪个实现类支持验证当前的权限认证对象(通过supports
方法判断),就调用哪个实现类进行认证。
ProviderManager
的authenticate
方法:
@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
GET的OAuth2AuthorizationCodeRequestAuthenticationConverter
转换器会将请求转为权限对象OAuth2AuthorizationCodeRequestAuthenticationToken
。public final class OAuth2AuthorizationCodeRequestAuthenticationConverter implements AuthenticationConverter {@Overridepublic Authentication convert(HttpServletRequest request) {//.................return new OAuth2AuthorizationCodeRequestAuthenticationToken(authorizationUri, clientId, principal,redirectUri, state, scopes, additionalParameters);}}
-
转换授权同意请求
/oauth2/authorize
POST的OAuth2AuthorizationConsentAuthenticationConverter
转换器会将请求转为权限对象OAuth2AuthorizationConsentAuthenticationToken
。public final class OAuth2AuthorizationConsentAuthenticationConverter implements AuthenticationConverter {@Overridepublic Authentication convert(HttpServletRequest request) {//.................return new OAuth2AuthorizationConsentAuthenticationToken(authorizationUri, clientId, principal,state, scopes, additionalParameters);}}
再看两个Provider
的supports
方法:
-
OAuth2AuthorizationCodeRequestAuthenticationProvider
的supports
方法:@Override public boolean supports(Class<?> authentication) {//判断权限对象类型是否为OAuth2AuthorizationCodeRequestAuthenticationToken,是则支持处理//对应转换器OAuth2AuthorizationCodeRequestAuthenticationConverter返回的权限对象return OAuth2AuthorizationCodeRequestAuthenticationToken.class.isAssignableFrom(authentication); }
-
OAuth2AuthorizationConsentAuthenticationProvider
的supports
方法:@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
)。
然后在OAuth2AuthorizationCodeRequestAuthenticationConverter
的convert
方法最后返回转换完成的权限对象时,返回的权限对象OAuth2AuthorizationCodeRequestAuthenticationToken
默认继承父类AbstractAuthenticationToken
,并继承其authenticated
属性,值为false
:
public abstract class AbstractAuthenticationToken implements Authentication, CredentialsContainer {private final Collection<GrantedAuthority> authorities;private Object details;//默认falseprivate boolean authenticated = false;//.................
}
然后对授权码请求转换过来的权限对象进行验证,在
OAuth2AuthorizationCodeRequestAuthenticationProvider
的authenticate
方法如下代码处,通过isPrincipalAuthenticated
方法判断authenticated
属性:
由于上面转换返回的OAuth2AuthorizationCodeRequestAuthenticationToken
的authenticated = 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),授权码请求经过转换后,再次到达OAuth2AuthorizationCodeRequestAuthenticationProvider
的authenticate
方法中进行验证:
此时已登录,不会再进入if代码中,也不会返回isAuthenticated()
是 false
的 authorizationCodeRequestAuthentication
对象,而是继续向下执行代码,判断请求是否已经过授权:
@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
:将OAuth2AuthorizationCodeRequestAuthenticationProvider
的authenticate
方法验证后的结果,返回到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
过滤器重定向回客户端
继续接上面的步骤,当登录、授权都通过后,代码执行到OAuth2AuthorizationCodeRequestAuthenticationProvider
的authenticate
方法如下生成授权码注释位置:
@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
起到关键作用,其内部主要做了以下事情:
- 验证用户是否已登录。
- 验证请求是否已授权。
- 在用户已登录且已对请求进行授权后,为授权请求生成授权码。
而
OAuth2AuthorizationEndpointFilter
过滤器在调用OAuth2AuthorizationCodeRequestAuthenticationProvider
时,做了如下事情:
- Provider检测用户未登录时,过滤器向下执行过滤,让用户进行登录。
- Provider检测请求未授权时,过滤器调用授权同意请求的转换器及验证器完成授权。
- 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
对象。具体步骤包括:
- 检查请求方法是否为 GET 或是否符合 OIDC 请求匹配器。
- 提取请求参数并进行验证。
- 验证和处理
response_type
,client_id
,redirect_uri
,scope
,state
,code_challenge
, 和code_challenge_method
等参数。其中response_type
,client_id
为必须,没有会抛异常,其他为可选。 - 提取附加参数。
- 将所有提取的信息封装到
OAuth2AuthorizationCodeRequestAuthenticationToken
对象中并返回。
通过这些步骤,该转换器确保授权码请求的参数有效且完整,为后续的认证处理提供所需的信息。
授权请求认证源码解析
OAuth2AuthorizationCodeRequestAuthenticationProvider
的authenticate
方法
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());
}
总结
OAuth2AuthorizationCodeRequestAuthenticationProvider
的authenticate
方法的大体处理流程
- 根据请求的客户端来检索授权服务中已注册的客户端,未注册的客户端验证不通过
- 验证请求redirect_uri重定向地址的有效性
- 检查请求的授权类型是否为授权码类型,校验code_challenge参数(如果有)
- 检查用户是否已登录,未登录则结束认证方法,由授权服务向登录页重定向,来进行用户登录
- 验证已登录后,检查客户端请求是否被同意授权,未授权则结束方法,由授权服务向收取按同意页重定向,来进行用户授权
- 验证已登录且已完成请求授权后,生成授权码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
对象。具体步骤包括:
- 检查请求方法是否为 POST,并确保请求不包含
response_type
参数。 - 提取请求参数并进行验证。
- 验证和处理
client_id
,state
,scope
等参数。 - 提取附加参数。
- 将所有提取的信息封装到
OAuth2AuthorizationConsentAuthenticationToken
对象中并返回。
通过这些步骤,该转换器确保授权同意请求的参数有效且完整,为后续的认证处理提供所需的信息。
授权同意请求认证源码解析
OAuth2AuthorizationConsentAuthenticationProvider
的authenticate
方法
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);
}
总结
流程大致总结
- 客户端授权请求记录中是否存在state参数
- 验证用户是否登录、身份是否有效,客户端是否已经注册
- 检查请求的授权范围,并确保给与的权限范围只包括用户批准的权限
- 生成授权码,更新客户端请求授权的信息记录,最后返回带有授权码的权限对象,用于过滤器进行重定向返回给客户端
两个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_jwt
与private_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
请求参数解释:
-
client_assertion_type
:- 值为
urn:ietf:params:oauth:client-assertion-type:jwt-bearer
(固定值),表示使用 JWT 作为客户端断言。
- 值为
-
client_assertion
:- JWT 断言的具体值。这是一个签名的 JWT,包含客户端身份信息。
-
client_id
:- 客户端的 ID,用于标识客户端。
-
grant_type
:- 授权类型,此处为
client_credentials
,可指定为其他类型
- 授权类型,此处为
示例 JWT 断言(简化版):
{"alg": "RS256","typ": "JWT"
}
{"sub": "clientId","aud": "https://example.com/oauth2/token","iat": 1516239022
}
client_secret_basic方式
对应的是ClientAuthenticationMethod
中的CLIENT_SECRET_BASIC
:
-
客户端会使用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
请求头解释:
-
Authorization
- 值为
Basic Y2xpZW50SWQ6Y2xpZW50U2VjcmV0
,表示 Basic 认证,其中Y2xpZW50SWQ6Y2xpZW50U2VjcmV0
是clientId:clientSecret
(客户端id:客户端密钥)经过 URL 编码后的字符串。
- 值为
client_secret_post方式
对应ClientAuthenticationMethod
的CLIENT_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
:客户端 IDclient_secret
:客户端密钥
- 其他参数:可能包括
grant_type
、code
和redirect_uri
等,如果有的话。
这种请求格式用于 OAuth 2.0 客户端凭据授予流程,客户端通过 POST 方法将自己的凭据发送给授权服务器,以获取访问令牌。
PKCE方式
对应ClientAuthenticationMethod
中的 NONE
:用于公共客户端认证。
公共客户端通常是在没有客户端密钥的情况下进行认证的,PKCE 的工作原理是通过增加一个动态密钥来防止授权码被劫持。它在 OAuth 2.0 授权码流程的基础上,增加了两个新的参数:code_challenge
和 code_verifier
。
PKCE大致处理流程
- 客户端生成一个随机字符串
code_verifier
,使用哈希算法(通常是 SHA-256)对code_verifier
进行哈希运算,生成code_challenge
。 - 客户端发起授权码请求时,将
code_challenge
以及其他必要的授权参数(如client_id
、redirect_uri
等)一起发送到授权服务器。 - 用户在授权服务器上进行认证时,会保存客户端发来的
code_challenge
,然后返回授权码给客户端。 - 客户端接收到授权码后,将授权码和
code_verifier
发送到授权服务器,以交换访问令牌。 - 授权服务器接收到请求,使用相同的哈希算法对
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
:客户端 IDcode_verifier
:PKCE 流程中的 code_verifier
- 其他参数:可能包括
grant_type
、code
和redirect_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
请求处理流程
- 接收请求:客户端发送 POST 请求到授权服务器的
/oauth2/token
端点,包含所需的头部和参数。 - 提取头部:
ClientSecretBasicAuthenticationConverter
从请求中提取Authorization
头部。 - 验证头部:检查头部是否存在,且类型是否为
Basic
。 - 解码凭证:将 Base64 编码的凭证部分解码为用户名和密码。
- 验证凭证:检查凭证是否包含用户名和密码两个部分,且不为空。
- 创建认证对象:如果所有检查通过,创建一个
OAuth2ClientAuthenticationToken
对象,并填充相应的参数和附加参数。 - 返回认证对象:返回生成的认证对象供后续使用。
源码解析
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_basic
或client_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());
}
源码流程概括
-
转换认证对象:
- 将传入的
Authentication
对象转换为OAuth2ClientAuthenticationToken
对象。
- 将传入的
-
验证认证方法:
- 检查客户端的认证方法是否为
client_secret_basic
或client_secret_post
,如果不是,返回null
表示不支持该认证方法。
- 检查客户端的认证方法是否为
-
获取客户端ID和查找已注册的客户端信息:
- 获取客户端ID,并从存储库中查找对应的已注册客户端信息。如果找不到,抛出异常。
-
检查已注册客户端的认证方法:
- 检查已注册客户端是否支持当前使用的认证方法,如果不支持,抛出异常。
-
验证客户端凭据:
- 检查客户端凭据是否为空。
- 获取客户端密钥,并使用
passwordEncoder
验证请求中的密钥与已注册客户端密钥是否匹配。如果不匹配,抛出异常。
-
检查客户端密钥是否过期:
- 检查客户端密钥是否已过期,如果过期,抛出异常。
-
日志记录:
- 如果启用跟踪日志,则记录相关信息,如已检索到的客户端、已验证的客户端认证参数和已认证的客户端密钥。
-
验证
code_verifier
参数:- 对于保密客户端,验证
code_verifier
参数(如果可用)。
- 对于保密客户端,验证
-
返回认证结果:
- 返回新的
OAuth2ClientAuthenticationToken
,表示认证成功。
- 返回新的
密钥匹配
ClientSecretAuthenticationProvider
验证的关键点在于密钥的匹配验证,通过DelegatingPasswordEncoder
的matches
方法:
DelegatingPasswordEncoder
是Spring Security中的一个密码编码器,用于根据不同的密码编码算法来匹配密码,它可以根据密码的前缀来选择适当的编码器进行密码匹配
@Override
public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {//如果rawPassword和prefixEncodedPassword都为null,则认为匹配成功。//这是为了处理特殊情况,比如在密码为空的情况下进行比较。if (rawPassword == null && prefixEncodedPassword == null) {return true;}//提取出密码编码器的ID。这个ID用于确定使用哪个具体的PasswordEncoder进行密码匹配String id = extractId(prefixEncodedPassword);//根据提取出的ID从idToPasswordEncoder映射中获取具体的PasswordEncoder实例。PasswordEncoder delegate = this.idToPasswordEncoder.get(id);//如果没有找到对应的编码器,则使用默认的密码匹配器进行验证。if (delegate == null) {return this.defaultPasswordEncoderForMatches.matches(rawPassword, prefixEncodedPassword);}//提取出编码后的密码部分,然后使用对应的PasswordEncoder进行实际的密码匹配操作String encodedPassword = extractEncodedPassword(prefixEncodedPassword);return delegate.matches(rawPassword, encodedPassword);
}
对于一个密码{bcrypt}$2a$10$...
,extractId
方法提取到的ID是bcrypt
,然后从idToPasswordEncoder
映射中获取BCryptPasswordEncoder
实例来验证密码。
DelegatingPasswordEncoder
下的密码编码器实现有很多,具体参考如下路径源码的注解:
org.springframework.security.crypto.password.DelegatingPasswordEncoder
String idForEncode = "bcrypt";Map<String,PasswordEncoder> encoders = new HashMap<>();encoders.put(idForEncode, new BCryptPasswordEncoder());encoders.put("noop", NoOpPasswordEncoder.getInstance());encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());encoders.put("scrypt", new SCryptPasswordEncoder());encoders.put("sha256", new StandardPasswordEncoder()); PasswordEncoder passwordEncoder = new DelegatingPasswordEncoder(idForEncode, encoders);
client_secret_post认证源码解析
使用
ClientSecretPostAuthenticationConverter
将请求转为认证对象,使用ClientSecretAuthenticationProvider
对转换后的认证对象进行验证:
ClientSecretBasicAuthenticationConverter
会从请求体表单参数中取出client_secret
的值,这里的client_secret
未经过任何加密ClientSecretAuthenticationProvider
负责将取出的密钥部分与存储中的客户端信息密钥做对比,对比成功则验证通过
转换器
转换器类:ClientSecretPostAuthenticationConverter
ClientSecretPostAuthenticationConverter
用于将通过 POST 请求方式提交客户端 ID 和客户端密钥的请求转换为 OAuth2ClientAuthenticationToken
对象。这种转换器主要用于 OAuth2 客户端认证。
源码解析
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_post
与client_secret_basic
均使用ClientSecretAuthenticationProvider
进行验证:
client_secret_post
和 client_secret_basic
的区别在于它们的客户端凭证传递方式不同:
client_secret_post
:客户端将客户端ID和客户端密钥作为请求体参数发送。这种方法的安全性较低,因为客户端密钥以明文形式发送。client_secret_basic
:客户端将客户端ID和客户端密钥编码为Base64,并将其作为HTTP Basic认证的头部发送。这种方法比client_secret_post
稍微安全一些,因为客户端密钥在传输时经过了Base64编码,但仍然不提供足够的安全性。
PKCE认证源码解析
转换器
转换器类:
PublicClientAuthenticationConverter
OAuth2AuthorizationCodeRequestAuthenticationConverter
的convert
方法,将PKCE参数code_challenge
与code_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);
会调用CodeVerifierAuthenticator
的authenticate
对CODE_CHALLENGE
及CODE_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_type
和client_assertion
参数,并验证其存在和格式。如果符合预期格式,则会创建一个
OAuth2ClientAuthenticationToken
,其中包含客户端的 ID 和 JWT 断言,供后续的身份验证流程使用。
public final class JwtClientAssertionAuthenticationConverter implements AuthenticationConverter {private static final ClientAuthenticationMethod JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD =new ClientAuthenticationMethod("urn:ietf:params:oauth:client-assertion-type:jwt-bearer");@Nullable@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_type
及client_assertion
参数,则会被JwtClientAssertionAuthenticationConverter
转换成功并返回认证对象OAuth2ClientAuthenticationToken
,交由Provider
进行验证
认证器
认证类:
JwtClientAssertionAuthenticationProvider
如下是JwtClientAssertionAuthenticationProvider
的authenticate
方法。该方法用于验证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);
}
代码功能概述
-
类型转换和方法检查: 首先将传入的
Authentication
对象转换为OAuth2ClientAuthenticationToken
类型,并检查其认证方法是否为JWT客户端断言认证。 -
客户端ID和注册客户端查找: 从
Authentication
对象中获取客户端ID,并在注册的客户端存储库中查找对应的RegisteredClient
对象。如果找不到,抛出异常。 -
客户端认证方法检查: 确保注册的客户端支持
PRIVATE_KEY_JWT
或CLIENT_SECRET_JWT
认证方法,如果不支持,抛出异常。 -
客户端凭据检查: 检查客户端凭据是否为空,如果为空,抛出异常。
-
JWT解码和验证: 使用
JwtDecoder
解码客户端凭据,生成JWT断言。如果解码失败,抛出异常。 -
验证
code_verifier
参数: 如果可用,验证机密客户端的code_verifier
参数。 -
确定客户端认证方法: 根据客户端的签名算法确定认证方法是
PRIVATE_KEY_JWT
还是CLIENT_SECRET_JWT
。 -
返回已验证的身份验证令牌: 创建并返回一个新的
OAuth2ClientAuthenticationToken
对象,包含已验证的客户端和JWT断言。
认证完成后,则向下执行过滤器,由OAuth2TokenEndpointFilter
进行token处理。
解码器
上面源码中,通过
JwtClientAssertionAuthenticationProvider
构造方法制定了默认的解码器JwtClientAssertionDecoderFactory
public JwtClientAssertionAuthenticationProvider(RegisteredClientRepository registeredClientRepository,OAuth2AuthorizationService authorizationService) {Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");Assert.notNull(authorizationService, "authorizationService cannot be null");this.registeredClientRepository = registeredClientRepository;this.codeVerifierAuthenticator = new CodeVerifierAuthenticator(authorizationService);//指定默认解码器this.jwtDecoderFactory = new JwtClientAssertionDecoderFactory();
}
解码器
JwtClientAssertionDecoderFactory
中的buildDecoder
方法构建了解析jwt
的逻辑:
根据注册客户端RegisteredClient
的设置来决定如何验证JWT签名。以下是逐行解释:
private static NimbusJwtDecoder buildDecoder(RegisteredClient registeredClient) {// 从注册客户端的设置中获取JWS算法JwsAlgorithm jwsAlgorithm = registeredClient.getClientSettings().getTokenEndpointAuthenticationSigningAlgorithm();// 如果JWS算法是签名算法(非对称加密)if (jwsAlgorithm instanceof SignatureAlgorithm) {// 获取JWK Set 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);
}
关键点解释
-
JWS算法获取:
- 从
RegisteredClient
的设置中获取用于JWT签名的算法。
- 从
-
处理签名算法(非对称加密):
- 检查JWS算法是否是
SignatureAlgorithm
的实例。 - 获取JWK Set URL,用于验证JWT的签名。
- 如果JWK Set URL为空,抛出
OAuth2AuthenticationException
异常。 - 如果JWK Set URL存在,使用该URL和签名算法创建并返回
NimbusJwtDecoder
实例。
- 检查JWS算法是否是
-
处理MAC算法(对称加密):
- 检查JWS算法是否是
MacAlgorithm
的实例。 - 获取客户端密钥
clientSecret
,用于对称加密。 - 如果客户端密钥为空,抛出
OAuth2AuthenticationException
异常。 - 如果客户端密钥存在,创建
SecretKeySpec
对象,用于对称加密。 - 使用客户端密钥和MAC算法创建并返回
NimbusJwtDecoder
实例。
- 检查JWS算法是否是
-
处理无效的JWS算法:
- 如果JWS算法既不是签名算法也不是MAC算法,抛出
OAuth2AuthenticationException
异常,提示配置无效的JWS算法。
- 如果JWS算法既不是签名算法也不是MAC算法,抛出
以使用常用的HS256签名算法JWT为例,关键在于
JwsAlgorithm jwsAlgorithm = registeredClient.getClientSettings().getTokenEndpointAuthenticationSigningAlgorithm();
会去读取客户端注册配置,获取签名算法:
// 客户端相关配置
ClientSettings clientSettings = ClientSettings.builder()// 是否需要用户授权确认.requireAuthorizationConsent(true)//指定使用client_secret_jwt认证方式时的签名算法.tokenEndpointAuthenticationSigningAlgorithm(MacAlgorithm.HS256).build();
然后在:
SecretKeySpec secretKeySpec = new SecretKeySpec(clientSecret.getBytes(StandardCharsets.UTF_8),JCA_ALGORITHM_MAPPINGS.get(jwsAlgorithm));
中,获取客户端的密钥client-secret
进行JWT解析
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 授权模式的请求流程
-
客户端请求访问受保护的资源:
- 客户端应用程序直接请求受保护的资源,而不涉及用户的浏览器。
-
检查现有的访问令牌:
- 客户端应用程序检查是否已经拥有有效的访问令牌。如果有,则使用该令牌访问资源。
-
请求访问令牌:
- 如果客户端没有有效的访问令牌或令牌已过期,则客户端应用程序向授权服务器请求新的访问令牌。请求路径通常是
/oauth2/token
。 - 这个请求需要包含客户端的凭证(
client_id
和client_secret
)。
- 如果客户端没有有效的访问令牌或令牌已过期,则客户端应用程序向授权服务器请求新的访问令牌。请求路径通常是
-
授权服务器颁发访问令牌:
- 授权服务器验证客户端凭证。如果验证通过,授权服务器会颁发一个新的访问令牌。
-
使用访问令牌访问受保护的资源:
- 客户端应用程序使用获得的访问令牌来访问受保护的资源。
请求示例
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_type
、code
和redirect_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) &¶meters.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包下),针对OAuth2AuthorizationCodeAuthenticationProvider
的authenticate
方法,分段描述其大致认证流程和token生成过程。
认证流程
进入
OAuth2AuthorizationCodeAuthenticationProvider
的authenticate
方法进行认证,以下是其中的核心源码解析
-
转换认证对象:
OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthentication =(OAuth2AuthorizationCodeAuthenticationToken) authentication;
将传入的
authentication
对象转换为OAuth2AuthorizationCodeAuthenticationToken
类型。 -
获取已认证的客户端:
OAuth2ClientAuthenticationToken clientPrincipal =getAuthenticatedClientElseThrowInvalidClient(authorizationCodeAuthentication); RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
调用
getAuthenticatedClientElseThrowInvalidClient
方法,从转换后的请求对象中获取已认证的客户端信息,存入RegisteredClient
对象。 -
验证授权码:
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
表中进行查询。这里实际就是去验证请求中的授权码是否在授权服务中存在,即授权服务是否生成过这个授权码
-
验证授权请求和客户端信息:
// 从授权服务的授权记录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
异常。 -
构建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
主要用与客户端验证用户的身份,大致概括:
-
授权请求:
- 客户端向授权服务器发起包含
openid
scope 的授权请求。 - 用户通过授权服务器认证并同意授权。
- 客户端向授权服务器发起包含
-
授权码交换:
- 授权服务器返回授权码给客户端。
- 客户端使用授权码向授权服务器请求访问令牌和 ID Token。
-
身份验证和用户信息获取:
- 客户端使用 ID Token 验证用户身份。
- 客户端可以使用访问令牌访问用户信息端点获取更多用户信息。
-
资源访问:
- 客户端使用访问令牌访问资源服务器上的受保护资源。
- 资源服务器验证访问令牌的有效性,并允许或拒绝访问。
源码分析:
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;
}
最后步骤
-
无效化授权码:
保证授权码只能用一次
authorization = OAuth2AuthenticationProviderUtils.invalidate(authorization, authorizationCode.getToken()); this.authorizationService.save(authorization);
-
返回认证结果:
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
端点主要用于以下场景:
-
资源服务器验证令牌:
资源服务器接收到客户端的请求,携带一个访问令牌。资源服务器使用/oauth2/introspect
端点来验证该令牌是否有效,以及获取与该令牌相关的信息。这有助于资源服务器确定是否允许访问资源。 -
获取令牌元数据:
客户端或资源服务器可以通过introspect
端点获取令牌的详细信息,如令牌的有效期、作用域、关联的用户等。这样可以根据这些信息做出相应的访问控制决策。
工作流程示例
-
客户端获取访问令牌:
客户端通过授权流程(例如授权码流程、客户端凭证流程等)从授权服务器获取访问令牌。 -
客户端请求资源服务器:
客户端使用该访问令牌向资源服务器请求受保护资源。 -
资源服务器验证令牌:
资源服务器接收到请求后,使用/oauth2/introspect
端点验证令牌的有效性和获取元数据。 -
返回结果:
如果令牌有效且权限足够,资源服务器返回受保护资源给客户端;否则,返回相应的错误信息。
源码解析
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;
authenticationManager
:用于处理认证请求的管理器。tokenIntrospectionEndpointMatcher
:用于匹配 introspection 请求的路径和方法(默认匹配/oauth2/introspect
和 POST 请求)。authenticationConverter
:将 HTTP 请求转换为Authentication
对象的转换器。tokenIntrospectionHttpResponseConverter
:将OAuth2TokenIntrospection
对象转换为 HTTP 响应的消息转换器。errorHttpResponseConverter
:将OAuth2Error
对象转换为 HTTP 错误响应的消息转换器。authenticationSuccessHandler
和authenticationFailureHandler
:处理认证成功和失败的处理器。
构造方法
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();
}
构造方法初始化 authenticationManager
和 tokenIntrospectionEndpointMatcher
,并设置默认的 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);}
}
处理流程:
-
请求匹配:
- 使用
tokenIntrospectionEndpointMatcher
检查请求是否匹配/oauth2/introspect
和 POST 方法。 - 如果不匹配,调用
filterChain.doFilter
继续处理请求链。
- 使用
-
请求转换:
- 使用
authenticationConverter
将请求转换为Authentication
对象(即OAuth2TokenIntrospectionAuthenticationToken
)。
- 使用
-
令牌认证:
- 使用
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
的验证过程包括以下步骤:
- 将传入的
Authentication
对象转换为OAuth2TokenIntrospectionAuthenticationToken
。 - 获取经过认证的客户端身份信息。
- 使用令牌查找对应的授权信息。
- 检查令牌是否处于活跃状态。
- 构建令牌元数据信息。
- 记录日志并返回包含验证结果的
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);
}
处理流程:
- 将
Authentication
对象转换为OAuth2TokenIntrospectionAuthenticationToken
。 - 获取
tokenClaims
,即令牌的元数据信息。 - 使用
tokenIntrospectionHttpResponseConverter
将tokenClaims
写入 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);
}
处理流程
- 从
AuthenticationException
中获取OAuth2Error
对象。 - 设置 HTTP 响应状态为
BAD_REQUEST
(400)。 - 使用
errorHttpResponseConverter
将OAuth2Error
写入 HTTP 响应。
/oauth2/revoke撤销令牌请求
请求作用
在 OAuth 2.0 安全框架中,/oauth2/revoke
是一个用于撤销(revoke)访问令牌或刷新令牌的请求端点。其主要用途是允许客户端或资源所有者明确地使某个令牌失效,从而使其不再能被使用访问受保护的资源。
/oauth2/revoke
请求是 OAuth 2.0 中的重要端点,用于安全撤销访问令牌和刷新令牌。通过它,客户端可以确保在不需要时安全地使令牌失效,提升整个系统的安全性。
主要用途
-
提升安全性:当访问令牌或刷新令牌泄露时,能够立即使其失效,从而防止未经授权的访问。
-
终止会话:当用户在某个客户端应用上登出时,可以使用撤销端点使当前令牌失效,确保用户会话的终止。
-
减少权限:当客户端应用不再需要某个令牌的权限时,可以主动撤销它,减少潜在的滥用风险。
请求和响应格式
请求
/oauth2/revoke
端点通常通过 POST 请求进行访问。请求的主要参数包括:
token
: 必需参数,要被撤销的访问令牌或刷新令牌。token_type_hint
: 可选参数,指示令牌的类型(如access_token
或refresh_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 状态码,以避免泄露令牌信息。
撤销流程
- 验证客户端:授权服务器首先验证发起撤销请求的客户端的身份。通常需要客户端凭据(如 client_id 和 client_secret)。
- 查找令牌:服务器查找请求中指定的令牌。如果提供了
token_type_hint
,会使用该提示来加速查找。 - 撤销令牌:如果找到该令牌,服务器会将其标记为已撤销。对于访问令牌,这通常意味着删除或标记令牌为无效;对于刷新令牌,可能涉及删除相关的所有访问令牌。
- 响应请求:服务器返回 200 状态码,指示撤销请求已被处理。
安全性注意事项
- 客户端验证:确保只有授权客户端能够撤销其发行的令牌。
- 速率限制:防止滥用撤销端点进行拒绝服务(DoS)攻击。
- 最小权限:尽量限制令牌的权限和有效期,以减少被滥用的风险。
源码解析
/oauth2/revoke
请求是由OAuth2TokenRevocationEndpointFilter
过滤器进行处理的
OAuth2TokenRevocationEndpointFilter
过滤器的主要作用是处理/oauth2/revoke
请求,验证请求的有效性,并根据认证结果返回适当的响应。通过匹配请求路径、转换请求、进行认证、处理认证结果,这个过滤器确保了令牌撤销操作的安全性和有效性。
OAuth2TokenRevocationEndpointFilter
是一个用于处理 OAuth 2.0 令牌撤销请求的过滤器。它负责从 /oauth2/revoke
端点接收撤销令牌的请求,验证请求,并处理撤销操作。以下是这个过滤器的原理和请求处理流程的详细讲解。
原理
-
匹配请求:过滤器首先会检查请求是否匹配预期的撤销令牌端点
/oauth2/revoke
。 -
转换请求:将 HTTP 请求转换为
Authentication
对象,以便进行认证处理。 -
认证请求:通过
AuthenticationManager
进行认证。 -
处理结果:根据认证结果,调用相应的成功或失败处理器。
请求处理流程
初始化
过滤器在初始化时设置了以下几个关键组件:
- authenticationManager:处理认证的核心组件。
- tokenRevocationEndpointMatcher:用于匹配撤销令牌的请求路径和方法。
- authenticationConverter:将 HTTP 请求转换为
Authentication
对象的转换器。 - authenticationSuccessHandler 和 authenticationFailureHandler:分别处理成功和失败的认证结果。
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 令牌。在配置过程中,可以通过 OAuth2AuthorizationServerConfigurer
的 tokenGenerator
方法来设置自定义的 OAuth2TokenGenerator
实现。
默认情况下的实现
在默认情况下,Spring Authorization Server 提供了一个默认的 OAuth2TokenGenerator
实现,即 DelegatingOAuth2TokenGenerator
。DelegatingOAuth2TokenGenerator
是一个委派模式的生成器,它会将令牌生成的任务委派给多个具体的令牌生成器。通常,它包含以下几个具体的生成器:
-
JwtGenerator:
用于生成 JWT(JSON Web Token)类型的访问令牌和 ID 令牌。
-
OAuth2AccessTokenGenerator:
用于生成常规的 OAuth2 访问令牌。
-
OAuth2RefreshTokenGenerator:
用于生成 OAuth2 刷新令牌。
配置默认的
OAuth2TokenGenerator
在没有自定义配置的情况下,Spring Authorization Server 会自动配置上述默认的 OAuth2TokenGenerator
实现。这意味着,在默认配置下,this.tokenGenerator.generate(tokenContext)
会使用 DelegatingOAuth2TokenGenerator
来生成令牌,并根据上下文信息选择合适的具体生成器(如 JwtGenerator
、OAuth2AccessTokenGenerator
或 OAuth2RefreshTokenGenerator
)。
默认的配置示例
假设我们没有自定义任何 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
来决定使用哪个具体的生成器:
- 如果
tokenType
是OAuth2TokenType.ACCESS_TOKEN
,它会使用OAuth2AccessTokenGenerator
来生成访问令牌。 - 如果
tokenType
是OAuth2TokenType.REFRESH_TOKEN
,它会使用OAuth2RefreshTokenGenerator
来生成刷新令牌。 - 如果
tokenType
是ID_TOKEN_TOKEN_TYPE
,它会使用JwtGenerator
来生成 ID 令牌。
在OAuth2AuthorizationCodeAuthenticationProvider
的认证方法中,当grant_type
为code
时,会默认指定 tokenType
为 OAuth2TokenType.ACCESS_TOKEN
。
想要生成刷新令牌时,指定grant_type
为refresh_token
即可。
如果需要生成 ID 令牌,需要在 OpenID Connect 的上下文中明确指定 tokenType
为 ID_TOKEN_TOKEN_TYPE
。
自定义
OAuth2TokenGenerator
如果需要自定义 OAuth2TokenGenerator
,可以通过 OAuth2AuthorizationServerConfigurer
的 tokenGenerator
方法进行配置。例如:
@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) 端点。
主要作用:
-
公开 JWKS 端点:
NimbusJwkSetEndpointFilter
负责公开一个 JWKS 端点,通常位于路径/oauth2/jwks
。这个端点用于公开授权服务器的公钥,这些公钥可以被 OAuth 2.0 客户端和资源服务器用来验证来自授权服务器的 JSON Web Token (JWT) 的签名。 -
提供公钥信息:
该过滤器会返回一个包含授权服务器用于签名 JWT 的公钥集合的 JSON 响应。这些公钥是使用 JSON Web Key (JWK) 格式表示的,客户端和资源服务器可以从中提取并使用相应的公钥来验证签名。 -
与 Nimbus 实现集成:
该过滤器通常与 Nimbus JOSE + JWT 库集成,用于处理 JWT 的签名和验证。Nimbus 是一个广泛使用的开源库,用于处理 JOSE (JSON Object Signing and Encryption) 和 JWT。 -
保证安全性:
通过公开的 JWKS 端点,授权服务器可以安全地与其他服务共享公钥,而无需直接暴露私钥。客户端和资源服务器通过访问该端点,可以获取最新的公钥,用于验证从授权服务器收到的 JWT 签名。
工作流程:
-
JWT 签名和验证:
授权服务器生成的 JWT 通常是使用私钥进行签名的。客户端和资源服务器需要知道相应的公钥才能验证 JWT 的签名,确保 JWT 是由授权服务器签发的,并且未被篡改。 -
JWKS 端点的作用:
NimbusJwkSetEndpointFilter
处理到 JWKS 端点的 HTTP 请求,当收到请求时,它返回授权服务器的公钥集合。这个公钥集合以 JSON Web Key Set (JWKS) 格式表示,包含多个 JSON Web Key (JWK) 对象。 -
响应结构:
该过滤器生成的响应是一个 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
的:
-
首先检查共享对象池:如上所述,代码首先使用
getSharedObject(JWKSource.class)
检查HttpSecurity
是否已经包含了一个JWKSource
对象。 -
尝试从 Spring 容器获取:如果共享对象池中没有找到
JWKSource
,则通过getOptionalBean(httpSecurity, type)
尝试从 Spring 的ApplicationContext
中获取JWKSource
Bean。 -
设置共享对象:如果成功获取到
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 时非常常见,分别适用于不同的安全需求。