• 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认证方式列表
    	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没有任何对象存储
    		// strategy.getContext()会创建一个空的SecurityContext对象,并且该空的SecurityContext对象会存入ThreadLocal
    		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没有任何对象存储
    		// strategy.getContext()会创建一个空的SecurityContext对象,并且该空的SecurityContext对象会存入ThreadLocal
    		SecurityContext ctx = contextHolder.get();
    
    		if (ctx == null) {
    			ctx = createEmptyContext();
    			contextHolder.set(ctx);
    		}
    
    		return ctx;
    	}
    
    	// 设置当前线程对应的ThreadLocal
    	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");
    			}
    		}
    	}
    
    }
    
  • 相关阅读:
    漏洞复现--鸿运主动安全监控云平台任意文件下载
    【产品经理】微信面试题:微信是将天平倾向于信息发送者还是信息接受者?
    SpringBoot中properties和yml有什么区别?
    打字通小游戏制作教程:用HTML5和JavaScript提升打字速度
    准备好迁移上云了?请收下这份迁移步骤清单
    搭建个人博客,Docsify+Github webhook+JGit解决方案
    算法多重要你还不知道吗?字节大佬把LeetCode前400题的解答笔记都整理好了,头发大佬掉,我们跟着吃经验!
    复制集群架构设计技巧
    探秘PMP和六西格玛的不同:哪一个能为你的职业生涯加分?
    【Lua】VSCode 搭建 Lua 开发环境
  • 原文地址:https://blog.csdn.net/qq_36602071/article/details/127009882