SpringSecurity(三)- SpringSecurity 原理

一、SpringSecurity 过滤器介绍

SpringSecurity采用的是责任链的设计模式,它有一条很长的过滤器链。现在对这条过滤器链的15个过滤器进行说明:

1. WebAsyncManagerIntegrationFilter

将 Security 上下文与 Spring Web 中用于处理异步请求映射的 WebAsyncManager 进行集成。

2. SecurityContextPersistenceFilter

在每次请求处理之前将该请求相关的安全上下文信息加载到 SecurityContextHolder 中,然后在该次请求处理完成之后,将 SecurityContextHolder 中关于这次请求的信息存储到一个“仓储”中,然后将SecurityContextHolder 中的信息清除。
例如在Session中维护一个用户的安全信息就是这个过滤器处理的。

3. HeaderWriterFilter

用于将头信息加入响应中。

4. CsrfFilter

用于处理跨站请求伪造。

5. LogoutFilter

用于处理退出登录。

6. UsernamePasswordAuthenticationFilter

用于处理基于表单的登录请求,从表单中获取用户名和密码。默认情况下处理来自 /login 的请求。
从表单中获取用户名和密码时,默认使用的表单 name 值为 username 和 password,这两个值可以通过设置这个过滤器的usernameParameter 和 passwordParameter 参数来进行修改。

7. DefaultLoginPageGeneratingFilter

如果没有配置登录页面,那系统初始化时就会配置这个过滤器,并且用于在需要进行登录时生成一个登录表单页面。

8. BasicAuthenticationFilter

检测和处理 http basic 认证。

9. RequestCacheAwareFilter

用来处理请求的缓存。

10. SecurityContextHolderAwareRequestFilter

主要是包装请求对象request。

11. AnonymousAuthenticationFilter

检测 SecurityContextHolder 中是否存在 Authentication 对象,如果不存在为其提供一个匿名 Authentication。

12. SessionManagementFilter

管理 session 的过滤器

13. ExceptionTranslationFilter

处理 AccessDeniedException 和 AuthenticationException 异常。

14. FilterSecurityInterceptor

可以看做过滤器链的出口。

15. RememberMeAuthenticationFilter

当用户没有登录而直接访问资源时, 从 cookie 里找出用户的信息, 如果 Spring Security 能够识别出用户提供的remember me cookie, 用户将不必填写用户名和密码, 而是直接登录进入系统,该过滤器默认不开启。

二、SpringSecurity基本流程

Spring Security采取过滤链实现认证与授权,只有当前过滤器通过,才能进入下一个过滤器:

在这里插入图片描述

绿色部分是认证过滤器,需要我们自己配置,可以配置多个认证过滤器。认证过滤器可以使用Spring Security提供的认证过滤器,也可以自定义过滤器(例如:短信验证)。认证过滤器要在configure(HttpSecurity http)方法中配置,没有配置则不生效。下面会重点介绍以下三个过滤器:

UsernamePasswordAuthenticationFilter过滤器:该过滤器会拦截前端提交的 POST 方式的登录表单请求,并进行身份认证。
ExceptionTranslationFilter过滤器:该过滤器不需要我们配置,对于前端提交的请求会直接放行,捕获后续抛出的异常并进行处理(例如:权限访问限制)。
FilterSecurityInterceptor过滤器:该过滤器是过滤器链的最后一个过滤器,根据资源权限配置来判断当前请求是否有权限访问对应的资源。如果访问受限会抛出相关异常,并由ExceptionTranslationFilter过滤器进行捕获和处理。

三、SpringSecurity认证流程

认证流程是在UsernamePasswordAuthenticationFilter过滤器中处理的,具体流程如下所示:

在这里插入图片描述

1. 抽象父类AbstractAuthenticationProcessingFilter,doFilter方法源码

当前端提交一个 POST 方式的登录表单请求,就会被该过滤器拦截,并进行身份认证。该过滤器的 doFilter() 方法实现在其抽象父类AbstractAuthenticationProcessingFilter中,查看相关源码:

