Spring Security系列(24)- SecurityContext源码解析

前言

在Security中,登录以后,我们可以通过以下代码获取当前用户的认证信息:

        SecurityContext context = SecurityContextHolder.getContext();
        Authentication authentication = context.getAuthentication();

在SecurityContext 中,包含了用户信息、账号状态、拥有的权限等信息:
在这里插入图片描述
在获取用户信息的代码中,通过SecurityContextHolder获取SecurityContext ,然后在SecurityContext 中获取Authentication ,那么SecurityContext 就是保存用户认证的地方了,那么这个时候就有几个疑问需要了解:

  • SecurityContext 是什么?
  • 用户登录后,认证信息保存在哪里?
  • 已登录用户访问,是如何获取认证信息的?
  • 如何保证返回的是当前登录用户的信息?

带着以上疑问,我们分析下相关源码,进行解析

核心类

1. SecurityContextPersistenceFilter

SecurityContextPersistenceFilter是负责SecurityContext处理的过滤器,主要负责:

  • 认证成功后,将SecurityContext保存到Session中
  • 访问时,从Session中获取SecurityContext,设置到当前线程中

2. SecurityContext

SecurityContext安全上下文,主要是存储了认证信息,我们可以通过获取SecurityContext,来获取已认证的用户信息,提供了获取和设置Authentication 的两个方法:

public interface SecurityContext extends Serializable {
    
    
    Authentication getAuthentication();

    void setAuthentication(Authentication var1);
}

它只有一个实现类SecurityContextImpl,其内部维护了一个Authentication 对象。

public class SecurityContextImpl implements SecurityContext {
    
    
    private Authentication authentication;
}

3. SecurityContextRepository

SecurityContextRepository接口定义了一些SecurityContext持久化和查询的方法:

public interface SecurityContextRepository {
    
    
	// 加载(获取)SecurityContext 
    SecurityContext loadContext(HttpRequestResponseHolder var1);
	// 保存SecurityContext 
    void saveContext(SecurityContext var1, HttpServletRequest var2, HttpServletResponse var3);
	
    boolean containsContext(HttpServletRequest var1);
}

SecurityContextRepository只有两个实现类HttpSessionSecurityContextRepository、NullSecurityContextRepository。

HttpSessionSecurityContextRepository会将SecurityContext保存在Session中,获取的时候,从Session中查询。

NullSecurityContextRepository则不会存储SecurityContext。

4. SecurityContextHolder

SecurityContextHolder意为SecurityContex拥有者,我们可以通过它在代码中来获取SecurityContex,进而获取当前登陆用户信息。

SecurityContextHolder有一个重要的属性SecurityContextHolderStrategy。

   private static SecurityContextHolderStrategy strategy;

5. SecurityContextHolderStrategy

SecurityContextRepository负责持久化存储SecurityContext,SecurityContextHolder则负责将持久化的数据,设置到相关使用场景中,方便获取。而SecurityContextHolderStrategy就是SecurityContextHolder使用策略。

SecurityContextHolderStrategy 定义个获取、删除、设置、创建等操作SecurityContext的方法

public interface SecurityContextHolderStrategy {
    
    
    void clearContext();

    SecurityContext getContext();

    void setContext(SecurityContext var1);

    SecurityContext createEmptyContext();
}

其有三个实现类:
在这里插入图片描述

  • GlobalSecurityContextHolderStrategy:SecurityContext为静态属性,所有在该类下的对象共享这一个属性,所以适用于应用从开启到关闭的整个生命周期只有一个用户在使用。
  • ThreadLocalSecurityContextHolderStrategy(默认策略):利用ThreadLocal保存多个SecurityContext(安全上下文),每个线程都可以利用ThreadLocal获取其自己的SecurityContext安全上下文。
  • InheritableThreadLocalSecurityContextHolderStrategy:使用ThreadLocal的子类InheritableThreadLocal来实现,子线程也可以获取到(本来父线程的本地变量是无法传递给子线程的)。

1. 项目初始化阶段

