Spring OAuth2.0客户端源码解析

主要分析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 登录流程和资源保护。这些过滤器的执行顺序至关重要,以确保正确地处理认证和授权请求。以下是这些过滤器的主要顺序及其作用:

主要过滤器及其顺序

  1. OAuth2AuthorizationRequestRedirectFilter

    • 触发条件:用户访问受保护资源但尚未认证。
    • 作用:构建并发送 OAuth2 授权请求/oauth2/authorize,重定向用户到授权服务器进行认证。
  2. OAuth2LoginAuthenticationFilter

    • 触发条件:授权服务器重定向回客户端应用,并附带授权码。
    • 作用:处理授权服务器的重定向请求,提取授权码并交换访问令牌,完成用户认证。
  3. OAuth2AuthorizationCodeGrantFilter

    • 作用:处理授权码授权流程,获取访问令牌和刷新令牌。

客户端访问受保护资源的流程

假设客户端应用配置了 OAuth2 登录,并且用户尝试访问受保护资源但尚未认证。以下是详细的请求流程:

1. 用户访问受保护资源
  • 用户尝试访问受保护资源 URL,如 https://client-app.com/protected-resource
  • Spring Security 检测到用户未认证,抛出Access Denied异常:当已经认证的用户尝试访问他们无权访问的资源时,会触发 AccessDeniedException。这意味着用户已经通过身份验证,但其权限不足以访问特定资源。
  • ExceptionTranslationFilter 过滤器会捕获 Access Denied 异常,在缓存中保存此次未通过的资源请求(默认使用session保存),并调用 AuthenticationEntryPointcommence 方法。
  • 在 OAuth2 的场景下, AuthenticationEntryPoint 通常是 LoginUrlAuthenticationEntryPoint
  • 触发LoginUrlAuthenticationEntryPointcommence 方法,该方法会发起重定向到 /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 检测到用户已认证,并携带有效的访问令牌。
  • 请求被允许访问受保护资源。

进一步解释

  1. OAuth2AuthorizationRequestRedirectFilter 的配置

    • OAuth2AuthorizationRequestRedirectFilter 的默认 URL 是 /oauth2/authorization/{registrationId},这个 URL 在 Spring Security OAuth2 客户端配置中通过 registrationId 关联。
  2. 授权码交换流程

    • OAuth2LoginAuthenticationFilter 调用 AuthenticationManagerauthenticate 方法。AuthenticationManager 会委托给 OAuth2AuthorizationCodeAuthenticationProvider 去执行授权码交换,获取令牌和用户信息。
  3. 安全上下文的更新

    • 一旦授权码交换成功,OAuth2LoginAuthenticationFilter 将创建一个新的 OAuth2LoginAuthenticationToken 并将其放入 SecurityContextHolder 中,从而将用户标记为已认证。
  4. 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 客户端的授权请求基础路径:

yaml 复制代码
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的客户端:

java 复制代码
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 进行认证。

具体流程

  1. 配置解析

    java 复制代码
    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 客户端功能。
  2. 过滤器链初始化

    • 当 Spring Security 初始化时,会根据 SecurityFilterChain 配置初始化相应的过滤器链。
  3. 过滤器链中的关键过滤器

    • OAuth2AuthorizationRequestRedirectFilter:负责处理 OAuth 2.0 授权请求的重定向。
    • OAuth2LoginAuthenticationFilter:处理 OAuth 2.0 登录认证。
    • OAuth2AccessTokenResponseClient :处理访问令牌的请求和响应。
  4. 请求处理流程

    • 当客户端发出请求时,SecurityFilterChain 中的各个过滤器按顺序处理请求。
    • 如果请求未经过认证,OAuth2AuthorizationRequestRedirectFilter 会拦截请求。
    • OAuth2AuthorizationRequestRedirectFilter 会将请求重定向到授权服务器的授权端点。
    • 用户在授权服务器上进行认证,并授权客户端访问其资源。
    • 授权服务器返回授权码(authorization code)给客户端。
    • 客户端使用授权码向授权服务器的令牌端点请求访问令牌。
  5. 认证完成后的请求

    • 一旦客户端获取到访问令牌,后续的请求将携带访问令牌。
    • 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过滤方法源码:

java 复制代码
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获取上下文权限信息

    java 复制代码
    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实现类中:

    java 复制代码
    @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 过滤器捕获,进而触发LoginUrlAuthenticationEntryPointcommence 方法,发起 /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 处理客户端认证信息的关键步骤和组件:

