本文为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-urlencoded client_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 Y2xpZW50SWQ6Y2xpZW50U2VjcmV0 grant_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-urlencoded
client_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 { @Override public 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 { @Override public 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;
//默认false
private 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 AuthenticationEntryPoint
filterChain.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属性为true
return 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 false
return 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 { @Override protected 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_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);
}
}
重定向执行
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());
//构建请求url
String 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`的其他参数作为附加参数存入map
Map<String, Object> additionalParameters = new HashMap<>();
parameters.forEach((key, value) -> {
if (!key.equals(OAuth2ParameterNames.RESPONSE_TYPE) &&
!key.equals(OAuth2ParameterNames.CLIENT_ID) &&
!key.equals(OAuth2ParameterNames.REDIRECT_URI) &&
!key.equals(OAuth2ParameterNames.SCOPE) &&
!key.equals(OAuth2ParameterNames.STATE)) {
additionalParameters.put(key, value.get(0));
}
});
// 返回封装后的认证对象
// 将提取的参数和信息封装成一个 `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 false
return 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,否则为null
Set<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 作为授权 URI
String 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 consent
authorizedScopes.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:off
OAuth2AuthorizationConsentAuthenticationContext authorizationConsentAuthenticationContext =
OAuth2AuthorizationConsentAuthenticationContext.with(authorizationConsentAuthentication)
.authorizationConsent(authorizationConsentBuilder)
.registeredClient(registeredClient)
.authorization(authorization)
.authorizationRequest(authorizationRequest)
.build();
// @formatter:on
this.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) 保存两种自带实现,默认内存
@Resource
private 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} 弃用
*/
@Deprecated
public static final ClientAuthenticationMethod BASIC = new ClientAuthenticationMethod("basic");
/**
* @since 5.5
*/
public static final ClientAuthenticationMethod CLIENT_SECRET_BASIC = new ClientAuthenticationMethod(
"client_secret_basic");
/**
* @deprecated Use {@link #CLIENT_SECRET_POST} 弃用
*/
@Deprecated
public static final ClientAuthenticationMethod POST = new ClientAuthenticationMethod("post");
/**
* @since 5.5
*/
public static final ClientAuthenticationMethod CLIENT_SECRET_POST = new ClientAuthenticationMethod(
"client_secret_post");
/**
* @since 5.5
*/
public static final ClientAuthenticationMethod CLIENT_SECRET_JWT = new ClientAuthenticationMethod(
"client_secret_jwt");
/**
* @since 5.5
*/
public static final ClientAuthenticationMethod PRIVATE_KEY_JWT = new ClientAuthenticationMethod("private_key_jwt");
/**
* @since 5.2
*/
public static final ClientAuthenticationMethod NONE = new ClientAuthenticationMethod("none");
}
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-urlencoded
client_id=your-client-id&
client_secret=your-client-secret&
grant_type=authorization_code&
code=authorization-code&
redirect_uri=your-redirect-uri
关键点
- 必要参数:
client_id
:客户端 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-urlencoded
client_id=your-client-id&
code_verifier=your-code-verifier&
grant_type=authorization_code&
code=authorization-code&
redirect_uri=your-redirect-uri
关键点
- 必要参数:
client_id
:客户端 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/token
new AntPathRequestMatcher(
authorizationServerSettings.getTokenEndpoint(),
HttpMethod.POST.name()),
// 匹配/oauth2/introspect
new AntPathRequestMatcher(
authorizationServerSettings.getTokenIntrospectionEndpoint(),
HttpMethod.POST.name()),
// 匹配/oauth2/revoke
new AntPathRequestMatcher(
authorizationServerSettings.getTokenRevocationEndpoint(),
HttpMethod.POST.name()));
List<AuthenticationProvider> authenticationProviders = createDefaultAuthenticationProviders(httpSecurity);
if (!this.authenticationProviders.isEmpty()) {
authenticationProviders.addAll(0, this.authenticationProviders);
}
this.authenticationProvidersConsumer.accept(authenticationProviders);
authenticationProviders.forEach(authenticationProvider ->
httpSecurity.authenticationProvider(postProcess(authenticationProvider)));
}
总结,OAuth2ClientAuthenticationFilter的处理大体分为三步:
authenticationConverter
将过滤的请求转为Authentication
认证对象- 使用
authenticationManager
进行认证 - 使用
Handler
做认证成功或失败的处理,如果成功则向下执行其他过滤器
根据委托设计模式,authenticationConverter会将不同类型的请求转为不同的认证对象,authenticationManager又会根据不同类型的认证对象,使用不同的Provider进行认证
client_secret_basic认证源码解析
请求转换器
ClientSecretBasicAuthenticationConverter
请求处理流程
- 接收请求:客户端发送 POST 请求到授权服务器的
/oauth2/token
端点,包含所需的头部和参数。 - 提取头部:
ClientSecretBasicAuthenticationConverter
从请求中提取Authorization
头部。 - 验证头部:检查头部是否存在,且类型是否为
Basic
。 - 解码凭证:将 Base64 编码的凭证部分解码为用户名和密码。
- 验证凭证:检查凭证是否包含用户名和密码两个部分,且不为空。
- 创建认证对象:如果所有检查通过,创建一个
OAuth2ClientAuthenticationToken
对象,并填充相应的参数和附加参数。 - 返回认证对象:返回生成的认证对象供后续使用。
源码解析
public final class ClientSecretBasicAuthenticationConverter implements AuthenticationConverter {
@Nullable
@Override
public Authentication convert(HttpServletRequest request) {
//取出请求头中的Authorization参数值,如果为空返回null
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
if (header == null) {
return null;
}
//斜杠小写s正则匹配的是不可见字符,包括空格、制表符、换页符等,
//这里就是按空格拆分Authorization参数值
String[] parts = header.split("\\s");
//如果拆分出来的第一个值在忽略大小写情况下不是Basic,直接结束方法返回null,
//从此处看出这个转换器匹配的是请求头Authorization参数值为'Basic ***'、携带未加密用户名密码的
if (!parts[0].equalsIgnoreCase("Basic")) {
return null;
}
//拆分完的Authorization参数值如果不是2个,直接抛出invalid_request异常
if (parts.length != 2) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
}
//解析Authorization参数值两段中的第二段,先转为utf-8字节,再用Base64解码,解析失败则抛出invalid_request异常
byte[] decodedCredentials;
try {
decodedCredentials = Base64.getDecoder().decode(
parts[1].getBytes(StandardCharsets.UTF_8));
} catch (IllegalArgumentException ex) {
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST), ex);
}
//将解码后的凭证转换为字符串,并按 : 分割成用户名和密码。
//检查分割后的数组是否包含用户名和密码两个部分,并且两部分内容都不为空。
//如果不满足上面这些条件,抛出 OAuth2AuthenticationException 异常,表示请求无效。
String credentialsString = new String(decodedCredentials, StandardCharsets.UTF_8);
String[] credentials = credentialsString.split(":", 2);
if (credentials.length != 2 ||
!StringUtils.hasText(credentials[0]) ||
!StringUtils.hasText(credentials[1])) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
}
//尝试解码用户名和密码部分。如果解码失败,抛出 OAuth2AuthenticationException 异常,表示请求无效。
String clientID;
String clientSecret;
try {
clientID = URLDecoder.decode(credentials[0], StandardCharsets.UTF_8.name());
clientSecret = URLDecoder.decode(credentials[1], StandardCharsets.UTF_8.name());
} catch (Exception ex) {
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST), ex);
}
//如果解码成功,创建一个新的 OAuth2ClientAuthenticationToken 权限对象,
//并将客户端 ID、认证方法(CLIENT_SECRET_BASIC)和客户端密钥作为参数传入。
return new OAuth2ClientAuthenticationToken(clientID, ClientAuthenticationMethod.CLIENT_SECRET_BASIC, clientSecret,
OAuth2EndpointUtils.getParametersIfMatchesAuthorizationCodeGrantRequest(request));
}
}
请求验证
ClientSecretAuthenticationProvider
这段代码是
ClientSecretAuthenticationProvider
类中的authenticate
方法,用于处理客户端使用client_secret_basic
或client_secret_post
方法进行认证的逻辑。以下是逐行解释:
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 将传入的Authentication对象转换为OAuth2ClientAuthenticationToken对象
OAuth2ClientAuthenticationToken clientAuthentication =
(OAuth2ClientAuthenticationToken) authentication;
// 检查客户端的认证方法是否为client_secret_basic或client_secret_post
if (!ClientAuthenticationMethod.CLIENT_SECRET_BASIC.equals(clientAuthentication.getClientAuthenticationMethod()) &&
!ClientAuthenticationMethod.CLIENT_SECRET_POST.equals(clientAuthentication.getClientAuthenticationMethod())) {
return null;
}
// 获取客户端ID
String clientId = clientAuthentication.getPrincipal().toString();
// 从存储库中查找已注册的客户端信息
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
if (registeredClient == null) {
throwInvalidClient(OAuth2ParameterNames.CLIENT_ID);
}
// 如果启用跟踪日志,则记录已检索到的客户端信息
if (this.logger.isTraceEnabled()) {
this.logger.trace("Retrieved registered client");
}
// 检查客户端注册信息中是否包含当前使用的认证方法
if (!registeredClient.getClientAuthenticationMethods().contains(
clientAuthentication.getClientAuthenticationMethod())) {
throwInvalidClient("authentication_method");
}
// 检查客户端凭据是否为空
if (clientAuthentication.getCredentials() == null) {
throwInvalidClient("credentials");
}
// 获取客户端密钥
String clientSecret = clientAuthentication.getCredentials().toString();
// 验证客户端密钥是否匹配,使用委托模式调用DelegatingPasswordEncoder来进行对比
if (!this.passwordEncoder.matches(clientSecret, registeredClient.getClientSecret())) {
throwInvalidClient(OAuth2ParameterNames.CLIENT_SECRET);
}
// 检查客户端密钥是否过期
if (registeredClient.getClientSecretExpiresAt() != null &&
Instant.now().isAfter(registeredClient.getClientSecretExpiresAt())) {
throwInvalidClient("client_secret_expires_at");
}
// 如果启用跟踪日志,则记录已验证的客户端认证参数
if (this.logger.isTraceEnabled()) {
this.logger.trace("Validated client authentication parameters");
}
// 验证保密客户端的“code_verifier”参数(如果可用)
this.codeVerifierAuthenticator.authenticateIfAvailable(clientAuthentication, registeredClient);
// 如果启用跟踪日志,则记录已认证的客户端密钥
if (this.logger.isTraceEnabled()) {
this.logger.trace("Authenticated client secret");
}
// 返回新的OAuth2ClientAuthenticationToken,表示认证成功
return new OAuth2ClientAuthenticationToken(registeredClient,
clientAuthentication.getClientAuthenticationMethod(), clientAuthentication.getCredentials());
}
源码流程概括
-
转换认证对象:
- 将传入的
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
@Override
public Authentication convert(HttpServletRequest request) {
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
// 取出请求中携带的client_id参数值,如果为空返回null
String clientId = parameters.getFirst(OAuth2ParameterNames.CLIENT_ID);
if (!StringUtils.hasText(clientId)) {
return null;
}
// client_id参数值只能是1个,否则抛出invalid_request异常
if (parameters.get(OAuth2ParameterNames.CLIENT_ID).size() != 1) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
}
// 取出请求中携带的client_secret参数值,如果为空返回null
String clientSecret = parameters.getFirst(OAuth2ParameterNames.CLIENT_SECRET);
if (!StringUtils.hasText(clientSecret)) {
return null;
}
// client_secret参数值只能是1个,否则抛出invalid_request异常
if (parameters.get(OAuth2ParameterNames.CLIENT_SECRET).size() != 1) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
}
// 获取其他请求参数,这些参数必须匹配授权码授权请求的格式,并排除 client_id 和 client_secret 参数
Map<String, Object> additionalParameters = OAuth2EndpointUtils.getParametersIfMatchesAuthorizationCodeGrantRequest(request,
OAuth2ParameterNames.CLIENT_ID,
OAuth2ParameterNames.CLIENT_SECRET);
// 创建权限对象并返回,其中包含客户端 ID、认证方法(CLIENT_SECRET_POST)、客户端密钥和额外的参数。
return new OAuth2ClientAuthenticationToken(clientId, ClientAuthenticationMethod.CLIENT_SECRET_POST, clientSecret,
additionalParameters);
}
}
认证器
见上面的ClientSecretAuthenticationProvider
client_secret_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
@Override
public Authentication convert(HttpServletRequest request) {
// 请求必须携带code_verifier参数且不为null;
// 请求的grant_type参数值必须是'authorization_code',且code参数不能为空。
// 即:检查请求是否匹配 PKCE 令牌请求。如果请求不匹配,则返回 null,表示无法进行转换。
if (!OAuth2EndpointUtils.matchesPkceTokenRequest(request)) {
return null;
}
//获取请求中的所有参数及其值
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
//获取 client_id 参数,并检查它是否为空。
//如果为空或者 client_id 参数的值不唯一,则抛出 invalid_request异常,表示请求无效
String clientId = parameters.getFirst(OAuth2ParameterNames.CLIENT_ID);
if (!StringUtils.hasText(clientId) ||
parameters.get(OAuth2ParameterNames.CLIENT_ID).size() != 1) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
}
// code_verifier必须不为空且必须只有1个值
if (parameters.get(PkceParameterNames.CODE_VERIFIER).size() != 1) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
}
// 从请求中移除client_id
//从参数列表中移除client_id参数目的是为了确保在创建 OAuth2ClientAuthenticationToken 对象时不会包含此参数。
parameters.remove(OAuth2ParameterNames.CLIENT_ID);
// 创建权限对象并返回,其中包含客户端 ID、认证方法(ClientAuthenticationMethod.NONE)、客户端密钥(此处为 null)和额外的参数
return new OAuth2ClientAuthenticationToken(clientId, ClientAuthenticationMethod.NONE, null,
new HashMap<>(parameters.toSingleValueMap()));
}
}
认证器
认证类:
PublicClientAuthenticationProvider
:
public final class PublicClientAuthenticationProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
OAuth2ClientAuthenticationToken clientAuthentication =
(OAuth2ClientAuthenticationToken) authentication;
//检查客户端的身份验证方法是否为 NONE。如果不是,返回 null,表示该 AuthenticationProvider 无法处理此请求。
if (!ClientAuthenticationMethod.NONE.equals(clientAuthentication.getClientAuthenticationMethod())) {
return null;
}
//获取 clientId,并从 registeredClientRepository 中查找相应的注册客户端。如果未找到,抛出 INVALID_CLIENT 错误。
String clientId = clientAuthentication.getPrincipal().toString();
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
if (registeredClient == null) {
throwInvalidClient(OAuth2ParameterNames.CLIENT_ID);
}
if (this.logger.isTraceEnabled()) {
this.logger.trace("Retrieved registered client");
}
//检查注册客户端是否支持方法传入authentication中指定的身份验证方法。如果不支持,抛出 INVALID_CLIENT 错误
if (!registeredClient.getClientAuthenticationMethods().contains(
clientAuthentication.getClientAuthenticationMethod())) {
throwInvalidClient("authentication_method");
}
if (this.logger.isTraceEnabled()) {
this.logger.trace("Validated client authentication parameters");
}
//调用 codeVerifierAuthenticator 的 authenticateRequired 方法,验证公共客户端的 code_verifier 参数
this.codeVerifierAuthenticator.authenticateRequired(clientAuthentication, registeredClient);
if (this.logger.isTraceEnabled()) {
this.logger.trace("Authenticated public client");
}
//创建并返回一个新的 OAuth2ClientAuthenticationToken,其中包含已注册的客户端、客户端身份验证方法以及 null 凭据
return new OAuth2ClientAuthenticationToken(registeredClient,
clientAuthentication.getClientAuthenticationMethod(), null);
}
}
上面的关键之处:
this.codeVerifierAuthenticator.authenticateRequired(clientAuthentication, registeredClient);
会调用CodeVerifierAuthenticator
的authenticate
对CODE_CHALLENGE
及CODE_VERIFIER
进行认证:
final class CodeVerifierAuthenticator {
private boolean authenticate(OAuth2ClientAuthenticationToken clientAuthentication,
RegisteredClient registeredClient) {
//获取客户端身份验证请求中的附加参数,并检查该请求是否为授权码类型。如果不是,返回 false
Map<String, Object> parameters = clientAuthentication.getAdditionalParameters();
if (!authorizationCodeGrant(parameters)) {
return false;
}
//使用附加参数中的授权码从 authorizationService 查找相应的授权信息。如果找不到,抛出 INVALID_GRANT 错误
//这里的authorizationService对应之前在OAuth2AuthorizationCodeRequestAuthenticationProvider中保存的客户端授权记录,里面存有客户端请求授权码时传过来的code_challenge
OAuth2Authorization authorization = this.authorizationService.findByToken(
(String) parameters.get(OAuth2ParameterNames.CODE),
AUTHORIZATION_CODE_TOKEN_TYPE);
if (authorization == null) {
throwInvalidGrant(OAuth2ParameterNames.CODE);
}
//如果日志记录级别设置为 TRACE,记录一条日志,表示已检索到授权信息。
if (this.logger.isTraceEnabled()) {
this.logger.trace("Retrieved authorization with authorization code");
}
//从authorizationService中取出的授权信息中获取授权请求
OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(
OAuth2AuthorizationRequest.class.getName());
//从授权请求中提取 code_challenge。
//如果 code_challenge 为空且注册客户端要求使用 Proof Key,则抛出 INVALID_GRANT 错误。
//如果 code_challenge 为空且不要求 Proof Key,记录日志并返回 false
String codeChallenge = (String) authorizationRequest.getAdditionalParameters()
.get(PkceParameterNames.CODE_CHALLENGE);
if (!StringUtils.hasText(codeChallenge)) {
if (registeredClient.getClientSettings().isRequireProofKey()) {
throwInvalidGrant(PkceParameterNames.CODE_CHALLENGE);
} else {
if (this.logger.isTraceEnabled()) {
this.logger.trace("Did not authenticate code verifier since requireProofKey=false");
}
return false;
}
}
//如果日志记录级别设置为 TRACE,记录一条日志,表示已验证 code_verifier 参数
if (this.logger.isTraceEnabled()) {
this.logger.trace("Validated code verifier parameters");
}
//从授权请求的附加参数中获取 code_challenge_method, 即加密方法
String codeChallengeMethod = (String) authorizationRequest.getAdditionalParameters()
.get(PkceParameterNames.CODE_CHALLENGE_METHOD);
//从客户端身份验证请求的附加参数中获取 code_verifier
String codeVerifier = (String) parameters.get(PkceParameterNames.CODE_VERIFIER);
//调用 codeVerifierValid 方法验证 code_verifier 是否有效。如果无效,抛出 INVALID_GRANT 错误
//使用SHA-256的算法对code_verifier进行哈希运算,将运算结果与code_challenge对比
if (!codeVerifierValid(codeVerifier, codeChallenge, codeChallengeMethod)) {
throwInvalidGrant(PkceParameterNames.CODE_VERIFIER);
}
//如果日志记录级别设置为 TRACE,记录一条日志,表示已认证 code_verifierV
if (this.logger.isTraceEnabled()) {
this.logger.trace("Authenticated code verifier");
}
//如果所有检查都通过,返回 true,表示认证成功。
return true;
}
}
PKCE的关键验证为使用SHA-256算法加密进行对比认证,这里取出之前客户端请求授权码时保存的
code_challenge
,与此次发来的code_verifier
运算后的结果进行对比
上面代码的codeVerifierValid
对比方法源码
private static boolean codeVerifierValid(String codeVerifier, String codeChallenge, String codeChallengeMethod) {
if (!StringUtils.hasText(codeVerifier)) {
return false;
} else if ("S256".equals(codeChallengeMethod)) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] digest = md.digest(codeVerifier.getBytes(StandardCharsets.US_ASCII));
String encodedVerifier = Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
return encodedVerifier.equals(codeChallenge);
} catch (NoSuchAlgorithmException ex) {
// It is unlikely that SHA-256 is not available on the server. If it is not available,
// there will likely be bigger issues as well. We default to SERVER_ERROR.
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.SERVER_ERROR);
}
}
return false;
}
返回true则对比一致,客户端认证通过,交由OAuth2TokenEndpointFilter
做后续处理,PKCE认证结束。
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
@Override
public Authentication convert(HttpServletRequest request) {
//如果请求中取不到client_assertion_type或client_assertion参数,转换方法返回空
if (request.getParameter(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE) == null ||
request.getParameter(OAuth2ParameterNames.CLIENT_ASSERTION) == null) {
return null;
}
//获取请求中的所有参数,存入map
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
// 请求必须带有client_assertion_type参数,且其值只能是一个,否则抛出invalid_request异常
String clientAssertionType = parameters.getFirst(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE);
if (parameters.get(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE).size() != 1) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
}
// 请求中client_assertion_type属性的值如果不是'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'就返回null
if (!JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD.getValue().equals(clientAssertionType)) {
return null;
}
// 请求必须带有client_assertion参数,且其值只能是一个,否则抛出invalid_request异常
String jwtAssertion = parameters.getFirst(OAuth2ParameterNames.CLIENT_ASSERTION);
if (parameters.get(OAuth2ParameterNames.CLIENT_ASSERTION).size() != 1) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
}
// 如果请求中携带了client_id参数,其值必须是一个,否则抛出invalid_request异常
String clientId = parameters.getFirst(OAuth2ParameterNames.CLIENT_ID);
if (!StringUtils.hasText(clientId) ||
parameters.get(OAuth2ParameterNames.CLIENT_ID).size() != 1) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
}
// 获取请求中除了client_assertion_type、client_assertion、client_id之外的参数值存入additionalParameters
Map<String, Object> additionalParameters = OAuth2EndpointUtils.getParametersIfMatchesAuthorizationCodeGrantRequest(request,
OAuth2ParameterNames.CLIENT_ASSERTION_TYPE,
OAuth2ParameterNames.CLIENT_ASSERTION,
OAuth2ParameterNames.CLIENT_ID);
// 结合验证过的请求参数创建权限对象并返回
return new OAuth2ClientAuthenticationToken(clientId, JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD,
jwtAssertion, additionalParameters);
}
}
委托模式执行转换获取结果
在OAuth2ClientAuthenticationFilter
过滤器的doFilterInternal
方法中,如下代码会通过委托模式调用转换器来获取认证对象
Authentication authenticationRequest = this.authenticationConverter.convert(request);
委托模式的实现类DelegatingAuthenticationConverter
获取实际转换器并返回认证对象
@Nullable
@Override
public Authentication convert(HttpServletRequest request) {
Assert.notNull(request, "request cannot be null");
//循环所有的converter实现,那个能转换成功,就返回那个成功的结果
for (AuthenticationConverter converter : this.converters) {
Authentication authentication = converter.convert(request);
if (authentication != null) {
return authentication;
}
}
return null;
}
通过上面源码分析,如果请求包含client_assertion_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客户端断言认证,则返回null
if (!JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD.equals(clientAuthentication.getClientAuthenticationMethod())) {
return null;
}
// 获取客户端ID
String clientId = clientAuthentication.getPrincipal().toString();
// 根据客户端ID查找注册的客户端
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
if (registeredClient == null) {
// 如果找不到注册的客户端,则抛出异常
throwInvalidClient(OAuth2ParameterNames.CLIENT_ID);
}
// 如果日志级别为trace,则记录日志
if (this.logger.isTraceEnabled()) {
this.logger.trace("Retrieved registered client");
}
// 检查客户端是否支持PRIVATE_KEY_JWT或CLIENT_SECRET_JWT认证方法
if (!registeredClient.getClientAuthenticationMethods().contains(ClientAuthenticationMethod.PRIVATE_KEY_JWT) &&
!registeredClient.getClientAuthenticationMethods().contains(ClientAuthenticationMethod.CLIENT_SECRET_JWT)) {
// 如果不支持,则抛出异常
throwInvalidClient("authentication_method");
}
// 检查客户端凭据是否为空
if (clientAuthentication.getCredentials() == null) {
// 如果为空,则抛出异常
throwInvalidClient("credentials");
}
// 初始化Jwt对象
Jwt jwtAssertion = null;
// 创建JwtDecoder对象,已通过构造方法指定为JwtClientAssertionDecoderFactory
JwtDecoder jwtDecoder = this.jwtDecoderFactory.createDecoder(registeredClient);
try {
// 使用JwtDecoder解码客户端凭据
jwtAssertion = jwtDecoder.decode(clientAuthentication.getCredentials().toString());
} catch (JwtException ex) {
// 如果解码失败,则抛出异常
throwInvalidClient(OAuth2ParameterNames.CLIENT_ASSERTION, ex);
}
// 如果日志级别为trace,则记录日志
if (this.logger.isTraceEnabled()) {
this.logger.trace("Validated client authentication parameters");
}
// 验证机密客户端的"code_verifier"参数,如果可用
this.codeVerifierAuthenticator.authenticateIfAvailable(clientAuthentication, registeredClient);
// 确定客户端认证方法
ClientAuthenticationMethod clientAuthenticationMethod =
registeredClient.getClientSettings().getTokenEndpointAuthenticationSigningAlgorithm() instanceof SignatureAlgorithm ?
ClientAuthenticationMethod.PRIVATE_KEY_JWT :
ClientAuthenticationMethod.CLIENT_SECRET_JWT;
// 如果日志级别为trace,则记录日志
if (this.logger.isTraceEnabled()) {
this.logger.trace("Authenticated client assertion");
}
// 返回新的OAuth2ClientAuthenticationToken对象,其中包含已验证的客户端和JWT断言
return new OAuth2ClientAuthenticationToken(registeredClient, clientAuthenticationMethod, jwtAssertion);
}
代码功能概述
-
类型转换和方法检查: 首先将传入的
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 URL
String jwkSetUrl = registeredClient.getClientSettings().getJwkSetUrl();
// 如果JWK Set URL为空,则抛出异常
if (!StringUtils.hasText(jwkSetUrl)) {
OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT,
"Failed to find a Signature Verifier for Client: '"
+ registeredClient.getId()
+ "'. Check to ensure you have configured the JWK Set URL.",
JWT_CLIENT_AUTHENTICATION_ERROR_URI);
throw new OAuth2AuthenticationException(oauth2Error);
}
// 使用JWK Set URL和签名算法创建并返回NimbusJwtDecoder
return NimbusJwtDecoder.withJwkSetUri(jwkSetUrl).jwsAlgorithm((SignatureAlgorithm) jwsAlgorithm).build();
}
// 如果JWS算法是MAC算法(对称加密)
if (jwsAlgorithm instanceof MacAlgorithm) {
// 获取客户端密钥
String clientSecret = registeredClient.getClientSecret();
// 如果客户端密钥为空,则抛出异常
if (!StringUtils.hasText(clientSecret)) {
OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT,
"Failed to find a Signature Verifier for Client: '"
+ registeredClient.getId()
+ "'. Check to ensure you have configured the client secret.",
JWT_CLIENT_AUTHENTICATION_ERROR_URI);
throw new OAuth2AuthenticationException(oauth2Error);
}
// 创建SecretKeySpec,用于对称加密
SecretKeySpec secretKeySpec = new SecretKeySpec(clientSecret.getBytes(StandardCharsets.UTF_8),
JCA_ALGORITHM_MAPPINGS.get(jwsAlgorithm));
// 使用客户端密钥和MAC算法创建并返回NimbusJwtDecoder
return NimbusJwtDecoder.withSecretKey(secretKeySpec).macAlgorithm((MacAlgorithm) jwsAlgorithm).build();
}
// 如果JWS算法既不是签名算法也不是MAC算法,则抛出异常
OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT,
"Failed to find a Signature Verifier for Client: '"
+ registeredClient.getId()
+ "'. Check to ensure you have configured a valid JWS Algorithm: '" + jwsAlgorithm + "'.",
JWT_CLIENT_AUTHENTICATION_ERROR_URI);
throw new OAuth2AuthenticationException(oauth2Error);
}
关键点解释
-
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-urlencoded
grant_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-urlencoded
grant_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-urlencoded
grant_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` POST
if (!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
@Override
public 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();
// 获取所有请求参数存入map
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
// code (REQUIRED)
String code = parameters.getFirst(OAuth2ParameterNames.CODE);
// 检查 `code` 参数是否存在且仅有一个值,如果不符合条件则抛出异常
if (!StringUtils.hasText(code) ||
parameters.get(OAuth2ParameterNames.CODE).size() != 1) {
OAuth2EndpointUtils.throwError(
OAuth2ErrorCodes.INVALID_REQUEST,
OAuth2ParameterNames.CODE,
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
}
// 检查 `redirect_uri` 参数是否存在且仅有一个值,只有当 `redirect_uri` 在请求中存在时才进行检查
String redirectUri = parameters.getFirst(OAuth2ParameterNames.REDIRECT_URI);
if (StringUtils.hasText(redirectUri) &&
parameters.get(OAuth2ParameterNames.REDIRECT_URI).size() != 1) {
OAuth2EndpointUtils.throwError(
OAuth2ErrorCodes.INVALID_REQUEST,
OAuth2ParameterNames.REDIRECT_URI,
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
}
// 处理附加参数
//将其他的请求参数(除了grant_type、client_id、code和redirect_uri)存储在map中。
Map<String, Object> additionalParameters = new HashMap<>();
parameters.forEach((key, value) -> {
if (!key.equals(OAuth2ParameterNames.GRANT_TYPE) &&
!key.equals(OAuth2ParameterNames.CLIENT_ID) &&
!key.equals(OAuth2ParameterNames.CODE) &&
!key.equals(OAuth2ParameterNames.REDIRECT_URI)) {
additionalParameters.put(key, value.get(0));
}
});
// 创建并返回一个包含授权码、客户端认证信息、重定向 URI 和附加参数的对象回过滤器
return new OAuth2AuthorizationCodeAuthenticationToken(
code, clientPrincipal, redirectUri, additionalParameters);
}
}
授权码验证请求认证源码解析
OAuth2AuthorizationCodeAuthenticationProvider
会对OAuth2AuthorizationCodeAuthenticationConverter
转换过来的对象进行认证
下面是OAuth2AuthorizationCodeAuthenticationProvider
的源码解释(org.springframework.security.oauth2.server.authorization.authentication包下),针对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 {
@Override
public 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 {
@Override
public 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);
//获取并配置JWS
JWKSource<com.nimbusds.jose.proc.SecurityContext> jwkSource = OAuth2ConfigurerUtils.getJwkSource(httpSecurity);
if (jwkSource != null) {
//通过构造方法构建NimbusJwkSetEndpointFilter,为其指定JWKSource
NimbusJwkSetEndpointFilter 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();
// 构建JWK
RSAKey rsaKey = new RSAKey.Builder(keyPair.getPublic())
.privateKey(keyPair.getPrivate())
.keyID("rsa-key")
.build();
// 构建JWKSet
JWKSet 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==");
// 构建JWK
OctetSequenceKey octetSequenceKey = new OctetSequenceKey.Builder(secret)
.keyID("hmac-key")
.build();
// 构建JWKSet
JWKSet jwkSet = new JWKSet(octetSequenceKey);
// 返回JWKSource实例
return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}
解释
-
非对称密钥: 使用 RSA 密钥对来生成 JWK。
RSAKey
代表 RSA 密钥对,包含公钥和私钥。这个配置适用于需要使用公钥加密和私钥解密的场景,比如签名验证。 -
对称密钥: 使用一个随机生成的对称密钥来生成 JWK。
OctetSequenceKey
代表对称密钥。这个配置适用于 HMAC 签名或 AES 加密的场景。
这两种配置方式在使用 Spring Security 的 OAuth2.0 或 JWT 时非常常见,分别适用于不同的安全需求。