项目在启动过程中,肯定会根据配置类加载各种对象,比如以下配置,我们配置了一些放行路径、处理器、session创建策略等。

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        // 关闭csrf,开启跨域支持
        http.csrf().disable().cors();
        http.authorizeRequests()
                .antMatchers( "/sms/send/code", "/sms/login","/permission/**").permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .formLogin()
                .failureHandler(authenticationFailureHandler);
                //.successHandler(authenticationSuccessHandler); // 配置登录失败处理器
        http.exceptionHandling().accessDeniedHandler(accessDeniedHandler); // 配置DeniedHandler处理器
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);
        // 添加手机号短信登录
        http.apply(smsSecurityConfigurerAdapter);
    }

HttpSecurity 配置加载过程中,SessionManagementConfigurer(Session管理配置类)会对SecurityContextRepository(安全上下文存储仓库)进行初始化。

    public void init(H http) {
    
    
    	// 1. 获取HTTPSecurity 共享对象 SecurityContextRepository
        SecurityContextRepository securityContextRepository = (SecurityContextRepository)http.getSharedObject(SecurityContextRepository.class);
        boolean stateless = this.isStateless();
        // 2. 不存在 则会创建
        if (securityContextRepository == null) {
    
    
        	// 2.1 如果设置了不需要,设置SecurityContextRepository为NullSecurityContextRepository
            if (stateless) {
    
    
                http.setSharedObject(SecurityContextRepository.class, new NullSecurityContextRepository());
            } else {
    
    
            	// 2.2 需要则会创建HttpSessionSecurityContextRepository(基于Session存储安全上下文)
                HttpSessionSecurityContextRepository httpSecurityRepository = new HttpSessionSecurityContextRepository();
                // 是否启用会话 URL 重写
                httpSecurityRepository.setDisableUrlRewriting(!this.enableSessionUrlRewriting);
                // 是否允许创建会话 
                httpSecurityRepository.setAllowSessionCreation(this.isAllowSessionCreation());
                AuthenticationTrustResolver trustResolver = (AuthenticationTrustResolver)http.getSharedObject(AuthenticationTrustResolver.class);
                if (trustResolver != null) {
    
    
                    httpSecurityRepository.setTrustResolver(trustResolver);
                }
				// 设置身份验证信任解析器
                http.setSharedObject(SecurityContextRepository.class, httpSecurityRepository);
            }
        }
		// 3. HttpSecurity添加一些共享对象。
        RequestCache requestCache = (RequestCache)http.getSharedObject(RequestCache.class);
        if (requestCache == null && stateless) {
    
    
            http.setSharedObject(RequestCache.class, new NullRequestCache());
        }

        http.setSharedObject(SessionAuthenticationStrategy.class, this.getSessionAuthenticationStrategy(http));
        http.setSharedObject(InvalidSessionStrategy.class, this.getInvalidSessionStrategy());
    }

上述代码中,创建了HttpSessionSecurityContextRepository 对象用于存储SecurityContext在Session中,在使用无参构造器new 这个对象的过程中,学过JVM的应该知道,在调用构造器之前成员属性会先进行初始化,而在这个类中,有下面这样一个属性:

    private final Object contextObject = SecurityContextHolder.createEmptyContext();

contextObject属性会调用SecurityContextHolder(安全上下文拥有者)来创建一个空的上下文,而在调用SecurityContextHolder类的时候,因为他有一个静态代码块,所以会执行static代码块进行初始化:

    static {
    
    
        initialize();
    }

代码块中的initialize方法主要是创建SecurityContextHolder相关的策略。

    private static void initialize() {
    
    
    	// 1. SecurityContext 策略名称不存在,则设置为MODE_THREADLOCAL(THREADLOCAL模式)
        if (!StringUtils.hasText(strategyName)) {
    
    
            strategyName = "MODE_THREADLOCAL";
        }
		// 2. 创建ThreadLocalSecurityContextHolderStrategy(ThreadLocal安全上下文策略)
        if (strategyName.equals("MODE_THREADLOCAL")) {
    
    
            strategy = new ThreadLocalSecurityContextHolderStrategy();
        } else if (strategyName.equals("MODE_INHERITABLETHREADLOCAL")) {
    
    
        	// 3. InheritableThreadLocal 策略()
            strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
        } else if (strategyName.equals("MODE_GLOBAL")) {
    
    
        	// 4. 全局策略
            strategy = new GlobalSecurityContextHolderStrategy();
        } else {
    
    
            try {
    
    
            	// 5. 自定义策略
                Class<?> clazz = Class.forName(strategyName);
                Constructor<?> customStrategy = clazz.getConstructor();
                strategy = (SecurityContextHolderStrategy)customStrategy.newInstance();
            } catch (Exception var2) {
    
    
                ReflectionUtils.handleReflectionException(var2);
            }
        }
		// 6. 计数器initializeCount +1
        ++initializeCount;
    }