关键组件和过滤器

  1. OAuth2LoginAuthenticationFilter :处理 OAuth2 登录请求,认证成功后将认证信息存储到 SecurityContextHolderHttpSession 中。
  2. OAuth2AuthorizedClientRepository :在新的请求中从 HttpSession 中恢复认证信息,并将其存入 SecurityContextHolder 中。
  3. SecurityContextPersistenceFilter :这是一个核心过滤器,用于在每个请求开始时从 HttpSession 中加载 SecurityContext,并在请求结束时将其保存回 HttpSession

工作流程

  1. OAuth2 登录认证

    当用户通过 OAuth2 登录时,OAuth2LoginAuthenticationFilter 会处理该请求。如果认证成功,它会将 Authentication 对象存储到 SecurityContextHolder 中,并通过 OAuth2AuthorizedClientRepository 将已认证的客户端信息保存到 HttpSession 中。

    java 复制代码
    Authentication authentication = // create authentication object after successful OAuth2 login
    SecurityContextHolder.getContext().setAuthentication(authentication);
    authorizedClientRepository.saveAuthorizedClient(authorizedClient, authentication, request, response);
  2. 恢复认证信息

    在后续请求中,SecurityContextPersistenceFilter 会在请求开始时从 HttpSession 中加载 SecurityContext 并将其存入 SecurityContextHolder 中。

    java 复制代码
    SecurityContext contextBeforeChainExecution = repo.loadContext(holder);
    SecurityContextHolder.setContext(contextBeforeChainExecution);

    同时,OAuth2AuthorizedClientRepository 也会从 HttpSession 中恢复已认证的客户端信息,并将其与 SecurityContextHolder 中的 Authentication 相关联。

  3. AuthorizationFilter 中的检查

    在你的 AuthorizationFilter 中,会通过 SecurityContextHolder 来获取当前请求的 Authentication 对象,并检查权限:

    java 复制代码
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    AuthorizationDecision decision = this.authorizationManager.check(() -> authentication, request);

源码解析

SecurityContextPersistenceFilter过滤器源码解析

构造及属性
java 复制代码
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 方法是过滤器的核心逻辑:

java 复制代码
@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");
    }
}
详细步骤
  1. 确保过滤器仅应用一次
  • 使用 FILTER_APPLIED 属性来检查当前请求是否已经应用了该过滤器,如果已经应用,则直接调用 chain.doFilter 继续过滤链的执行。
  1. 提前创建会话(可选)
  • 如果 forceEagerSessionCreationtrue,则提前创建会话。
  1. 加载 SecurityContext
  • 使用 SecurityContextRepository 加载当前请求的 SecurityContext
  1. 设置 SecurityContextSecurityContextHolder
  • 在过滤器链执行之前,将加载的 SecurityContext 设置到 SecurityContextHolder
  1. 执行过滤器链
  • 调用 chain.doFilter 执行后续的过滤器和最终的请求处理。
  1. 清理 SecurityContext

    • 在过滤器链执行完之后,将 SecurityContextHolder 中的 SecurityContext 保存到 SecurityContextRepository 中并清除。
关键点总结
  • SecurityContextPersistenceFilter 确保每个请求都有一个独立的 SecurityContext,并在请求处理完之后清理该上下文。
  • 它通过 SecurityContextRepository 来加载和保存 SecurityContext,默认实现是 HttpSessionSecurityContextRepository
  • 通过使用 SecurityContextHolder 来管理当前线程的安全上下文,使得 Spring Security 的其他组件能够方便地访问当前的 SecurityContext

大致总结

  • OAuth2LoginAuthenticationFilter 处理 OAuth2 登录并将认证信息存储到 SecurityContextHolderHttpSession 中。
  • SecurityContextPersistenceFilter 在每个请求开始时从 HttpSession 中加载 SecurityContext,并在请求结束时将其保存回 HttpSession
  • OAuth2AuthorizedClientRepositoryHttpSession 中恢复已认证的客户端信息,并与 SecurityContextHolder 中的 Authentication 相关联。
  • AuthorizationFilter 通过 SecurityContextHolder 获取当前请求的 Authentication 对象,并检查权限。

通过这些机制,Spring Security OAuth2 能够在不同请求之间保持客户端的认证状态。

SecurityContextHolderFilter

SecurityContextHolderFilter 是一个新的过滤器,用于在每个请求的开始和结束时管理 SecurityContext