private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
			throws IOException, ServletException {
	if (!requiresAuthentication(request, response)) {
		// 1. 判断该请求是否为POST方式的登录表单提交请求,若不是,则直接放行,进入下一个过滤器
		chain.doFilter(request, response);
		return;
	}
	try {
		// 2.attemptAuthentication调用的是子类UsernamePasswordAuthenticationFilter重写的方法进行身份认证
		// Authentication是用来存放用户信息的类
		Authentication authenticationResult = attemptAuthentication(request, response);
		if (authenticationResult == null) {
			return;
		}
		// 3.session策略处理(如果配置了用户session最大并发数,则在此处进行判断并处理)
		this.sessionStrategy.onAuthentication(authenticationResult, request, response);
		// 4.1认证成功的处理
		// continueChainBeforeSuccessfulAuthentication默认为false,所以认证成功后不进入下一个过滤器
		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);
		// 4.2认证失败的处理
		unsuccessfulAuthentication(request, response, failed);
	}
	catch (AuthenticationException ex) {
		// 4.2认证失败的处理
		unsuccessfulAuthentication(request, response, ex);
	}
}

2. 子类UsernamePasswordAuthenticationFilter,attemptAuthentication方法源码

UsernamePasswordAuthenticationFilter重写attemptAuthentication方法进行身份认证

public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
	// 提交的登录表单,用户名参数名称默认为“username”,密码参数名称默认为“password”
	public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
	public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
	// 默认请求方式只能为POST
	private boolean postOnly = true;
	// 提交的登录表单,默认路径是/login,提交方式为POST
	public UsernamePasswordAuthenticationFilter() {
		super(new AntPathRequestMatcher("/login", "POST"));
	}		

	@Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
		// 1. 默认情况下,如果请求不是POST,会抛出异常
		if (this.postOnly && !request.getMethod().equals("POST")) {
			throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
		}
		// 获取请求携带的username 、password 
		String username = obtainUsername(request);
		username = (username != null) ? username : "";
		username = username.trim();
		String password = obtainPassword(request);
		password = (password != null) ? password : "";
		// 3. UsernamePasswordAuthenticationToken是 Authentication 接口的实现类
		// 使用前端传入的username 、password构造Authentication对象,标记该对象为未认证状态
		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
		// 4.允许子类设置“详细信息”属性到Authentication对象中,如:remoteAddress,sessionId
		setDetails(request, authRequest);
		// 5.this.getAuthenticationManager()返回的是AuthenticationManager接口,实现类是ProviderManager,
		// 调用ProviderManager类的authenticate方法进行身份认证
		return this.getAuthenticationManager().authenticate(authRequest);
	}
}

3. Authentication接口的UsernamePasswordAuthenticationToken实现类源码

UsernamePasswordAuthenticationToken是 Authentication 接口的实现类,该类有两个构造器:
一个用于封装前端请求传入的未认证的用户信息,一个用于封装认证成功后的用户信息

public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {

	// 用于封装前端请求传入的未认证的用户信息
	// UsernamePasswordAuthenticationFilter重写attemptAuthentication方法中的authRequest对象就是调用该构造器进行构造的
	public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
		super(null); // 用户权限为null
		this.principal = principal; // 前端传入的用户名
		this.credentials = credentials; // 前端传入的密码
		setAuthenticated(false); // 标记为未认证状态
	}

	// 用于封装认证成功的用户信息 
	public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
		super(authorities); // 用户权限集合
		this.principal = principal; // 封装认证用户信息的UserDetail对象,不再是用户名
		this.credentials = credentials; // 前端传入的密码
		super.setAuthenticated(true); // 标记为认证成功状态
	}
}

4. Authentication 接口源码

Authentication 接口的实现类用于存储用户认证信息

一旦 AuthenticationManager.authenticate(Authentication) 方法处理了请求,则表示身份验证请求或经过身份验证的主体的令牌。一旦请求通过身份验证,身份验证通常将存储在由正在使用的身份验证机制的 SecurityContextHolder 管理的线程本地 SecurityContext 中。通过创建 Authentication 实例并使用代码,可以在不使用 Spring Security 的身份验证机制之一的情况下实现显式身份验证:
SecurityContext context = SecurityContextHolder.createEmptyContext(); context.setAuthentication(anAuthentication);
SecurityContextHolder.setContext(context);