静态代码块执行完成后,HttpSessionSecurityContextRepository 开始初始化成员属性contextObject :

private final Object contextObject = SecurityContextHolder.createEmptyContext();

createEmptyContext方法会创建一个空的SecurityContext,调用的是ThreadLocalSecurityContextHolderStrategy中的方法。

    public static SecurityContext createEmptyContext() {
    
    
        return strategy.createEmptyContext();
    }

ThreadLocalSecurityContextHolderStrategy直接new 了一个SecurityContextImpl对象。

    public SecurityContext createEmptyContext() {
    
    
        return new SecurityContextImpl();
    }

SecurityContextImpl类实现了SecurityContext,它包含了一个Authentication对象。

public class SecurityContextImpl implements SecurityContext {
    
    
    private static final long serialVersionUID = 550L;
    private Authentication authentication;
}

至此关于HttpSessionSecurityContextRepository 的初始化就已经完成了。

2. 登录处理SecurityContext

我们知道Security登录和访问,都是由过滤器链来完成的,SecurityContext处理是由SecurityContextPersistenceFilter(SecurityContext持久化过滤器)来完成的。

用户输入账号密码后,进入SecurityContextPersistenceFilter,执行以下过滤方法:

    private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
    
    
    	// 1. 判断是否已执行当前逻辑,已执行则直接放行
        if (request.getAttribute("__spring_security_scpf_applied") != null) {
    
    
            chain.doFilter(request, response);
        } else {
    
    
        	// 2. 设置已执行标记
            request.setAttribute("__spring_security_scpf_applied", Boolean.TRUE);
            // 3. 如果启用了并发会话控制,获取Session
            if (this.forceEagerSessionCreation) {
    
    
                HttpSession session = request.getSession();
                if (this.logger.isDebugEnabled() && session.isNew()) {
    
    
                    this.logger.debug(LogMessage.format("Created session %s eagerly", session.getId()));
                }
            }
			// 4. 创建HttpRequestResponseHolder (理解为包含了request、response的一个对象)
            HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
            // 5. 存储库中,获取SecurityContext
            SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder);
            boolean var10 = false;

            try {
    
    
                var10 = true;
                // 6. SecurityContextHolder设置Context(这里SecurityContext是未认证的)
                SecurityContextHolder.setContext(contextBeforeChainExecution);
                if (contextBeforeChainExecution.getAuthentication() == null) {
    
    
                    this.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());
                var10 = false;
            // 7. 经过其他过滤器后(认证完成),后续最终处理
            } finally {
    
    
                if (var10) {
    
    
                    SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
                    SecurityContextHolder.clearContext();
                    this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
                    request.removeAttribute("__spring_security_scpf_applied");
                    this.logger.debug("Cleared SecurityContextHolder to complete request");
                }
            }
			// 8.获取SecurityContext
            SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
            // 9. 清理SecurityContext
            SecurityContextHolder.clearContext();
            // 10. 新的SecurityContext 保存到存储库中
            this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
            request.removeAttribute("__spring_security_scpf_applied");
            this.logger.debug("Cleared SecurityContextHolder to complete request");
        }
    }