springboot 2.7.10及相关版本中,SecurityContextPersistenceFilter过滤器被标记为弃用,但源码执行过程中,默认仍使用SecurityContextPersistenceFilter过滤器来存取上下文

简介

SecurityContextHolderFilter 是一个过滤器,用于在每个请求开始时从适当的存储中加载 SecurityContext,并在请求结束时清除或保存 SecurityContext

SecurityContextHolderFilter 的工作机制

  1. 在每个请求开始时,从适当的存储(如 HttpSession)中加载 SecurityContext
  2. SecurityContext 存入 SecurityContextHolder
  3. 在请求结束时,清除或保存 SecurityContext

处理客户端认证

如前所述,当客户端认证成功后,认证信息会被存储到 HttpSession 中。在随后的请求中,SecurityContextHolderFilter 会从 HttpSession 中加载 SecurityContext 并将其存入 SecurityContextHolder。这样,在每个请求中,SecurityContextHolder 都会包含最新的认证信息。

通过这种方式,即使客户端已经认证通过,新的请求到达时,SecurityContextHolderFilter 也会确保认证信息被正确加载并存入 SecurityContextHolder,从而保证客户端的认证状态在请求之间得以保持。

使用示例

以下是一个简单的示例,展示了如何在 Spring Security 5.7.7 中使用 SecurityContextHolderFilter 来替代 SecurityContextPersistenceFilter

java 复制代码
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;
    }
}

主要组件说明

  1. SecurityContextHolderFilter :替代了 SecurityContextPersistenceFilter,负责管理 SecurityContext 的加载和保存。
  2. SecurityFilterChain:配置 HTTP 安全性的方式,通过 Spring Security 的新配置方式实现。
  3. OAuth2AuthorizedClientManager:管理 OAuth2 已授权客户端的组件。

OAuth2AuthorizationRequestRedirectFilter

OAuth2AuthorizationRequestRedirectFilter 是 Spring Security OAuth2 客户端中的一个过滤器,它负责拦截并处理需要用户授权的请求。

它主要用于在用户访问受保护资源但未认证时,将用户重定向到授权服务器以进行认证和授权。

针对authorization_codeimplicit两种模式

过滤的请求

OAuth2AuthorizationRequestRedirectFilter 会过滤与 OAuth2 授权请求相关的特定路径的请求。默认情况下,这些路径是以 /oauth2/authorization 开头的 URI 。

例如,当发起 /oauth2/authorization/{registrationId}请求 时,该过滤器会进行拦截和处理。

过滤器的匹配路径是通过 RequestMatcher 配置的,通常使用 AntPathRequestMatcher 来匹配特定的路径模式,下面是其源码的一部分:

java 复制代码
private static final String DEFAULT_AUTHORIZATION_REQUEST_BASE_URI = "/oauth2/authorization";

public OAuth2AuthorizationRequestRedirectFilter(ClientRegistrationRepository clientRegistrationRepository) {
    this(clientRegistrationRepository, DEFAULT_AUTHORIZATION_REQUEST_BASE_URI);
}

处理流程

  1. 匹配请求路径

    OAuth2AuthorizationRequestRedirectFilter 会检查请求的 URI 是否匹配配置的路径模式,例如 /oauth2/authorization/{registrationId}

  2. 解析授权请求

    过滤器会使用 OAuth2AuthorizationRequestResolver 解析授权请求信息。默认实现是 DefaultOAuth2AuthorizationRequestResolver,它会从请求 URI 中提取 registrationId 并构建 OAuth2AuthorizationRequest 对象。

  3. 重定向到授权服务器

    如果解析成功,过滤器会将用户重定向到 OAuth2 授权服务器的授权端点。这是通过 RedirectStrategy 实现的,通常会保存当前请求以便在授权完成后能够返回。

请求如何被发起

在 Spring Security OAuth2 客户端应用中,/oauth2/authorization/{registrationId}请求通常是由用户访问受保护资源时触发的。例如,当用户尝试访问一个受保护的页面或 API,但尚未认证时,Spring Security 会引导用户进行 OAuth2 登录授权流程。