请注意,除非 Authentication 将 authenticated 属性设置为 true,否则它仍将由遇到它的任何安全拦截器(用于方法或 Web 调用)进行身份验证。在大多数情况下,框架会透明地为您管理安全上下文和身份验证对象。

public interface Authentication extends Principal, Serializable {
	
	// 用户权限集合
	// 由 AuthenticationManager 设置以指示已授予主体的权限。
	// 请注意,类不应依赖此值作为有效值,除非它已由受信任的 AuthenticationManager 设置。
	// 实现应确保对返回集合数组的修改不会影响 Authentication 对象的状态,或使用不可修改的实例。
	Collection<? extends GrantedAuthority> getAuthorities();

	// 用户密码
	// 证明委托人正确的凭据。这通常是一个密码,但可以是与 AuthenticationManager 相关的任何内容。调用者应填充凭据。
	Object getCredentials();

	// 存储有关身份验证请求的其他详细信息。这些可能是 IP 地址、证书序列号等。
	Object getDetails();

	// 被认证的主体的身份。在使用用户名和密码的身份验证请求的情况下,这将是用户名。调用者应填充身份验证请求的主体。 
	// AuthenticationManager 实现通常会返回一个包含更丰富信息的 Authentication 作为应用程序使用的主体。
	// 许多身份验证提供程序将创建一个 UserDetails 对象作为主体。
	Object getPrincipal();

	// 用于向 AbstractSecurityInterceptor 指示它是否应该向 AuthenticationManager 提供身份验证令牌。
	// 通常,AuthenticationManager(或者更常见的是它的 AuthenticationProviders 之一)将在成功认证后返回一个不可变的认证令牌,在这种情况下,该令牌可以安全地向该方法返回 true。
	// 返回 true 将提高性能,因为不再需要为每个请求调用 AuthenticationManager。
	// 出于安全原因,这个接口的实现应该非常小心地从这个方法返回 true,除非它们是不可变的,或者有某种方式确保属性自最初创建以来没有被更改。
	boolean isAuthenticated();

	// 设置是否被认证
	// 实现应始终允许使用 false 参数调用此方法,因为各种类使用它来指定不应信任的身份验证令牌。
	// 如果实现希望拒绝使用 true 参数的调用(这将表明身份验证令牌是可信的 - 潜在的安全风险),则实现应该抛出 IllegalArgumentException。
	void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;

}

5. ProviderManager 源码

UsernamePasswordAuthenticationFilter过滤器的 attemptAuthentication() 方法将未认证的 Authentication 对象传入 ProviderManager 类的 authenticate() 方法进行身份认证。
ProviderManager 是 AuthenticationManager 接口的实现类,该接口是认证相关的核心接口,也是认证的入口。

在实际开发中,我们可能有多种不同的认证方式,例如:用户名+密码、邮箱+密码、手机号+验证码等,而这些认证方式的入口始终只有一个,那就是 AuthenticationManager。在该接口的常用实现类 ProviderManager 内部会维护一个List< AuthenticationProvider>列表,存放多种认证方式,实际上这是委托者模式(Delegate)的应用。每种认证方式对应着一个 AuthenticationProvider,AuthenticationManager 根据认证方式的不同(根据传入的 Authentication 类型判断)委托对应的 AuthenticationProvider 进行用户认证。