在过滤器中,会在存储库中,获取SecurityContext,这个方法主要是Session中获取SecurityContext ,没有则会创建一个空的,并设置到Session 中。

    public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
    
    
    	// 1. 获取request 、response、httpSession 
        HttpServletRequest request = requestResponseHolder.getRequest();
        HttpServletResponse response = requestResponseHolder.getResponse();
        HttpSession httpSession = request.getSession(false);
        // 2. 从Session 中获取键为SPRING_SECURITY_CONTEXT的属性
        SecurityContext context = this.readSecurityContextFromSession(httpSession);
        // 3. 登录时,是没有的,所以会创建一个空的SecurityContext (SecurityContextImpl)
        if (context == null) {
    
    
            context = this.generateNewContext();
            if (this.logger.isTraceEnabled()) {
    
    
                this.logger.trace(LogMessage.format("Created %s", context));
            }
        }
		// 4. 设置新的request 、response、httpSession(SPRING_SECURITY_CONTEXT) 
        HttpSessionSecurityContextRepository.SaveToSessionResponseWrapper wrappedResponse = new HttpSessionSecurityContextRepository.SaveToSessionResponseWrapper(response, request, httpSession != null, context);
        requestResponseHolder.setResponse(wrappedResponse);
        requestResponseHolder.setRequest(new HttpSessionSecurityContextRepository.SaveToSessionRequestWrapper(request, wrappedResponse));
        return context;
    }

接下来看下过滤器中使用SecurityContextHolder的getContext方法是如何执行的:

	// getContext或从SecurityContextHolder的存储库中获取Context
    public static SecurityContext getContext() {
    
    
        return strategy.getContext();
    }

因为模式使用的是ThreadLocalSecurityContextHolderStrategy,所以会ThreadLocal中获取SecurityContext,清理方法也是直接移除当前ThreadLocal中的信息:

    public SecurityContext getContext() {
    
    
        SecurityContext ctx = (SecurityContext)contextHolder.get();
        if (ctx == null) {
    
    
            ctx = this.createEmptyContext();
            contextHolder.set(ctx);
        }

        return ctx;
    }

最后看下saveContext是如何存储SecurityContext的,saveContext最终调用的是HttpSessionSecurityContextRepository内部类SaveToSessionResponseWrapper中的方法,

        protected void saveContext(SecurityContext context) {
    
    
        	// 1. 获取认证对象,Session、Key(SPRING_SECURITY_CONTEXT)
            Authentication authentication = context.getAuthentication();
            HttpSession httpSession = this.request.getSession(false);
            String springSecurityContextKey = HttpSessionSecurityContextRepository.this.springSecurityContextKey;
            if (authentication != null && !HttpSessionSecurityContextRepository.this.trustResolver.isAnonymous(authentication)) {
    
    
                httpSession = httpSession != null ? httpSession : this.createNewSessionIfAllowed(context, authentication);
                if (httpSession != null && (this.contextChanged(context) || httpSession.getAttribute(springSecurityContextKey) == null)) {
    
    
                	// 2. 设置到Session中
                    httpSession.setAttribute(springSecurityContextKey, context);
                    this.isSaveContextInvoked = true;
                    if (this.logger.isDebugEnabled()) {
    
    
                        this.logger.debug(LogMessage.format("Stored %s to HttpSession [%s]", context, httpSession));
                    }
                }

            } else {
    
    
                if (httpSession != null && this.authBeforeExecution != null) {
    
    
                    httpSession.removeAttribute(springSecurityContextKey);
                    this.isSaveContextInvoked = true;
                }

                if (this.logger.isDebugEnabled()) {
    
    
                    if (authentication == null) {
    
    
                        this.logger.debug("Did not store empty SecurityContext");
                    } else {
    
    
                        this.logger.debug("Did not store anonymous SecurityContext");
                    }
                }

            }
        }

至此,整个登录处理SecurityContext 流程就结束了,总计以下:

  1. 进入SecurityContextPersistenceFilter过滤器,创建一个空的SecurityContext
  2. 后续过滤器认证通过后,在当前线程获取SecurityContext
  3. 将SecurityContext保存到Session中

3. 再次访问,获取SecurityContext 分析

登录成功后,访问接口,依然是经过了SecurityContextPersistenceFilter过滤器。只是因为登录时,将SecurityContext 保存到了Session中,所以过滤器中的以下方法会直接获取到认证过的SecurityContext :

   SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder);

然后SecurityContextHolder会将SecurityContext 设置到ThreadLocal中,那么当前线程,无论在什么地方,都可以获取到当前用户的SecurityContext 中的认证信息了。

  SecurityContextHolder.setContext(contextBeforeChainExecution);

猜你喜欢

转载自blog.csdn.net/qq_43437874/article/details/121258988
今日推荐