具体步骤如下:

  1. 用户访问受保护资源

    用户访问一个需要认证的资源,例如 /protected/resource

  2. 未认证的请求被重定向

    • Spring Security 检测到用户未认证,抛出Access Denied异常。
    • ExceptionTranslationFilter过滤器会捕获Access Denied异常,触发DelegatingAuthenticationEntryPointcommence方法。
    • commence方法继续向下触发LoginUrlAuthenticationEntryPointcommence方法,发起/oauth2/authorization请求。
    • 请求到达 OAuth2AuthorizationRequestRedirectFilter过滤器处理。
  3. OAuth2AuthorizationRequestRedirectFilter 处理请求

    该过滤器匹配到 /oauth2/authorization/{registrationId} 路径后,解析授权请求并将用户重定向到授权服务器的授权端点。即发起/oauth2/authorize请求,交由OAuth2-ServerOAuth2AuthorizationEndpointFilter过滤器处理

  4. 用户进行授权

    用户在授权服务器上进行登录和授权。

  5. 返回授权码

    授权服务器返回授权码到客户端应用的回调 URI,默认路径为/login/oauth2/code/*(这里进入了OAuth2LoginAuthenticationFilter过滤器进行处理)

  6. 客户端交换令牌

    客户端应用使用授权码向授权服务器请求访问令牌(OAuth2LoginAuthenticationFilter的处理过程中发起/oauth2/token请求,用授权码换取令牌)。

  7. 访问受保护资源

    令牌获取成功后,客户端应用使用访问令牌请求受保护的资源(向资源请求重定向)。

源码解析

OAuth2AuthorizationRequestRedirectFilter 是 Spring Security OAuth 2.0 客户端的一部分,用于处理 OAuth 2.0 授权请求的重定向。它会捕获未认证的请求,并将其重定向到 OAuth 2.0 授权服务器以完成认证过程。下面详细讲解该过滤器的工作原理及请求处理流程:

1. 主要成员变量

  • authorizationRequestResolver:用于解析 OAuth 2.0 授权请求。
  • authorizationRequestRepository:用于存储和检索 OAuth 2.0 授权请求。
  • requestCache:用于缓存原始请求,以便在授权后能够恢复。
  • authorizationRedirectStrategy:用于执行重定向操作。

2. 构造方法

过滤器的构造方法根据传入的 ClientRegistrationRepositoryauthorizationRequestBaseUri 初始化 authorizationRequestResolver

java 复制代码
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 方法中实现:

java 复制代码
@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 授权请求所需的所有信息。

  1. 捕获请求并调用 resolve 方法:

    OAuth2AuthorizationRequestRedirectFilter 中,当一个未认证的请求被捕获时,会调用 authorizationRequestResolver.resolve(request) 来解析授权请求。

  2. 调用DefaultOAuth2AuthorizationRequestResolverresolve 方法解析请求:

    因为OAuth2AuthorizationRequestResolver只有一个实现类DefaultOAuth2AuthorizationRequestResolver ,所以会调用它的resolve方法

    java 复制代码
    @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中配置)。

    java 复制代码
    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"

    java 复制代码
    private String getAction(HttpServletRequest request, String defaultAction) {
        String action = request.getParameter("action");
        if (action == null) {
            return defaultAction;
        }
        return action;
    }

    调用 resolve 方法:

    使用 registrationIdredirectUriAction 构建 OAuth2AuthorizationRequest 对象。

    • 获取 ClientRegistration : 使用 registrationIdclientRegistrationRepository 中查找 ClientRegistration 对象。
    • 构建 OAuth2AuthorizationRequest : 使用 ClientRegistration 信息构建 OAuth2AuthorizationRequest,包括 clientIdauthorizationUriredirectUriscopes 以及 state
    • 自定义请求 : 使用 authorizationRequestCustomizer 自定义构建器。
    • 生成重定向url :通过解析请求的完整 URL,并结合 ClientRegistration 中的重定向 URI 模板,构建最终的重定向 URI。
    • 返回 OAuth2AuthorizationRequest 对象 : 通过上面信息返回构建完成的 OAuth2AuthorizationRequest 对象。
    java 复制代码
    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 对象。

    yaml 复制代码
    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 解析请求,具体步骤如下:

  1. 捕获未认证请求并调用 resolve 方法。
  2. 从请求中提取 registrationIdaction
  3. 使用提取的信息和 ClientRegistration 对象构建 OAuth2AuthorizationRequest
  4. 返回构建完成的 OAuth2AuthorizationRequest 对象,用于后续的授权请求重定向。

通过这种方式,Spring Security OAuth 2.0 客户端能够自动处理未认证的请求,并将用户重定向到授权服务器进行认证。

4. 重定向处理

如果解析到授权请求,OAuth2AuthorizationRequestRedirectFilter过滤器的sendRedirectForAuthorization 方法会保存授权请求并执行重定向,来向认证服务发起/oauth2/authorize请求,去获取code

java 复制代码
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 错误:

java 复制代码
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
  • 认证成功后,AbstractAuthenticationProcessingFilterdoFilter 方法处理后续逻辑,包括清除缓存、触发事件和重定向。
  • 认证前的原始请求信息由 RequestCache 保存,并在认证成功后用于重定向。

具体来说,它会处理从授权服务器返回的包含授权码或令牌的回调请求,默认请求路径为/login/oauth2/code/*,大致流程如下:

  1. OAuth2AuthorizationRequestRedirectFilter过滤器向授权服务器发起请求并获取到code后,会向/login/oauth2/code/*请求进行重定向,到达OAuth2LoginAuthenticationFilter
  2. OAuth2LoginAuthenticationFilter 会获取/login/oauth2/code/*回调请求中的授权码即code ,由OidcAuthorizationCodeAuthenticationProvider发起/oauth2/token请求,用code 换取token ,并返回带有access_token的认证结果。
  3. OAuth2LoginAuthenticationFilter 中得到token后,源码执行会在其父类AbstractAuthenticationProcessingFilterdoFilter方法中,向最开始用户发起的资源请求进行重定向,携带令牌访问所需的资源。

主要作用

  1. 处理授权码交换
  • 当授权服务器重定向回客户端应用时,OAuth2LoginAuthenticationFilter 负责处理该回调请求,提取授权码并与授权服务器交换访问令牌。
  1. 处理 OAuth2 登录认证
  • 它会将获取到的访问令牌以及相关的用户信息进行认证处理,将用户信息保存到安全上下文中,从而完成登录。

会过滤的请求

OAuth2LoginAuthenticationFilter 过滤的请求通常是授权服务器重定向回客户端应用的回调请求。这些请求的特征是包含 OAuth2 授权码或者直接包含访问令牌。

默认情况下,这些请求的路径是 "/login/oauth2/code/{registrationId}",其中 {registrationId} 是客户端在 ClientRegistration 中配置的标识符。

请求的发起过程

  1. 用户访问受保护资源

    • 用户尝试访问受保护的资源,但尚未登录。
    • Spring Security 检测到用户未认证,会引导到 OAuth2 登录流程。
  2. 重定向到授权服务器

    • OAuth2AuthorizationRequestRedirectFilter 构建并发送授权请求,用户被重定向到授权服务器进行登录和授权。
  3. 用户在授权服务器上进行授权

    • 用户在授权服务器上进行登录和授权。
    • 授权服务器处理完用户的认证和授权后,将用户重定向回客户端应用的回调 URI(通常包含授权码)。
  4. 回调请求被 OAuth2LoginAuthenticationFilter 处理

    • 用户的浏览器收到授权服务器的重定向响应,导航到客户端应用的回调 URI(如 "/login/oauth2/code/{registrationId}")。
    • OAuth2LoginAuthenticationFilter 拦截并处理这个回调请求。

源码解析

OAuth2LoginAuthenticationFilter继承了AbstractAuthenticationProcessingFilter,所以在查看源码执行断点时,他的doFilter方法是在AbstractAuthenticationProcessingFilter中执行的。

然后AbstractAuthenticationProcessingFilterdoFilter方法中,会调用OAuth2LoginAuthenticationFilterattemptAuthentication方法,此过滤器的核心处理逻辑,都在attemptAuthentication方法中。

代码展示

OAuth2LoginAuthenticationFilter 是处理 OAuth 2.0 登录认证请求的核心过滤器。它负责处理从授权服务器返回的授权响应,并执行相应的认证逻辑。以下是 doFilter 方法的详细解析:

java 复制代码
@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 方法的详细解析:

java 复制代码
@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;
}

详细解释

  1. 将请求参数转换为 MultiValueMap

    java 复制代码
    MultiValueMap<String, String> params = OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap());
    • 将请求参数转换为 MultiValueMap,便于后续处理。
  2. 检查是否是一个授权响应

    java 复制代码
    if (!OAuth2AuthorizationResponseUtils.isAuthorizationResponse(params)) {
        OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST);
        throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
    }
    • 检查请求是否包含授权响应的必要参数,即上面转换的请求参数中是否包含如codestate等关键参数,如果没有则抛出异常。
  3. 从请求中移除授权请求

    java 复制代码
    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进行授权
  4. 获取客户端注册信息

    java 复制代码
    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 信息,即获取请求客户端的信息,如果不存在则抛出异常。
  5. 构建重定向 URI

    java 复制代码
    String redirectUri = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
            .replaceQuery(null)
            .build()
            .toUriString();
    • 构建重定向 URI,用于后续的授权响应处理。
  6. 将请求参数转换为授权响应

    java 复制代码
    OAuth2AuthorizationResponse authorizationResponse = OAuth2AuthorizationResponseUtils.convert(params, redirectUri);
    • 将请求参数转换为 OAuth2AuthorizationResponse 对象。
  7. 构建认证详细信息

    java 复制代码
    Object authenticationDetails = this.authenticationDetailsSource.buildDetails(request);
    • 构建认证详细信息对象。
  8. 创建 OAuth2LoginAuthenticationToken

    java 复制代码
    OAuth2LoginAuthenticationToken authenticationRequest = new OAuth2LoginAuthenticationToken(clientRegistration,
            new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse));
    authenticationRequest.setDetails(authenticationDetails);
    • 创建 OAuth2LoginAuthenticationToken,包含客户端注册信息和授权交换信息。
  9. 执行认证并获取结果

    java 复制代码
    OAuth2LoginAuthenticationToken authenticationResult = (OAuth2LoginAuthenticationToken) this
            .getAuthenticationManager().authenticate(authenticationRequest);
    • 使用 AuthenticationManager 执行认证,并获取认证结果。

    • 这里scope包含oidc,会由OidcAuthorizationCodeAuthenticationProvider进行验证,并返回认证结果

    • OidcAuthorizationCodeAuthenticationProvider内部的验证方法会构建OAuth2AuthorizationCodeGrantRequest请求对象,在其源码内部的getResponse方法中,发起code换token的/oauth2/token请求(请求中还会携带客户端身份验证信息,如果在授权服务中,客户端身份认证不通过,就不会生成token。关于身份验证的流程分析,点此跳转),由授权服务的OAuth2TokenEndpointFilter过滤器处理

      java 复制代码
      //`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_tokenOAuth2LoginAuthenticationToken对象

  10. 转换认证结果为 OAuth2AuthenticationToken

    java 复制代码
    OAuth2AuthenticationToken oauth2Authentication = this.authenticationResultConverter
            .convert(authenticationResult);
    Assert.notNull(oauth2Authentication, "authentication result cannot be null");
    oauth2Authentication.setDetails(authenticationDetails);
    • 将带token的认证结果转换为 OAuth2AuthenticationToken 对象。

  1. 创建 OAuth2AuthorizedClient 并保存

    java 复制代码
    OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(
            authenticationResult.getClientRegistration(), oauth2Authentication.getName(),
            authenticationResult.getAccessToken(), authenticationResult.getRefreshToken());
    
    this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, oauth2Authentication, request, response);
    • 创建 OAuth2AuthorizedClient 对象,并保存到 authorizedClientRepository 中。

到这里时,代码执行会返回到OAuth2LoginAuthenticationFilterdoFilter 方法(断点执行时,代码在其父类AbstractAuthenticationProcessingFilter中),到达successfulAuthentication方法处:

java 复制代码
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方法内部:

主要是将认证通过的客户信息保存到上下文中,并携带得到的令牌向最初发起的资源访问的请求进行重定向

java 复制代码
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实现。

java 复制代码
@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源码:

java 复制代码
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);
}

最后请求资源,请求到达资源服务进行处理。

相关推荐
阿伟*rui2 小时前
配置管理,雪崩问题分析,sentinel的使用
java·spring boot·sentinel
XiaoLeisj4 小时前
【JavaEE初阶 — 多线程】单例模式 & 指令重排序问题
java·开发语言·java-ee
paopaokaka_luck4 小时前
【360】基于springboot的志愿服务管理系统
java·spring boot·后端·spring·毕业设计
dayouziei4 小时前
java的类加载机制的学习
java·学习
Yaml46 小时前
Spring Boot 与 Vue 共筑二手书籍交易卓越平台
java·spring boot·后端·mysql·spring·vue·二手书籍
小小小妮子~6 小时前
Spring Boot详解:从入门到精通
java·spring boot·后端
hong1616886 小时前
Spring Boot中实现多数据源连接和切换的方案
java·spring boot·后端
aloha_7896 小时前
从零记录搭建一个干净的mybatis环境
java·笔记·spring·spring cloud·maven·mybatis·springboot
记录成长java7 小时前
ServletContext,Cookie,HttpSession的使用
java·开发语言·servlet
睡觉谁叫~~~7 小时前
一文解秘Rust如何与Java互操作
java·开发语言·后端·rust