// 传入未认证的Authentication对象
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
	// 1.获取传入的Authentication类型,即UsernamePasswordAuthenticationToken.class
	Class<? extends Authentication> toTest = authentication.getClass();
	AuthenticationException lastException = null;
	AuthenticationException parentException = null;
	Authentication result = null;
	Authentication parentResult = null;
	int currentPosition = 0;
	int size = this.providers.size();
	// 2.遍历List<AuthenticationProvider>认证方式列表
	for (AuthenticationProvider provider : getProviders()) {
		// 3.判断当前AuthenticationProvider是否适用UsernamePasswordAuthenticationToken.class类型的Authentication
		if (!provider.supports(toTest)) {
			continue;
		}
		if (logger.isTraceEnabled()) {
			logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
					provider.getClass().getSimpleName(), ++currentPosition, size));
		}
		// 成功找到适配当前认证方式的AuthenticationProvider,此处为DaoAuthenticationProvider
		try {
			// 4.调用DaoAuthenticationProvider的authenticate方法进行认证
			result = provider.authenticate(authentication);
			// 4.1如果认证成功,会返回一个已认证标记的Authentication对象
			if (result != null) {
				// 5.1认证成功后,将传入的Authentication对象中的details信息拷贝到已认证的Authentication对象
				copyDetails(authentication, result);
				break;
			}
		}
		catch (AccountStatusException | InternalAuthenticationServiceException ex) {
			prepareException(ex, authentication);		
			throw ex;
		}
		catch (AuthenticationException ex) {
			lastException = ex;
		}
	}
	if (result == null && this.parent != null) {
		try {
			// 5.2认证失败后,使用父类型的AuthenticationManager进行认证
			parentResult = this.parent.authenticate(authentication);
			result = parentResult;
		}
		catch (ProviderNotFoundException ex) {
			
		}
		catch (AuthenticationException ex) {
			parentException = ex;
			lastException = ex;
		}
	}
	if (result != null) {
		// 6.认证成功后,去除result敏感信息,要求相关类实现CredentialsContainer接口
		if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
			// 6.1去除result敏感信息的过程就是调用CredentialsContainer接口的eraseCredentials方法
			((CredentialsContainer) result).eraseCredentials();
		}
		// 7.发布认证成功事件
		if (parentResult == null) {
			this.eventPublisher.publishAuthenticationSuccess(result);
		}
	
		return result;
	}
	
	// 8.认证失败后,抛出失败的异常信息
	if (lastException == null) {
		lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound",
				new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}"));
	}
	if (parentException == null) {
		prepareException(lastException, authentication);
	}
	throw lastException;
}

6. eraseCredentials方法源码

调用 CredentialsContainer 接口定义的 eraseCredentials() 方法去除敏感信息。查看 UsernamePasswordAuthenticationToken 实现的 eraseCredentials() 方法,该方法实现在其父类AbstractAuthenticationToken中:

// 实现了CredentialsContainer接口
public abstract class AbstractAuthenticationToken implements Authentication, CredentialsContainer {
	@Override
	public void eraseCredentials() {
		// credentials(前端传入的密码)会置为null
		eraseSecret(getCredentials());
		// principal在已认证的Authentication中是UserDetails实现类
		// 如果该实现类想要去除敏感信息,需要实现CredentialsContainer接口的eraseCredentials方法
		// 由于我们自定义的User类没有实现该接口,所以不进行任何操作
		eraseSecret(getPrincipal());
		eraseSecret(this.details);
	}
	
	private void eraseSecret(Object secret) {
		if (secret instanceof CredentialsContainer) {
			((CredentialsContainer) secret).eraseCredentials();
		}
	}
}	

7. 认证成功/失败处理源码

在父类AbstractAuthenticationProcessingFilter的doFilter方法会调用认证成功/失败的方法

successfulAuthentication 源码:

protected void successfulAuthentication(HttpServletRequest request,
			HttpServletResponse response, FilterChain chain, Authentication authResult)
			throws IOException, ServletException {

		if (logger.isDebugEnabled()) {
			logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
					+ authResult);
		}

		// 1.将认证成功后的用户信息Authentication对象封装进SecurityContext中,并存入SecurityContextHolder
		// SecurityContextHolder封装了ThreadLocal
		SecurityContextHolder.getContext().setAuthentication(authResult);

		// 2.rememberMe的处理
		rememberMeServices.loginSuccess(request, response, authResult);

		// Fire event
		if (this.eventPublisher != null) {
			// 3.发布认证成功事件
			eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
		}
		// 4.调用认证成功处理器
		successHandler.onAuthenticationSuccess(request, response, authResult);
}

unsuccessfulAuthentication 源码:

