主要分析spring-security-oauth2-client的源码,介绍OAuth2.0授权码模式下Spring Boot OAuth2客户端的运行流程
代码版本信息
Spring Boot 2.7.10
spring-security-oauth2-client 5.7.7
流程
在 Spring Security OAuth2 Client 中,多个过滤器协同工作以处理 OAuth2 登录流程和资源保护。这些过滤器的执行顺序至关重要,以确保正确地处理认证和授权请求。以下是这些过滤器的主要顺序及其作用:
主要过滤器及其顺序
OAuth2AuthorizationRequestRedirectFilter
:- 触发条件:用户访问受保护资源但尚未认证。
- 作用:构建并发送 OAuth2 授权请求
/oauth2/authorize
,重定向用户到授权服务器进行认证。
OAuth2LoginAuthenticationFilter
:- 触发条件:授权服务器重定向回客户端应用,并附带授权码。
- 作用:处理授权服务器的重定向请求,提取授权码并交换访问令牌,完成用户认证。
OAuth2AuthorizationCodeGrantFilter
:- 作用:处理授权码授权流程,获取访问令牌和刷新令牌。
- 作用:处理授权码授权流程,获取访问令牌和刷新令牌。
客户端访问受保护资源的流程
假设客户端应用配置了 OAuth2 登录,并且用户尝试访问受保护资源但尚未认证。以下是详细的请求流程:
1. 用户访问受保护资源
- 用户尝试访问受保护资源 URL,如
https://client-app.com/protected-resource
。 - Spring Security 检测到用户未认证,抛出
Access Denied
异常:当已经认证的用户尝试访问他们无权访问的资源时,会触发AccessDeniedException
。这意味着用户已经通过身份验证,但其权限不足以访问特定资源。 ExceptionTranslationFilter
过滤器会捕获Access Denied
异常,在缓存中保存此次未通过的资源请求(默认使用session保存),并调用AuthenticationEntryPoint
的commence
方法。- 在 OAuth2 的场景下,
AuthenticationEntryPoint
通常是LoginUrlAuthenticationEntryPoint
。 - 触发
LoginUrlAuthenticationEntryPoint
的commence
方法,该方法会发起重定向到/oauth2/authorization/{registrationId}
(此 URL 是 OAuth2 客户端配置的授权请求路径),即发起/oauth2/authorization
请求。 /oauth2/authorization
请求会被OAuth2AuthorizationRequestRedirectFilter
过滤器进行处理。
2. 触发 OAuth2AuthorizationRequestRedirectFilter
OAuth2AuthorizationRequestRedirectFilter
拦截到/oauth2/authorization/{registrationId}
请求,构建 OAuth2 授权请求。- 授权请求包括客户端 ID、重定向 URI、请求的权限范围等信息。
OAuth2AuthorizationRequestRedirectFilter
重定向用户到授权服务器的授权端点,发起向服务端进行认证的/oauth2/authorize
请求,URL 形如https://auth-server.com/oauth2/authorize?response_type=code&client_id=client-id&redirect_uri=https://client-app.com/login/oauth2/code/registration-id&scope=read
。
3. 用户在授权服务器上进行认证和授权
- 上一步发起的
/oauth2/authorize
请求被OAuth2 Server授权服务的OAuth2AuthorizationEndpointFilter
过滤器处理 - 用户在授权服务器
OAuth2AuthorizationEndpointFilter
过滤器处理过程中,进行登录并同意授权。
4. 授权服务器重定向回客户端应用
- 上一步授权服务器处理用户的认证登录和同意授权后,
OAuth2AuthorizationEndpointFilter
过滤器向客户端应用的回调 URI 发起重定向。 - 回调 URI 形如
http://client-app.com/login/oauth2/code/registration-id?code=auth-code&state=xyz
,此URL可由客户端程序的yaml进行配置,一般都是/login/oauth2/code
- Spring Security 中的
OAuth2LoginAuthenticationFilter
拦截这个回调请求。
5. 触发 OAuth2LoginAuthenticationFilter
OAuth2LoginAuthenticationFilter
处理/login/oauth2/code
,提取授权码和状态参数。- 验证状态参数,确保请求的合法性。
- 使用授权码向授权服务器的令牌端点发送请求以交换访问令牌。如果是开启oidc,会涉及到
OAuth2AuthorizationCodeAuthenticationProvider
,它实际执行令牌请求和处理响应。 OAuth2LoginAuthenticationFilter
过滤器通过OidcAuthorizationCodeAuthenticationProvider
发起/oauth2/token
请求,使用授权码向授权服务器请求访问令牌。- 授权服务器的
OAuth2TokenEndpointFilter
过滤器验证code,返回访问令牌和用户信息。 OAuth2LoginAuthenticationFilter
过滤器创建OAuth2LoginAuthenticationToken
并将其存储在安全上下文中,表示用户已登录。并向最初的资源请求重定向(携带获取的令牌进行重定向,最初的资源请求url会在ExceptionTranslationFilter
过滤器捕获Access Denied
异常时,保存在requestCache
中,默认使用session
进行保存,此处过滤器从session
中拿取最初的资源请求,发起重定向)。
6. 用户访问受保护资源
- 用户再次尝试访问受保护资源 URL。
- Spring Security 检测到用户已认证,并携带有效的访问令牌。
- 请求被允许访问受保护资源。
进一步解释
-
OAuth2AuthorizationRequestRedirectFilter
的配置:OAuth2AuthorizationRequestRedirectFilter
的默认 URL 是/oauth2/authorization/{registrationId}
,这个 URL 在 Spring Security OAuth2 客户端配置中通过registrationId
关联。
-
授权码交换流程:
OAuth2LoginAuthenticationFilter
调用AuthenticationManager
的authenticate
方法。AuthenticationManager
会委托给OAuth2AuthorizationCodeAuthenticationProvider
去执行授权码交换,获取令牌和用户信息。
-
安全上下文的更新:
- 一旦授权码交换成功,
OAuth2LoginAuthenticationFilter
将创建一个新的OAuth2LoginAuthenticationToken
并将其放入SecurityContextHolder
中,从而将用户标记为已认证。
- 一旦授权码交换成功,
-
LoginUrlAuthenticationEntryPoint
的认证请求重定向:security中针对认证失败发起登录请求重定向的处理有两种:
一种是非OAuth2的普通用户名密码登录,请求为
"/login"
,被UsernamePasswordAuthenticationFilter
过滤器处理。另一种是OAuth2请求,客户端起
"/oauth2/authorization"
请求,被OAuth2AuthorizationRequestRedirectFilter
过滤器处理,该过滤器转而发起"/oauth2/authorize"
请求,向授权服务请求授权码code。LoginUrlAuthenticationEntryPoint
针对两种登录的区分:如果配置中只开启了OAuth2的登录认证,则
LoginUrlAuthenticationEntryPoint
默认重定向地址为"/oauth2/authorization"
。如果同时配置了OAuth2与普通用户名密码登录,则
LoginUrlAuthenticationEntryPoint
默认重定向地址为"/login"
,但登录页面会出现两个选项,由用户自己选择进行哪个登录:一个是客户端系统自己的用户名密码登录,一个是显示OAuth2授权服务地址的OAuth2登录。
配置示例
yaml配置授权请求基础路径
下面是一个示例 YAML 配置,展示如何配置一个 OAuth2 客户端的授权请求基础路径:
spring:
security:
oauth2:
client:
registration:
test-client-oidc: # 客户端标识符
client-id: test-client
client-secret: secret
authorization-grant-type: authorization_code
redirect-uri: "{baseUrl}/{action}/oauth2/code/{registrationId}"
scope: openid,read,write
client-name: TestClient
authorization-uri: http://localhost:9000/oauth2/authorize # 授权端点 URI
provider:
authorization-server:
issuer-uri: http://localhost:9000 # 授权服务器发行者 URI
在这个示例中:
spring.security.oauth2.client.registration.test-client-oidc.authorization-uri
指定了授权服务器的授权端点 URI。redirect-uri
使用了{baseUrl}
、{action}
和{registrationId}
等动态参数来构建重定向 URI。
这样配置后,Spring Security OAuth2 将会按照这些配置处理 /login/oauth2/authorization/{registrationId}
路径的请求,动态替换其中的 {registrationId}
部分为实际的客户端注册标识符,从而生成正确的重定向 URI。
这种配置方式使得客户端能够灵活地指定授权请求的基础路径,并且可以根据需要进行调整和定制。
代码配置客户端
先看一段配置,通过如下代码,将ClientSecurityConfig
所在的项目配置为OAuth2的客户端:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
public class ClientSecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(authorize ->
// 任何请求都需要认证
authorize.anyRequest().authenticated()
)
.oauth2Login(Customizer.withDefaults())
.oauth2Client(Customizer.withDefaults());
return http.build();
}
}
通过上面的配置代码,客户端所有的请求都会被 OAuth2AuthorizationRequestRedirectFilter
过滤,并在需要时重定向请求到 Spring Security OAuth 2.0 Authorization Server 进行认证。
具体流程
-
配置解析:
http.authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated()) .oauth2Login(Customizer.withDefaults()) .oauth2Client(Customizer.withDefaults());
authorize.anyRequest().authenticated()
:配置所有请求都需要认证。oauth2Login(Customizer.withDefaults())
:配置 OAuth 2.0 登录功能。oauth2Client(Customizer.withDefaults())
:配置 OAuth 2.0 客户端功能。
-
过滤器链初始化:
- 当 Spring Security 初始化时,会根据
SecurityFilterChain
配置初始化相应的过滤器链。
- 当 Spring Security 初始化时,会根据
-
过滤器链中的关键过滤器:
OAuth2AuthorizationRequestRedirectFilter
:负责处理 OAuth 2.0 授权请求的重定向。OAuth2LoginAuthenticationFilter
:处理 OAuth 2.0 登录认证。OAuth2AccessTokenResponseClient
:处理访问令牌的请求和响应。
-
请求处理流程:
- 当客户端发出请求时,
SecurityFilterChain
中的各个过滤器按顺序处理请求。 - 如果请求未经过认证,
OAuth2AuthorizationRequestRedirectFilter
会拦截请求。 OAuth2AuthorizationRequestRedirectFilter
会将请求重定向到授权服务器的授权端点。- 用户在授权服务器上进行认证,并授权客户端访问其资源。
- 授权服务器返回授权码(authorization code)给客户端。
- 客户端使用授权码向授权服务器的令牌端点请求访问令牌。
- 当客户端发出请求时,
-
认证完成后的请求:
- 一旦客户端获取到访问令牌,后续的请求将携带访问令牌。
OAuth2Client
相关的过滤器会自动处理这些请求,验证令牌的有效性并确保请求被授权。
流程说明
以下是上述流程的说明:
Client Request -> SecurityFilterChain -> OAuth2AuthorizationRequestRedirectFilter (未认证) -> 重定向到授权服务器
授权服务器 -> 用户认证并授权 -> 返回授权码
Client -> 使用授权码请求访问令牌 -> 获取访问令牌
后续 Client 请求 -> 携带访问令牌 -> SecurityFilterChain -> OAuth2Client 相关过滤器 -> 处理请求
结论
通过上述配置代码,客户端的所有请求都会被 OAuth2AuthorizationRequestRedirectFilter
过滤。如果请求未经过认证,过滤器会将请求重定向到 Spring Security OAuth 2.0 Authorization Server 进行认证,确保客户端能够安全地访问受保护的资源。
Access Denied异常
介绍
OAuth2场景中,
Access Denied
异常是因为当前请求的客户端没有通过认证而被抛出的,主要通过SecurityContext
上下文来进行检验。
即:如果客户端在当前请求之前已经通过认证,则客户端信息会被保存起来 (默认session
中,可以自定义实现),然后有一个过滤器SecurityContextHolderFilter
,在每个请求到来时,此过滤器会从适当的存储(如 HttpSession
)中加载 SecurityContext
,将已认证的信息保存到 SecurityContext
上下文中,供下面进行认证。
主要涉及的过滤器:
-
AuthorizationFilter
-
SecurityContextHolderFilter
AuthorizationFilter
客户端请求认证的入口为
AuthorizationFilter
过滤器,负责检查客户端权限
AuthorizationFilter
过滤方法源码:
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
throws ServletException, IOException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
if (this.observeOncePerRequest && isApplied(request)) {
chain.doFilter(request, response);
return;
}
if (skipDispatch(request)) {
chain.doFilter(request, response);
return;
}
String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);
try {
//权限检查
AuthorizationDecision decision = this.authorizationManager.check(this::getAuthentication, request);
this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, request, decision);
if (decision != null && !decision.isGranted()) {
throw new AccessDeniedException("Access Denied");
}
chain.doFilter(request, response);
}
finally {
request.removeAttribute(alreadyFilteredAttributeName);
}
}
其中的
AuthorizationDecision decision = this.authorizationManager.check(this::getAuthentication, request);
会检索上下文,检查请求是否通过认证:
-
this::getAuthentication
获取上下文权限信息private Authentication getAuthentication() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication == null) { throw new AuthenticationCredentialsNotFoundException( "An Authentication object was not found in the SecurityContext"); } return authentication; }
-
this.authorizationManager.check
进行检查,在AuthenticatedAuthorizationManager
实现类中:@Override public AuthorizationDecision check(Supplier<Authentication> authentication, T object) { boolean granted = isGranted(authentication.get()); return new AuthorizationDecision(granted); } private boolean isGranted(Authentication authentication) { return authentication != null && isNotAnonymous(authentication) && authentication.isAuthenticated(); } private boolean isNotAnonymous(Authentication authentication) { return !this.trustResolver.isAnonymous(authentication); }
在客户端第一次请求资源时,会因为认证不通过(SecurityContext
上下文没有已认证的客户端信息),在此处抛出Access Denied
异常,并被ExceptionTranslationFilter
过滤器捕获,进而触发LoginUrlAuthenticationEntryPoint
的 commence
方法,发起 /oauth2/authorization/{registrationId}
重定向请求
SecurityContextPersistenceFilter
在 Spring Security OAuth2 中,客户端认证信息的存储和恢复通过一系列内置组件和过滤器来实现。当客户端认证成功后,认证信息会被存储在 HttpSession
中。在随后的请求中,这些信息会被自动恢复并存入 SecurityContextHolder
。
SecurityContextPersistenceFilter
便负责此操作。
此过滤器的作用
-
在请求开始时,先获取之前请求中,已认证通过的用户或客户端信息(默认
session
中保存,可自己配置), 然后赋予到当前请求的上下文SecurityContext
中。 -
请求结束后,在此过滤器的finally操作中,将本次请求已认证通过的客户端或用户信息更新到
session
中 (或自配置的其他保存方式)。
保存方式通过实现SecurityContextRepository
接口自定义,默认是session
。
在oauth2中的作用
SecurityContextPersistenceFilter
过滤器在security的过滤器链中非常靠前,在oauth2请求流程尚未开始之前,就会进行上述的上下文存取操作,用以进行权限验证。
当请求到来时,如果此过滤器没有从存储中找到已认证的客户端信息,在AuthorizationFilter
过滤器中,便会抛出Access Denied
异常,进而被ExceptionTranslationFilter
捕获,然后进一步发起 /oauth2/authorization/{registrationId}
请求,开始整个oauth2的认证授权流程。
当oauth2的认证授权流程完成后,客户端拿到token,结束认证授权流程时,SecurityContextPersistenceFilter
过滤器doFilter
方法的finally
代码块中,会将已认证通过的客户端上下文信息保存到存储(默认session,可自定义配置)中,用以下一次认证使用。
以下是 Spring Security OAuth2 处理客户端认证信息的关键步骤和组件:
关键组件和过滤器
OAuth2LoginAuthenticationFilter
:处理 OAuth2 登录请求,认证成功后将认证信息存储到SecurityContextHolder
和HttpSession
中。OAuth2AuthorizedClientRepository
:在新的请求中从HttpSession
中恢复认证信息,并将其存入SecurityContextHolder
中。SecurityContextPersistenceFilter
:这是一个核心过滤器,用于在每个请求开始时从HttpSession
中加载SecurityContext
,并在请求结束时将其保存回HttpSession
。
工作流程
-
OAuth2 登录认证
当用户通过 OAuth2 登录时,
OAuth2LoginAuthenticationFilter
会处理该请求。如果认证成功,它会将Authentication
对象存储到SecurityContextHolder
中,并通过OAuth2AuthorizedClientRepository
将已认证的客户端信息保存到HttpSession
中。Authentication authentication = // create authentication object after successful OAuth2 login SecurityContextHolder.getContext().setAuthentication(authentication); authorizedClientRepository.saveAuthorizedClient(authorizedClient, authentication, request, response);
-
恢复认证信息
在后续请求中,
SecurityContextPersistenceFilter
会在请求开始时从HttpSession
中加载SecurityContext
并将其存入SecurityContextHolder
中。SecurityContext contextBeforeChainExecution = repo.loadContext(holder); SecurityContextHolder.setContext(contextBeforeChainExecution);
同时,
OAuth2AuthorizedClientRepository
也会从HttpSession
中恢复已认证的客户端信息,并将其与SecurityContextHolder
中的Authentication
相关联。 -
AuthorizationFilter 中的检查
在你的
AuthorizationFilter
中,会通过SecurityContextHolder
来获取当前请求的Authentication
对象,并检查权限:Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); AuthorizationDecision decision = this.authorizationManager.check(() -> authentication, request);
源码解析
SecurityContextPersistenceFilter过滤器源码解析
构造及属性
public class SecurityContextPersistenceFilter extends GenericFilterBean {
static final String FILTER_APPLIED = "__spring_security_scpf_applied";
private SecurityContextRepository repo;
private boolean forceEagerSessionCreation = false;
public SecurityContextPersistenceFilter() {
this(new HttpSessionSecurityContextRepository());
}
public SecurityContextPersistenceFilter(SecurityContextRepository repo) {
this.repo = repo;
}
}
FILTER_APPLIED
: 用于标识当前请求是否已经应用了该过滤器,避免重复执行。repo
:SecurityContextRepository
实例,用于加载和保存SecurityContext
。默认实现是HttpSessionSecurityContextRepository
,它将SecurityContext
存储在 HTTP 会话中。forceEagerSessionCreation
: 标志是否强制提前创建会话。默认值为false
。
两个构造函数,一个无参构造函数默认使用 HttpSessionSecurityContextRepository
,另一个允许传入自定义的 SecurityContextRepository
。
doFilter 方法
doFilter
方法是过滤器的核心逻辑:
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
}
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
//确保过滤器仅应用一次
if (request.getAttribute(FILTER_APPLIED) != null) {
chain.doFilter(request, response);
return;
}
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
//提前创建会话
if (this.forceEagerSessionCreation) {
HttpSession session = request.getSession();
if (this.logger.isDebugEnabled() && session.isNew()) {
this.logger.debug(LogMessage.format("Created session %s eagerly", session.getId()));
}
}
//加载SecurityContext
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder);
try {
//设置 `SecurityContext` 到 `SecurityContextHolder
SecurityContextHolder.setContext(contextBeforeChainExecution);
if (contextBeforeChainExecution.getAuthentication() == null) {
logger.debug("Set SecurityContextHolder to empty SecurityContext");
}
else {
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", contextBeforeChainExecution));
}
}
chain.doFilter(holder.getRequest(), holder.getResponse());
}
//保存并清除上下文
finally {
SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
// Crucial removal of SecurityContextHolder contents before anything else.
SecurityContextHolder.clearContext();
this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
request.removeAttribute(FILTER_APPLIED);
this.logger.debug("Cleared SecurityContextHolder to complete request");
}
}
详细步骤
- 确保过滤器仅应用一次
- 使用
FILTER_APPLIED
属性来检查当前请求是否已经应用了该过滤器,如果已经应用,则直接调用chain.doFilter
继续过滤链的执行。
- 提前创建会话(可选)
- 如果
forceEagerSessionCreation
为true
,则提前创建会话。
- 加载
SecurityContext
- 使用
SecurityContextRepository
加载当前请求的SecurityContext
。
- 设置
SecurityContext
到SecurityContextHolder
- 在过滤器链执行之前,将加载的
SecurityContext
设置到SecurityContextHolder
。
- 执行过滤器链
- 调用
chain.doFilter
执行后续的过滤器和最终的请求处理。
-
清理
SecurityContext
- 在过滤器链执行完之后,将
SecurityContextHolder
中的SecurityContext
保存到SecurityContextRepository
中并清除。
- 在过滤器链执行完之后,将
关键点总结
SecurityContextPersistenceFilter
确保每个请求都有一个独立的SecurityContext
,并在请求处理完之后清理该上下文。- 它通过
SecurityContextRepository
来加载和保存SecurityContext
,默认实现是HttpSessionSecurityContextRepository
。 - 通过使用
SecurityContextHolder
来管理当前线程的安全上下文,使得 Spring Security 的其他组件能够方便地访问当前的SecurityContext
。
大致总结
- OAuth2LoginAuthenticationFilter 处理 OAuth2 登录并将认证信息存储到
SecurityContextHolder
和HttpSession
中。 - SecurityContextPersistenceFilter 在每个请求开始时从
HttpSession
中加载SecurityContext
,并在请求结束时将其保存回HttpSession
。 - OAuth2AuthorizedClientRepository 从
HttpSession
中恢复已认证的客户端信息,并与SecurityContextHolder
中的Authentication
相关联。 - AuthorizationFilter 通过
SecurityContextHolder
获取当前请求的Authentication
对象,并检查权限。
通过这些机制,Spring Security OAuth2 能够在不同请求之间保持客户端的认证状态。
SecurityContextHolderFilter
SecurityContextHolderFilter
是一个新的过滤器,用于在每个请求的开始和结束时管理SecurityContext
。
springboot 2.7.10及相关版本中,SecurityContextPersistenceFilter
过滤器被标记为弃用,但源码执行过程中,默认仍使用SecurityContextPersistenceFilter
过滤器来存取上下文
简介
SecurityContextHolderFilter
是一个过滤器,用于在每个请求开始时从适当的存储中加载 SecurityContext
,并在请求结束时清除或保存 SecurityContext
。
SecurityContextHolderFilter
的工作机制
- 在每个请求开始时,从适当的存储(如
HttpSession
)中加载SecurityContext
。 - 将
SecurityContext
存入SecurityContextHolder
。 - 在请求结束时,清除或保存
SecurityContext
。
处理客户端认证
如前所述,当客户端认证成功后,认证信息会被存储到 HttpSession
中。在随后的请求中,SecurityContextHolderFilter
会从 HttpSession
中加载 SecurityContext
并将其存入 SecurityContextHolder
。这样,在每个请求中,SecurityContextHolder
都会包含最新的认证信息。
通过这种方式,即使客户端已经认证通过,新的请求到达时,SecurityContextHolderFilter
也会确保认证信息被正确加载并存入 SecurityContextHolder
,从而保证客户端的认证状态在请求之间得以保持。
使用示例
以下是一个简单的示例,展示了如何在 Spring Security 5.7.7 中使用 SecurityContextHolderFilter
来替代 SecurityContextPersistenceFilter
。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.context.SecurityContextHolderFilter;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorizeRequests ->
authorizeRequests
.antMatchers("/").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(oauth2Login ->
oauth2Login
.userInfoEndpoint(userInfoEndpoint ->
userInfoEndpoint.oidcUserService(oidcUserService())
)
)
.addFilterAfter(new SecurityContextHolderFilter(), OAuth2LoginAuthenticationFilter.class);
return http.build();
}
@Bean
public OidcUserService oidcUserService() {
return new OidcUserService();
}
@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
return authorizedClientManager;
}
}
主要组件说明
SecurityContextHolderFilter
:替代了SecurityContextPersistenceFilter
,负责管理SecurityContext
的加载和保存。SecurityFilterChain
:配置 HTTP 安全性的方式,通过 Spring Security 的新配置方式实现。OAuth2AuthorizedClientManager
:管理 OAuth2 已授权客户端的组件。
OAuth2AuthorizationRequestRedirectFilter
OAuth2AuthorizationRequestRedirectFilter
是 Spring Security OAuth2 客户端中的一个过滤器,它负责拦截并处理需要用户授权的请求。它主要用于在用户访问受保护资源但未认证时,将用户重定向到授权服务器以进行认证和授权。
针对
authorization_code
、implicit
两种模式
过滤的请求
OAuth2AuthorizationRequestRedirectFilter
会过滤与 OAuth2 授权请求相关的特定路径的请求。默认情况下,这些路径是以 /oauth2/authorization
开头的 URI 。
例如,当发起 /oauth2/authorization/{registrationId}
请求 时,该过滤器会进行拦截和处理。
过滤器的匹配路径是通过 RequestMatcher
配置的,通常使用 AntPathRequestMatcher
来匹配特定的路径模式,下面是其源码的一部分:
private static final String DEFAULT_AUTHORIZATION_REQUEST_BASE_URI = "/oauth2/authorization";
public OAuth2AuthorizationRequestRedirectFilter(ClientRegistrationRepository clientRegistrationRepository) {
this(clientRegistrationRepository, DEFAULT_AUTHORIZATION_REQUEST_BASE_URI);
}
处理流程
-
匹配请求路径:
OAuth2AuthorizationRequestRedirectFilter
会检查请求的 URI 是否匹配配置的路径模式,例如/oauth2/authorization/{registrationId}
。 -
解析授权请求:
过滤器会使用
OAuth2AuthorizationRequestResolver
解析授权请求信息。默认实现是DefaultOAuth2AuthorizationRequestResolver
,它会从请求 URI 中提取registrationId
并构建OAuth2AuthorizationRequest
对象。 -
重定向到授权服务器:
如果解析成功,过滤器会将用户重定向到 OAuth2 授权服务器的授权端点。这是通过
RedirectStrategy
实现的,通常会保存当前请求以便在授权完成后能够返回。
请求如何被发起
在 Spring Security OAuth2 客户端应用中,/oauth2/authorization/{registrationId}
请求通常是由用户访问受保护资源时触发的。例如,当用户尝试访问一个受保护的页面或 API,但尚未认证时,Spring Security 会引导用户进行 OAuth2 登录授权流程。
具体步骤如下:
-
用户访问受保护资源:
用户访问一个需要认证的资源,例如
/protected/resource
。 -
未认证的请求被重定向:
- Spring Security 检测到用户未认证,抛出
Access Denied
异常。 ExceptionTranslationFilter
过滤器会捕获Access Denied
异常,触发DelegatingAuthenticationEntryPoint
的commence
方法。commence
方法继续向下触发LoginUrlAuthenticationEntryPoint
的commence
方法,发起/oauth2/authorization
请求。- 请求到达
OAuth2AuthorizationRequestRedirectFilter
过滤器处理。
- Spring Security 检测到用户未认证,抛出
-
OAuth2AuthorizationRequestRedirectFilter 处理请求
该过滤器匹配到
/oauth2/authorization/{registrationId}
路径后,解析授权请求并将用户重定向到授权服务器的授权端点。即发起/oauth2/authorize
请求,交由OAuth2-Server的OAuth2AuthorizationEndpointFilter
过滤器处理 -
用户进行授权:
用户在授权服务器上进行登录和授权。
-
返回授权码:
授权服务器返回授权码到客户端应用的回调 URI,默认路径为
/login/oauth2/code/*
(这里进入了OAuth2LoginAuthenticationFilter
过滤器进行处理) -
客户端交换令牌:
客户端应用使用授权码向授权服务器请求访问令牌(
OAuth2LoginAuthenticationFilter
的处理过程中发起/oauth2/token
请求,用授权码换取令牌)。 -
访问受保护资源:
令牌获取成功后,客户端应用使用访问令牌请求受保护的资源(向资源请求重定向)。
源码解析
OAuth2AuthorizationRequestRedirectFilter
是 Spring Security OAuth 2.0 客户端的一部分,用于处理 OAuth 2.0 授权请求的重定向。它会捕获未认证的请求,并将其重定向到 OAuth 2.0 授权服务器以完成认证过程。下面详细讲解该过滤器的工作原理及请求处理流程:
1. 主要成员变量
- authorizationRequestResolver:用于解析 OAuth 2.0 授权请求。
- authorizationRequestRepository:用于存储和检索 OAuth 2.0 授权请求。
- requestCache:用于缓存原始请求,以便在授权后能够恢复。
- authorizationRedirectStrategy:用于执行重定向操作。
2. 构造方法
过滤器的构造方法根据传入的 ClientRegistrationRepository
和 authorizationRequestBaseUri
初始化 authorizationRequestResolver
:
public OAuth2AuthorizationRequestRedirectFilter(ClientRegistrationRepository clientRegistrationRepository,
String authorizationRequestBaseUri) {
Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null");
Assert.hasText(authorizationRequestBaseUri, "authorizationRequestBaseUri cannot be empty");
this.authorizationRequestResolver = new DefaultOAuth2AuthorizationRequestResolver(clientRegistrationRepository,
authorizationRequestBaseUri);
}
3. 请求处理流程
过滤方法
客户端访问资源但未认证,
ExceptionTranslationFilter
过滤器捕获异常,LoginUrlAuthenticationEntryPoint
发起/oauth2/authorization
请求,进入OAuth2AuthorizationRequestRedirectFilter
如下的doFilterInternal
方法
过滤器的核心逻辑在 doFilterInternal
方法中实现:
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
// 尝试解析 /oauth2/authorization 授权请求
OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestResolver.resolve(request);
if (authorizationRequest != null) {
// 如果成功解析到授权请求,则执行重定向
this.sendRedirectForAuthorization(request, response, authorizationRequest);
return;
}
} catch (Exception ex) {
// 如果解析授权请求时出现异常,则执行失败处理
this.unsuccessfulRedirectForAuthorization(request, response, ex);
return;
}
try {
// 继续处理过滤器链中的其他过滤器
filterChain.doFilter(request, response);
} catch (Exception ex) {
// 处理异常,如果是 ClientAuthorizationRequiredException 则处理授权重定向
Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex);
ClientAuthorizationRequiredException authzEx = (ClientAuthorizationRequiredException) this.throwableAnalyzer
.getFirstThrowableOfType(ClientAuthorizationRequiredException.class, causeChain);
if (authzEx != null) {
try {
// 解析授权请求
OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestResolver.resolve(request,
authzEx.getClientRegistrationId());
if (authorizationRequest == null) {
throw authzEx;
}
// 缓存原始请求
this.requestCache.saveRequest(request, response);
// 执行授权重定向
this.sendRedirectForAuthorization(request, response, authorizationRequest);
} catch (Exception failed) {
// 如果授权重定向失败,则处理失败
this.unsuccessfulRedirectForAuthorization(request, response, failed);
}
return;
}
if (ex instanceof ServletException) {
throw (ServletException) ex;
}
if (ex instanceof RuntimeException) {
throw (RuntimeException) ex;
}
throw new RuntimeException(ex);
}
}
解析授权请求的流程
下面是this.authorizationRequestResolver.resolve(request)
解析授权的过程
在
OAuth2AuthorizationRequestRedirectFilter
中,当捕获到一个未认证的请求时,它会使用OAuth2AuthorizationRequestResolver
来解析授权请求。
DefaultOAuth2AuthorizationRequestResolver
是一个具体的实现,它的作用是构建一个OAuth2AuthorizationRequest
对象,该对象包含了 OAuth 2.0 授权请求所需的所有信息。
-
捕获请求并调用
resolve
方法:在
OAuth2AuthorizationRequestRedirectFilter
中,当一个未认证的请求被捕获时,会调用authorizationRequestResolver.resolve(request)
来解析授权请求。 -
调用
DefaultOAuth2AuthorizationRequestResolver
的resolve
方法解析请求:因为
OAuth2AuthorizationRequestResolver
只有一个实现类DefaultOAuth2AuthorizationRequestResolver
,所以会调用它的resolve
方法@Override public OAuth2AuthorizationRequest resolve(HttpServletRequest request) { //解析请求,获取`registrationId`,即请求中的客户端id String registrationId = resolveRegistrationId(request); if (registrationId == null) { return null; } //获取 `redirectUriAction`,请求中的action参数,默认为login String redirectUriAction = getAction(request, "login"); //调用 `resolve` 方法 return resolve(request, registrationId, redirectUriAction); }
解析
registrationId
:上面
resolveRegistrationId(request)
方法会去匹配请求,只有/oauth2/authorization
请求才会被DefaultOAuth2AuthorizationRequestResolver
解析,然后提取出registrationId
(客户端注册id,可在yaml中配置)。private String resolveRegistrationId(HttpServletRequest request) { //匹配 /oauth2/authorization 请求 if (this.authorizationRequestMatcher.matches(request)) { return this.authorizationRequestMatcher.matcher(request).getVariables() .get(REGISTRATION_ID_URI_VARIABLE_NAME); } return null; }
获取
redirectUriAction
:通过
getAction(request, "login")
获取redirectUriAction
,提取action
参数,如果请求参数中没有指定action
,则默认为"login"
。private String getAction(HttpServletRequest request, String defaultAction) { String action = request.getParameter("action"); if (action == null) { return defaultAction; } return action; }
调用
resolve
方法:使用
registrationId
和redirectUriAction
构建OAuth2AuthorizationRequest
对象。- 获取
ClientRegistration
: 使用registrationId
从clientRegistrationRepository
中查找ClientRegistration
对象。 - 构建
OAuth2AuthorizationRequest
: 使用ClientRegistration
信息构建OAuth2AuthorizationRequest
,包括clientId
、authorizationUri
、redirectUri
、scopes
以及state
。 - 自定义请求: 使用
authorizationRequestCustomizer
自定义构建器。 - 生成重定向url:通过解析请求的完整 URL,并结合
ClientRegistration
中的重定向 URI 模板,构建最终的重定向 URI。 - 返回
OAuth2AuthorizationRequest
对象: 通过上面信息返回构建完成的OAuth2AuthorizationRequest
对象。
private OAuth2AuthorizationRequest resolve(HttpServletRequest request, String registrationId, String redirectUriAction) { if (registrationId == null) { return null; } //会根据yaml配置获取信息,文章下面会展示yaml配置 ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId); if (clientRegistration == null) { throw new IllegalArgumentException("Invalid Client Registration with Id: " + registrationId); } //根据yaml配置的authorization-grant-type授权类型,用以构建出不同的OAuth2AuthorizationRequest //此方法内部,只判断authorization_code、implicit两种授权类型,如果两种都不是,会抛出IllegalArgumentException //对于 authorization_code 类型,还会根据是否包含 openid 范围和客户端认证方法是否为 NONE 来应用 nonce 参数和 PKCE。 OAuth2AuthorizationRequest.Builder builder = getBuilder(clientRegistration); //expandRedirectUri 方法通过解析请求的完整 URL,并结合 ClientRegistration 中的重定向 URI 模板,构建最终的重定向 URI。 //这个方法提取 URL 的各个部分(如协议、主机、端口、路径等),并将这些部分与 ClientRegistration 中定义的重定向 URI 模板相结合,生成最终的重定向 URI。 //例如,如果 ClientRegistration 的重定向 URI 模板是 {baseUrl}/{action}/oauth2/code/{registrationId}, //该方法会将 baseUrl、action 和 registrationId 替换为实际值,从而生成完整的重定向 URI。 //OAuth2 Server授权服务完成授权后,会对此地址进行重定向,把code授权码返回给客户端 String redirectUriStr = expandRedirectUri(request, clientRegistration, redirectUriAction); //根据clientRegistration来构建请求对象 builder.clientId(clientRegistration.getClientId()) .authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri()) .redirectUri(redirectUriStr) .scopes(clientRegistration.getScopes()) .state(DEFAULT_STATE_GENERATOR.generateKey()); this.authorizationRequestCustomizer.accept(builder); //执行构建并返回结果 return builder.build(); }
关于上面的
ClientRegistration
,其信息会从下面的yaml中获取:ClientRegistrationRepository
的实现通常会读取 Spring 配置文件中的信息(如 YAML 或 properties 文件),将这些配置解析为ClientRegistration
对象。在上述代码中,clientRegistrationRepository.findByRegistrationId(registrationId)
会从配置文件中读取messaging-client-oidc
的注册信息,并返回ClientRegistration
对象。spring: security: oauth2: client: registration: messaging-client-oidc: provider: authorization-server client-id: messaging-client client-secret: secret authorization-grant-type: authorization_code redirect-uri: "https://127.0.0.1:8000/login/oauth2/code/messaging-client-oidc" #redirect-uri: "{baseUrl}/{action}/oauth2/code/{registrationId}" scope: openid,message.read,message.write client-name: messaging-client-oidc
- 获取
总结起来,
OAuth2AuthorizationRequestRedirectFilter
使用OAuth2AuthorizationRequestResolver
解析请求,具体步骤如下:
- 捕获未认证请求并调用
resolve
方法。 - 从请求中提取
registrationId
和action
。 - 使用提取的信息和
ClientRegistration
对象构建OAuth2AuthorizationRequest
。 - 返回构建完成的
OAuth2AuthorizationRequest
对象,用于后续的授权请求重定向。
通过这种方式,Spring Security OAuth 2.0 客户端能够自动处理未认证的请求,并将用户重定向到授权服务器进行认证。
4. 重定向处理
如果解析到授权请求,OAuth2AuthorizationRequestRedirectFilter
过滤器的sendRedirectForAuthorization
方法会保存授权请求并执行重定向,来向认证服务发起/oauth2/authorize
请求,去获取code
:
private void sendRedirectForAuthorization(HttpServletRequest request, HttpServletResponse response,
OAuth2AuthorizationRequest authorizationRequest) throws IOException {
//这里检查 authorizationRequest 的授权类型是否为 AuthorizationGrantType.AUTHORIZATION_CODE授权码模式
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(authorizationRequest.getGrantType())) {
//如果授权类型是授权码模式,则将 authorizationRequest(授权请求对象)保存到 authorizationRequestRepository 中。
//authorizationRequestRepository 通常会将请求存储在用户会话中(可以通过配置自定义保存方式),以便在用户完成授权后能够恢复请求上下文。
//这一步的目的是在授权服务器重定向回客户端应用时,能够正确处理回调并进行后续的令牌交换。
this.authorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, request, response);
}
//使用 authorizationRedirectStrategy 将用户重定向到授权服务器的授权端点。
//authorizationRequest.getAuthorizationRequestUri() 返回授权请求的 URI,通常是授权服务器的授权端点地址。
//这一步会让浏览器导航到授权服务器的登录页面,用户在授权服务器上进行登录和授权操作。
this.authorizationRedirectStrategy.sendRedirect(request, response,
authorizationRequest.getAuthorizationRequestUri());
}
OAuth2AuthorizationRequestRedirectFilter
过滤器解析完/oauth2/authorization/{registrationId}
请求后,会将本次客户端的请求保存起来。其目的是为了在 OAuth2 授权码流程中,能够在授权服务器返回code授权码时找到并关联原始授权请求,后续在请求中用code换取token,在用token去访问资源。从而使得客户端能够在一次 HTTP 请求过程中完成从授权到资源访问的闭环流程。
因为授权服务、客户端、资源服务往往是不同的微服务,如果不进行请求会话的保存,请求与响应将无法对应,无法完成客户端在一次http请求中,向授权服务获取授权,用授权来向资源服务获取资源
的过程。
5. 失败处理
如果解析授权请求或重定向过程中出现异常,unsuccessfulRedirectForAuthorization
方法会记录错误并返回 500 错误:
private void unsuccessfulRedirectForAuthorization(HttpServletRequest request, HttpServletResponse response,
Exception ex) throws IOException {
this.logger.error(LogMessage.format("Authorization Request failed: %s", ex), ex);
response.sendError(HttpStatus.INTERNAL_SERVER_ERROR.value(),
HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase());
}
总结
OAuth2AuthorizationRequestRedirectFilter
负责捕获未经认证的请求,解析 OAuth 2.0 授权请求,并将请求重定向到授权服务器进行认证。成功认证后,原始请求会被恢复并继续处理,从而实现 OAuth 2.0 的授权流程。
OAuth2LoginAuthenticationFilter
OAuth2LoginAuthenticationFilter
是 Spring Security OAuth2 Client 中用于处理 OAuth2 登录流程的过滤器。它的主要作用是处理授权服务器重定向回客户端应用的/login/oauth2/code
请求,并完成 OAuth2 登录认证流程:
OAuth2LoginAuthenticationFilter
中的attemptAuthentication
方法负责处理 OAuth2 授权码交换和认证:即用code
去换token
- 认证成功后,
AbstractAuthenticationProcessingFilter
的doFilter
方法处理后续逻辑,包括清除缓存、触发事件和重定向。 - 认证前的原始请求信息由
RequestCache
保存,并在认证成功后用于重定向。
具体来说,它会处理从授权服务器返回的包含授权码或令牌的回调请求,默认请求路径为
/login/oauth2/code/*
,大致流程如下:
- 当
OAuth2AuthorizationRequestRedirectFilter
过滤器向授权服务器发起请求并获取到code后,会向/login/oauth2/code/*
请求进行重定向,到达OAuth2LoginAuthenticationFilter
。 OAuth2LoginAuthenticationFilter
会获取/login/oauth2/code/*
回调请求中的授权码即code,由OidcAuthorizationCodeAuthenticationProvider
发起/oauth2/token
请求,用code换取token,并返回带有access_token
的认证结果。OAuth2LoginAuthenticationFilter
中得到token后,源码执行会在其父类AbstractAuthenticationProcessingFilter
的doFilter
方法中,向最开始用户发起的资源请求进行重定向,携带令牌访问所需的资源。
主要作用
- 处理授权码交换:
- 当授权服务器重定向回客户端应用时,
OAuth2LoginAuthenticationFilter
负责处理该回调请求,提取授权码并与授权服务器交换访问令牌。
- 处理 OAuth2 登录认证:
- 它会将获取到的访问令牌以及相关的用户信息进行认证处理,将用户信息保存到安全上下文中,从而完成登录。
会过滤的请求
OAuth2LoginAuthenticationFilter
过滤的请求通常是授权服务器重定向回客户端应用的回调请求。这些请求的特征是包含 OAuth2 授权码或者直接包含访问令牌。
默认情况下,这些请求的路径是 "/login/oauth2/code/{registrationId}"
,其中 {registrationId}
是客户端在 ClientRegistration
中配置的标识符。
请求的发起过程
-
用户访问受保护资源:
- 用户尝试访问受保护的资源,但尚未登录。
- Spring Security 检测到用户未认证,会引导到 OAuth2 登录流程。
-
重定向到授权服务器:
OAuth2AuthorizationRequestRedirectFilter
构建并发送授权请求,用户被重定向到授权服务器进行登录和授权。
-
用户在授权服务器上进行授权:
- 用户在授权服务器上进行登录和授权。
- 授权服务器处理完用户的认证和授权后,将用户重定向回客户端应用的回调 URI(通常包含授权码)。
-
回调请求被
OAuth2LoginAuthenticationFilter
处理:- 用户的浏览器收到授权服务器的重定向响应,导航到客户端应用的回调 URI(如
"/login/oauth2/code/{registrationId}"
)。 OAuth2LoginAuthenticationFilter
拦截并处理这个回调请求。
- 用户的浏览器收到授权服务器的重定向响应,导航到客户端应用的回调 URI(如
源码解析
OAuth2LoginAuthenticationFilter
继承了AbstractAuthenticationProcessingFilter
,所以在查看源码执行断点时,他的doFilter
方法是在AbstractAuthenticationProcessingFilter
中执行的。
然后AbstractAuthenticationProcessingFilter
的doFilter
方法中,会调用OAuth2LoginAuthenticationFilter
的attemptAuthentication
方法,此过滤器的核心处理逻辑,都在attemptAuthentication
方法中。
代码展示
OAuth2LoginAuthenticationFilter
是处理 OAuth 2.0 登录认证请求的核心过滤器。它负责处理从授权服务器返回的授权响应,并执行相应的认证逻辑。以下是doFilter
方法的详细解析:
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
//进入下面的doFilter方法
doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
}
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
//如果不是 /login/oauth2/code/* 的请求不进行处理
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
try {
//进入 /login/oauth2/code 的核心认证处理逻辑
Authentication authenticationResult = attemptAuthentication(request, response);
if (authenticationResult == null) {
return;
}
//会话缓存
this.sessionStrategy.onAuthentication(authenticationResult, request, response);
if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
//重定向
successfulAuthentication(request, response, chain, authenticationResult);
}
catch (InternalAuthenticationServiceException failed) {
this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
unsuccessfulAuthentication(request, response, failed);
}
catch (AuthenticationException ex) {
unsuccessfulAuthentication(request, response, ex);
}
}
下面着重看
attemptAuthentication
方法的详细解析:
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
// 将请求参数转换为 MultiValueMap
MultiValueMap<String, String> params = OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap());
// 检查是否是一个授权响应,即上面转换的请求参数中是否包含code和state参数
if (!OAuth2AuthorizationResponseUtils.isAuthorizationResponse(params)) {
OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
// 从请求中移除授权请求,移除OAuth2AuthorizationRequestRedirectFilter过滤器处理过程中保存的authorizationRequest授权请求对象(在上一步第4个小标题重定向处理时保存的)
OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestRepository
.removeAuthorizationRequest(request, response);
if (authorizationRequest == null) {
OAuth2Error oauth2Error = new OAuth2Error(AUTHORIZATION_REQUEST_NOT_FOUND_ERROR_CODE);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
// 获取客户端注册信息
String registrationId = authorizationRequest.getAttribute(OAuth2ParameterNames.REGISTRATION_ID);
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
if (clientRegistration == null) {
OAuth2Error oauth2Error = new OAuth2Error(CLIENT_REGISTRATION_NOT_FOUND_ERROR_CODE,
"Client Registration not found with Id: " + registrationId, null);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
// 构建重定向 URI
String redirectUri = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
.replaceQuery(null)
.build()
.toUriString();
// 将请求参数转换为授权响应
OAuth2AuthorizationResponse authorizationResponse = OAuth2AuthorizationResponseUtils.convert(params, redirectUri);
// 构建认证详细信息
Object authenticationDetails = this.authenticationDetailsSource.buildDetails(request);
// 创建 OAuth2LoginAuthenticationToken
OAuth2LoginAuthenticationToken authenticationRequest = new OAuth2LoginAuthenticationToken(clientRegistration,
new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse));
authenticationRequest.setDetails(authenticationDetails);
// 执行认证并获取结果,这里scope包含oidc,会由OidcAuthorizationCodeAuthenticationProvider验证,
//OidcAuthorizationCodeAuthenticationProvider内部发起请求,用code换取token,并返回带有access_token的认证结果
OAuth2LoginAuthenticationToken authenticationResult = (OAuth2LoginAuthenticationToken) this
.getAuthenticationManager().authenticate(authenticationRequest);
// 转换认证结果为 OAuth2AuthenticationToken
OAuth2AuthenticationToken oauth2Authentication = this.authenticationResultConverter
.convert(authenticationResult);
Assert.notNull(oauth2Authentication, "authentication result cannot be null");
oauth2Authentication.setDetails(authenticationDetails);
// 创建 OAuth2AuthorizedClient 并保存
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(
authenticationResult.getClientRegistration(), oauth2Authentication.getName(),
authenticationResult.getAccessToken(), authenticationResult.getRefreshToken());
//保存授权后的客户端信息
this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, oauth2Authentication, request, response);
return oauth2Authentication;
}
详细解释
-
将请求参数转换为 MultiValueMap
MultiValueMap<String, String> params = OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap());
-
将请求参数转换为
MultiValueMap
,便于后续处理。
-
-
检查是否是一个授权响应
if (!OAuth2AuthorizationResponseUtils.isAuthorizationResponse(params)) { OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST); throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); }
-
检查请求是否包含授权响应的必要参数,即上面转换的请求参数中是否包含如
code
和state
等关键参数,如果没有则抛出异常。
-
-
从请求中移除授权请求
OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestRepository .removeAuthorizationRequest(request, response); if (authorizationRequest == null) { OAuth2Error oauth2Error = new OAuth2Error(AUTHORIZATION_REQUEST_NOT_FOUND_ERROR_CODE); throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); }
- 尝试从
authorizationRequestRepository
中移除并获取授权请求,如果获取不到则抛出异常。 - 这里移除的授权请求,对应上面
OAuth2AuthorizationRequestRedirectFilter
过滤器处理过程第4步重定向处理中,保存的authorizationRequest授权请求对象,会将其从会话中移除,并返回该对象。 - 为什么要移除:在完成 OAuth2 授权流程后,客户端不再需要保存这些临时的授权请求信息,移除这些信息有助于防止重复使用和潜在的安全问题。每次授权请求都是一次性的,成功处理后应及时清理。
- 如果这里因为某些原因没有获取到缓存的请求对象,则security会向浏览器返回一个授权页面,包含
authorization_request_not_found
提示及请求中的客户端id,需要手动点击客户端id进行授权
- 尝试从
-
获取客户端注册信息
String registrationId = authorizationRequest.getAttribute(OAuth2ParameterNames.REGISTRATION_ID); ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId); if (clientRegistration == null) { OAuth2Error oauth2Error = new OAuth2Error(CLIENT_REGISTRATION_NOT_FOUND_ERROR_CODE, "Client Registration not found with Id: " + registrationId, null); throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); }
- 根据
registrationId
获取ClientRegistration
信息,即获取请求客户端的信息,如果不存在则抛出异常。
- 根据
-
构建重定向 URI
String redirectUri = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request)) .replaceQuery(null) .build() .toUriString();
- 构建重定向 URI,用于后续的授权响应处理。
- 构建重定向 URI,用于后续的授权响应处理。
-
将请求参数转换为授权响应
OAuth2AuthorizationResponse authorizationResponse = OAuth2AuthorizationResponseUtils.convert(params, redirectUri);
- 将请求参数转换为
OAuth2AuthorizationResponse
对象。
- 将请求参数转换为
-
构建认证详细信息
Object authenticationDetails = this.authenticationDetailsSource.buildDetails(request);
-
构建认证详细信息对象。
-
-
创建 OAuth2LoginAuthenticationToken
OAuth2LoginAuthenticationToken authenticationRequest = new OAuth2LoginAuthenticationToken(clientRegistration, new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse)); authenticationRequest.setDetails(authenticationDetails);
-
创建
OAuth2LoginAuthenticationToken
,包含客户端注册信息和授权交换信息。
-
-
执行认证并获取结果
OAuth2LoginAuthenticationToken authenticationResult = (OAuth2LoginAuthenticationToken) this .getAuthenticationManager().authenticate(authenticationRequest);
-
使用
AuthenticationManager
执行认证,并获取认证结果。 -
这里
scope
包含oidc
,会由OidcAuthorizationCodeAuthenticationProvider
进行验证,并返回认证结果 -
OidcAuthorizationCodeAuthenticationProvider
内部的验证方法会构建OAuth2AuthorizationCodeGrantRequest
请求对象,在其源码内部的getResponse
方法中,发起code换token的/oauth2/token
请求(请求中还会携带客户端身份验证信息,如果在授权服务中,客户端身份认证不通过,就不会生成token。关于身份验证的流程分析,点此跳转),由授权服务的OAuth2TokenEndpointFilter
过滤器处理//`OidcAuthorizationCodeAuthenticationProvider`的`authenticate`方法调用了下面的`getResponse`方法 private OAuth2AccessTokenResponse getResponse(OAuth2LoginAuthenticationToken authorizationCodeAuthentication) { try { //`getResponse`方法中构建`OAuth2AuthorizationCodeGrantRequest`的授权码认证请求 //该请求由授权服务器OAuth2TokenEndpointFilter过滤器处理 return this.accessTokenResponseClient.getTokenResponse( new OAuth2AuthorizationCodeGrantRequest(authorizationCodeAuthentication.getClientRegistration(), authorizationCodeAuthentication.getAuthorizationExchange())); } catch (OAuth2AuthorizationException ex) { OAuth2Error oauth2Error = ex.getError(); throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex); } }
-
OAuth2TokenEndpointFilter
过滤器认证通过后返回带有access_token
的OAuth2LoginAuthenticationToken
对象
-
-
转换认证结果为 OAuth2AuthenticationToken
OAuth2AuthenticationToken oauth2Authentication = this.authenticationResultConverter .convert(authenticationResult); Assert.notNull(oauth2Authentication, "authentication result cannot be null"); oauth2Authentication.setDetails(authenticationDetails);
- 将带
token
的认证结果转换为OAuth2AuthenticationToken
对象。
- 将带
- 创建 OAuth2AuthorizedClient 并保存
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient( authenticationResult.getClientRegistration(), oauth2Authentication.getName(), authenticationResult.getAccessToken(), authenticationResult.getRefreshToken()); this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, oauth2Authentication, request, response);
- 创建
OAuth2AuthorizedClient
对象,并保存到authorizedClientRepository
中。
- 创建
到这里时,代码执行会返回到
OAuth2LoginAuthenticationFilter
的doFilter
方法(断点执行时,代码在其父类AbstractAuthenticationProcessingFilter
中),到达successfulAuthentication
方法处:
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
//如果不是 /login/oauth2/code/* 的请求不进行处理
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
try {
//进入/login/oauth2/code的核心认证处理逻辑,获得带有access_token的结果
Authentication authenticationResult = attemptAuthentication(request, response);
if (authenticationResult == null) {
return;
}
this.sessionStrategy.onAuthentication(authenticationResult, request, response);
if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
//保存上下文并重定向最初的访问资源的请求
successfulAuthentication(request, response, chain, authenticationResult);
}
catch (InternalAuthenticationServiceException failed) {
this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
unsuccessfulAuthentication(request, response, failed);
}
catch (AuthenticationException ex) {
unsuccessfulAuthentication(request, response, ex);
}
}
successfulAuthentication
方法内部:
主要是将认证通过的客户信息保存到上下文中,并携带得到的令牌向最初发起的资源访问的请求进行重定向
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,Authentication authResult) throws IOException, ServletException {
//将认证通过的客户端信息保存到security的上下文中
//SecurityContext非常核心,贯穿Security过滤器链始终,可从中获取已认证通过的用户信息
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authResult);
SecurityContextHolder.setContext(context);
this.securityContextRepository.saveContext(context, request, response);
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
}
this.rememberMeServices.loginSuccess(request, response, authResult);
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}
//向最初的资源请求重定向
//调用的是SavedRequestAwareAuthenticationSuccessHandler实现类的onAuthenticationSuccess方法
//然后交由资源服务器的BearerTokenAuthenticationFilter过滤器进行过滤处理
this.successHandler.onAuthenticationSuccess(request, response, authResult);
}
SavedRequestAwareAuthenticationSuccessHandler
重定向资源请求
如下为重定向源码,会从requestCache
中获取保存的原始资源请求,requestCache
默认使用session实现。
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws ServletException, IOException {
SavedRequest savedRequest = this.requestCache.getRequest(request, response);
if (savedRequest == null) {
super.onAuthenticationSuccess(request, response, authentication);
return;
}
String targetUrlParameter = getTargetUrlParameter();
if (isAlwaysUseDefaultTargetUrl()
|| (targetUrlParameter != null && StringUtils.hasText(request.getParameter(targetUrlParameter)))) {
this.requestCache.removeRequest(request, response);
super.onAuthenticationSuccess(request, response, authentication);
return;
}
clearAuthenticationAttributes(request);
// Use the DefaultSavedRequest URL
String targetUrl = savedRequest.getRedirectUrl();
getRedirectStrategy().sendRedirect(request, response, targetUrl);
}
这里的资源请求获取,一定是在之前先保存过,这里才能获取到,关于之前保存的步骤,是在ExceptionTranslationFilter
过滤器中handleAccessDeniedExceptionsendStartAuthentication
方法内调用 sendStartAuthentication
方法时保存的,即在最开始认证未通过时,就保存起来了,下面是sendStartAuthentication
源码:
protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
AuthenticationException reason) throws ServletException, IOException {
// SEC-112: Clear the SecurityContextHolder's Authentication, as the
// existing Authentication is no longer considered valid
SecurityContext context = SecurityContextHolder.createEmptyContext();
SecurityContextHolder.setContext(context);
this.requestCache.saveRequest(request, response);
this.authenticationEntryPoint.commence(request, response, reason);
}
最后请求资源,请求到达资源服务进行处理。