在Security中,登录以后,我们可以通过以下代码获取当前用户的认证信息:
SecurityContext context = SecurityContextHolder.getContext(); Authentication authentication = context.getAuthentication();
在SecurityContext 中,包含了用户信息、账号状态、拥有的权限等信息:
在获取用户信息的代码中,通过SecurityContextHolder获取SecurityContext ,然后在SecurityContext 中获取Authentication ,那么SecurityContext 就是保存用户认证的地方了,那么这个时候就有几个疑问需要了解:
- SecurityContext 是什么?
- 用户登录后,认证信息保存在哪里?
- 已登录用户访问,是如何获取认证信息的?
- 如何保证返回的是当前登录用户的信息?
带着以上疑问,我们分析下相关源码,进行解析
核心类 1. SecurityContextPersistenceFilterSecurityContextPersistenceFilter是负责SecurityContext处理的过滤器,主要负责:
- 认证成功后,将SecurityContext保存到Session中
- 访问时,从Session中获取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. SecurityContextHolderSecurityContextHolder意为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来实现,子线程也可以获取到(本来父线程的本地变量是无法传递给子线程的)。
项目在启动过程中,肯定会根据配置类加载各种对象,比如以下配置,我们配置了一些放行路径、处理器、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 流程就结束了,总计以下:
- 进入SecurityContextPersistenceFilter过滤器,创建一个空的SecurityContext
- 后续过滤器认证通过后,在当前线程获取SecurityContext
- 将SecurityContext保存到Session中
登录成功后,访问接口,依然是经过了SecurityContextPersistenceFilter过滤器。只是因为登录时,将SecurityContext 保存到了Session中,所以过滤器中的以下方法会直接获取到认证过的SecurityContext :
SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder);
然后SecurityContextHolder会将SecurityContext 设置到ThreadLocal中,那么当前线程,无论在什么地方,都可以获取到当前用户的SecurityContext 中的认证信息了。
SecurityContextHolder.setContext(contextBeforeChainExecution);
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)