protected void unsuccessfulAuthentication(HttpServletRequest request,
			HttpServletResponse response, AuthenticationException failed)
			throws IOException, ServletException {
		// 清除该线程在SecurityContextHolder中对应的SecurityContext对象
		SecurityContextHolder.clearContext();

		if (logger.isDebugEnabled()) {
			logger.debug("Authentication request failed: " + failed.toString(), failed);
			logger.debug("Updated SecurityContextHolder to contain null Authentication");
			logger.debug("Delegating to authentication failure handler " + failureHandler);
		}
		// 2. rememberMe处理
		rememberMeServices.loginFail(request, response);
		// 3. 调用认证失败处理器
		failureHandler.onAuthenticationFailure(request, response, failed);
}

四、 SpringSecurity 权限访问流程

权限访问流程,主要是对ExceptionTranslationFilter 过滤器和 FilterSecurityInterceptor 过滤器进行介绍。

1. ExceptionTranslationFilter 源码

该过滤器是用于处理异常的,不需要我们配置,对于前端提交的请求会直接放行,捕获后续抛出的异常并进行处理(例如:权限访问限制)

public class ExceptionTranslationFilter extends GenericFilterBean {
	public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;

		try {
			// 1.对于前端提交的请求会直接放行,不进行拦截
			chain.doFilter(request, response);

			logger.debug("Chain processed normally");
		}
		catch (IOException ex) {
			throw ex;
		}
		catch (Exception ex) {
			// 2.尝试从堆栈跟踪中提取 SpringSecurityException
			Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
			// 2.1访问需要认证的资源时,请求未认证则抛出此异常
			RuntimeException ase = (AuthenticationException) throwableAnalyzer.getFirstThrowableOfType(AuthenticationException.class, causeChain);

			if (ase == null) {
				// 2.2访问被拒绝时的异常
				ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
			}

			if (ase != null) {
				if (response.isCommitted()) {
					throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", ex);
				}
				handleSpringSecurityException(request, response, chain, ase);
			}
			else {
				// Rethrow ServletExceptions and RuntimeExceptions as-is
				if (ex instanceof ServletException) {
					throw (ServletException) ex;
				}
				else if (ex instanceof RuntimeException) {
					throw (RuntimeException) ex;
				}

				// Wrap other Exceptions. This shouldn't actually happen
				// as we've already covered all the possibilities for doFilter
				throw new RuntimeException(ex);
			}
		}
	}
}

2. FilterSecurityInterceptor 源码

FilterSecurityInterceptor 是过滤器链的最后一个过滤器,根据资源权限配置来判断当前请求是否有权限访问对应的资源。如果
访问受限会抛出相关异常,最终所抛出的异常会由前一个过滤器ExceptionTranslationFilter 进行捕获和处理。

public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
	// 先调用过滤器的doFilter方法
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
		FilterInvocation fi = new FilterInvocation(request, response, chain);
		// 再调用invoke方法
		invoke(fi);
	}

	public void invoke(FilterInvocation fi) throws IOException, ServletException {
		if ((fi.getRequest() != null)
				&& (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
				&& observeOncePerRequest) {
			// filter already applied to this request and user wants us to observe
			// once-per-request handling, so don't re-do security checking
			fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
		}
		else {
			// first time this request being called, so perform security checking
			if (fi.getRequest() != null && observeOncePerRequest) {
				fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
			}

			// 1.根据资源权限配置来判断当前请求是否有权限访问对应的资源。如果访问受限会抛出相关异常
			InterceptorStatusToken token = super.beforeInvocation(fi);

			try {
				// 2.访问相关资源时,根据在 SpringMVC 的核心组件DispatcherServlet进行访问
				fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
			}
			finally {
				super.finallyInvocation(token);
			}

			super.afterInvocation(token, null);
		}
	}
}

需要注意,Spring Security 的过滤器链是配置在 SpringMVC 的核心组件DispatcherServlet 运行之前。也就是说,请求通过 Spring Security 的所有过滤器,不意味着能够正常访问资源,该请求还需要通过 SpringMVC 的拦截器链。

五、 SpringSecurity 请求间共享认证信息

一般认证成功后的用户信息是通过 Session 在多个请求之间共享, Spring Security 会将已认证的用户信息对象 Authentication 与 Session 进行绑定

在这里插入图片描述
在前面讲解认证成功的处理方法 successfulAuthentication() 时,有以下代码:

protected void successfulAuthentication() {
			
		...	

		// 1.将认证成功后的用户信息Authentication对象封装进SecurityContext中,并存入SecurityContextHolder
		// SecurityContextHolder封装了ThreadLocal
		SecurityContextHolder.getContext().setAuthentication(authResult);	
		
		...
}

1. SecurityContext 源码

SecurityContextImpl是SecurityContext接口的实现类, 主要对Authentication进行封装

public class SecurityContextImpl implements SecurityContext {
    public SecurityContextImpl(Authentication authentication) {
        this.authentication = authentication;
    }
}

2. SecurityContextHolder 源码

SecurityContextHolder类其实是对ThreadLocal的封装 , 存储SecurityContext对象

将给定的 SecurityContext 与当前执行线程相关联。此类提供了一系列委托给 SecurityContextHolderStrategy 实例的静态方法。该类的目的是提供一种方便的方法来指定应该用于给定 JVM 的策略。这是 JVM 范围的设置,因为此类中的所有内容都是静态的,以便于调用代码时使用。要指定应使用哪种策略,您必须提供模式设置。模式设置是定义为Static fields字段的三个有效 MODE_ 设置之一,或者是提供公共无参数构造函数的 SecurityContextHolderStrategy 具体实现的完全限定类名。有两种方法可以指定所需的策略模式字符串。第一种是通过在 SYSTEM_PROPERTY 上键入的系统属性来指定它。第二种是在使用类之前调用 setStrategyName(String)。如果这两种方法都没有使用,则该类将默认使用MODE_THREADLOCAL,它向后兼容,具有较少的 JVM 不兼容性并且适用于服务器(而 MODE_GLOBAL 绝对不适合服务器使用)。

public class SecurityContextHolder {
	// ~ Static fields/initializers
	// =====================================================================================

	public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL";
	public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL";
	public static final String MODE_GLOBAL = "MODE_GLOBAL";
	public static final String SYSTEM_PROPERTY = "spring.security.strategy";
	private static String strategyName = System.getProperty(SYSTEM_PROPERTY);
	private static SecurityContextHolderStrategy strategy;
	private static int initializeCount = 0;

	static {
		initialize();
	}

	// ~ Methods
	// ========================================================================================================

	/**
	 * 从当前线程显式清除上下文值
	 */
	public static void clearContext() {
		strategy.clearContext();
	}

	/**
	 * 获取当前的 SecurityContext
	 */
	public static SecurityContext getContext() {
		// 注意:如果当前线程对应的ThreadLocal<SecurityContext>没有任何对象存储
		// strategy.getContext()会创建一个空的SecurityContext对象,并且该空的SecurityContext对象会存入ThreadLocal<SecurityContext>
		return strategy.getContext();
	}

    /**
	 * 将新的 SecurityContext 与当前执行线程相关联
	 */
	public static void setContext(SecurityContext context) {
		strategy.setContext(context);
	}

	/**
	 * 主要出于故障排除目的,此方法显示该类重新初始化其 SecurityContextHolderStrategy 的次数
	 *
	 * 返回:计数(应该是一,除非您调用 setStrategyName(String) 来切换到备用策略
	 */
	public static int getInitializeCount() {
		return initializeCount;
	}

	private static void initialize() {
		if (!StringUtils.hasText(strategyName)) {
			// 默认使用MODE_THREADLOCAL模式
			strategyName = MODE_THREADLOCAL;
		}

		if (strategyName.equals(MODE_THREADLOCAL)) {
			// 默认使用ThreadLocalSecurityContextHolderStrategy创建Strategy,其内部使用ThreadLocal对SecurityContext进行存储
			strategy = new ThreadLocalSecurityContextHolderStrategy();
		}

		initializeCount++;
	}		
	...
}

3. ThreadLocalSecurityContextHolderStrategy 源码

final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {
	// ~ Static fields/initializers
	// =====================================================================================

	// 使用ThreadLocal存储SecurityContext
	private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<>();

	// ~ Methods
	// ========================================================================================================

	public void clearContext() {
		contextHolder.remove();
	}

	public SecurityContext getContext() {
	  	// 注意:如果当前线程对应的ThreadLocal<SecurityContext>没有任何对象存储
		// strategy.getContext()会创建一个空的SecurityContext对象,并且该空的SecurityContext对象会存入ThreadLocal<SecurityContext>
		SecurityContext ctx = contextHolder.get();

		if (ctx == null) {
			ctx = createEmptyContext();
			contextHolder.set(ctx);
		}

		return ctx;
	}

	// 设置当前线程对应的ThreadLocal<SecurityContext>
	public void setContext(SecurityContext context) {
		Assert.notNull(context, "Only non-null SecurityContext instances are permitted");
		contextHolder.set(context);
	}

	public SecurityContext createEmptyContext() {
		// 创建一个空的SecurityContext对象
		return new SecurityContextImpl();
	}
}

4. SecurityContextPersistenceFilter 源码

前面提到过,在 UsernamePasswordAuthenticationFilter 过滤器认证成功之后,会在认证成功的处理方法中将已认证的用户信息对象 Authentication 封装进SecurityContext,并存入 SecurityContextHolder。

之后,响应会通过 SecurityContextPersistenceFilter 过滤器,该过滤器的位置在所有过滤器的最前面,请求到来时,先进入该过滤器;响应返回时,最后一个通过它,所以在该过滤器中会处理已认证的用户信息对象 Authentication 与 Session 的绑定。

认证成功的响应通过 SecurityContextPersistenceFilter 过滤器时,会从SecurityContextHolder 中取出封装了已认证用户信息对象 Authentication 的SecurityContext,放进 Session 中。当请求再次到来时,请求首先经过该过滤器,该过滤器会判断当前请求的 Session 是否存有 SecurityContext 对象,如果有则将该对象取出再次放入 SecurityContextHolder 中,之后该请求所在的线程获得认证用户信息,后续的资源访问不需要进行身份认证;当响应再次返回时,该过滤器同样从 SecurityContextHolder 取出SecurityContext 对象,放入 Session 中。

public class SecurityContextPersistenceFilter extends GenericFilterBean {

	// 执行该过滤器的doFilter方法
	public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {

		static final String FILTER_APPLIED = "__spring_security_scpf_applied";
		
		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;

		if (request.getAttribute(FILTER_APPLIED) != null) {
			// 确保每个请求只应用一次过滤器
			chain.doFilter(request, response);
			return;
		}

		final boolean debug = logger.isDebugEnabled();

		request.setAttribute(FILTER_APPLIED, Boolean.TRUE);

		if (forceEagerSessionCreation) {
			HttpSession session = request.getSession();

			if (debug && session.isNew()) {
				logger.debug("Eagerly created session: " + session.getId());
			}
		}

		HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
		// 1.请求到来时,检查当前session中是否存有SecurityContext
		// 若有,则从session中取出SecurityContext;若没有,则创建一个空的SecurityContext
		SecurityContext contextBeforeChainExecution = repo.loadContext(holder);

		try {
			// 2.将上述获得的SecurityContext存入SecurityContextHolder
			SecurityContextHolder.setContext(contextBeforeChainExecution);
			// 3.进入下一个过滤器
			chain.doFilter(holder.getRequest(), holder.getResponse());

		}
		finally {
			// 4.响应返回时,从SecurityContextHolder取出SecurityContext 
			SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
			// 5. 删除 SecurityContextHolder 内容的关键 - 在其他任何事情之前执行此操作
			SecurityContextHolder.clearContext();
			// 6.将取出的SecurityContext存入session,实现请求间共享认证信息
			repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
			request.removeAttribute(FILTER_APPLIED);

			if (debug) {
				logger.debug("SecurityContextHolder now cleared, as request processing completed");
			}
		}
	}

}
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值