SecurityContextHolder 顾名思义,他是一个 holder,用于持有的是安全上下文(security context)的信息。SecurityContextHolder 记录如下信息:当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色或权限等等。
在典型的 web 应用程序中,用户登录一次,然后由其会话 ID 标识。服务器缓存持续时间会话的主体信息。有人可能对 Tomcat 建立会话的流程还不熟悉,这里稍微整理一下。
当客户一般是认证成功后,在调用 request.getSession() 方法后,Tomcat 会创建一个 HttpSesion 对象,存入一个 ConcurrentHashMap,Key 是 SessionId,Value 就是 HttpSession。然后请求完成后,在返回的报文中添加 Set-COOKIE:JSESSIONID=xxx,然后客户端浏览器会保存这个 COOKIE。当浏览器再次访问这个服务器的时候,都会带上这个 COOKIE。Tomcat 接收到这个请求后,根据 JSESSIONID 把对应的 HttpSession 对象取出来,放入 HttpSerlvetRequest 对象里面。
SecurityContext 对象实际存储于 Tomcat HttpSession 中的一个 key 中,名为 “SPRING_SECURITY_CONTEXT”。
在 Spring Security 中,在请求之间存储 SecurityContext 的责任落在 SecurityContextPersistenceFilter 上,默认情况下,该上下文将上下文存储为 HTTP 请求之间的HttpSession 属性。SecurityContextPersistenceFilter 是 Security 的拦截器,而且是拦截链中的第一个拦截器,请求来临时它会从 HttpSession 中把 SecurityContext 取出来,然后放入 SecurityContextHolder。在所有拦截器都处理完成后,再把 SecurityContext 存入 HttpSession,并清除 SecurityContextHolder 内的引用。
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)throws IOException, ServletException {HttpServletRequest request = (HttpServletRequest) req;HttpServletResponse response = (HttpServletResponse) res;if (request.getAttribute(FILTER_APPLIED) != null) {// ensure that filter is only applied once per requestchain.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);// 利用HttpSecurityContextRepository从HttpSesion中获取SecurityContext对象// 如果没有HttpSession,即浏览器第一次访问服务器,还没有产生会话。// 它会创建一个空的SecurityContext对象SecurityContext contextBeforeChainExecution = repo.loadContext(holder);try {// 把SecurityContext放入到SecurityContextHolder中SecurityContextHolder.setContext(contextBeforeChainExecution);// 执行拦截链,这个链会逐层向下执行chain.doFilter(holder.getRequest(), holder.getResponse());}finally { // 当拦截器都执行完的时候把当前线程对应的SecurityContext从SecurityContextHolder中取出来SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();// Crucial removal of SecurityContextHolder contents - do this before anything// else.SecurityContextHolder.clearContext();// 利用HttpSecurityContextRepository把SecurityContext写入HttpSessionrepo.saveContext(contextAfterChainExecution, holder.getRequest(),holder.getResponse());request.removeAttribute(FILTER_APPLIED);if (debug) {logger.debug("SecurityContextHolder now cleared, as request processing completed");}}}
SecurityContextHolder 可以用来设置和获取 SecurityContext。它主要是给框架内部使用的,可以利用它获取当前用户的 SecurityContext 进行请求检查,和访问控制等。
SecurityContextHolder 默认使用 ThreadLocal 策略来存储认证信息。看到 ThreadLocal 也就意味着,这是一种与线程绑定的策略。在 web 环境下,Spring Security 在用户登录时自动绑定认证信息到当前线程,在用户退出时,自动清除当前线程的认证信息。
SecurityContextHolder 采用策略模式,根据 strategyName 字段创建不同的 SecurityContextHolderStrategy 对象。
public class SecurityContextHolder {// 三种存储策略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";// System.getProperty() 从JVM中获取配置的属性SYSTEM_PROPERTY// 获取不到 strategyName = nullprivate static String strategyName = System.getProperty(SYSTEM_PROPERTY);private static SecurityContextHolderStrategy strategy;private static int initializeCount = 0;// 随着类的加载而加载static {initialize();}...// 初始化private static void initialize() {if (!StringUtils.hasText(strategyName)) {// 设置默认策略strategyName = MODE_THREADLOCAL;}// 根据strategyName字段创建对应的SecurityContextHolderStrategy对象if (strategyName.equals(MODE_THREADLOCAL)) {strategy = new ThreadLocalSecurityContextHolderStrategy();}else if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) {strategy = new InheritableThreadLocalSecurityContextHolderStrategy();}else if (strategyName.equals(MODE_GLOBAL)) {strategy = new GlobalSecurityContextHolderStrategy();}else {// 自定义策略...}initializeCount++;}// public static void setContext(SecurityContext context) {strategy.setContext(context);}// 可以设置新的存储策略public static void setStrategyName(String strategyName) {SecurityContextHolder.strategyName = strategyName;// 修改strategyName后需要重新执行initialize创建新的SecurityContextHolderStrategy对象initialize();}...
}
SecurityContext 安全上下文,用户通过 Spring Security 的校验之后,验证信息存储在 SecurityContext 中,SecurityContext 接口只定义了两个方法,实际上其主要作用就是设置、获取 Authentication 对象。
public interface SecurityContext extends Serializable {Authentication getAuthentication();void setAuthentication(Authentication authentication);
}
Authentication 直译过来是“认证”的意思,在 Spring Security 中 Authentication 用来表示当前用户是谁,一般来讲你可以理解为 authentication 就是一组用户名密码信息。
public interface Authentication extends Principal, Serializable {// 权限信息列表,默认是GrantedAuthority接口的一些实现类,通常是代表权限信息的一系列字符串。Collection extends GrantedAuthority> getAuthorities();// 密码信息,用户输入的密码字符串,在认证过后通常会被移除,用于保障安全Object getCredentials();// 细节信息,web应用中的实现接口通常为 WebAuthenticationDetails,// 它记录了访问者的ip地址和sessionId的值。Object getDetails();// 最重要的身份信息,大部分情况下返回的是UserDetails接口的实现类,也是框架中的常用接口之一。Object getPrincipal();boolean isAuthenticated();void setAuthenticated(boolean var1) throws IllegalArgumentException;
}
GrantedAuthority 是在 Authentication 的接口中使用集合存储权限。
Collection extends GrantedAuthority> getAuthorities();
可以看到权限集合存放的元素是 GrantedAuthority 的实现类,也可以使用 String。
该接口表示了当前用户所拥有的权限(或者角色)信息。这些信息有授权负责对象 AccessDecisionManager 来使用,并决定最终用户是否可以访问某资源。
这个接口规范了用户详细信息所拥有的字段,譬如用户名、密码、账号是否过期、是否锁定等。在 Spring Security 中,获取当前登录的用户的信息,一般情况是需要在这个接口上面进行扩展,用来对接自己系统的用户。
public interface UserDetails extends Serializable {Collection extends GrantedAuthority> getAuthorities();String getPassword();String getUsername();// 用户账户是否过去,过期的用户不能被认证boolean isAccountNonExpired();// 用户是否被lock,lock的用户不能被认证boolean isAccountNonLocked();// 用户的credentials (password)是否过期,国企的不能认证成功boolean isCredentialsNonExpired();// 用户是enabled或者disabled,diabled的用户不能被认证boolean isEnabled();
}
这个接口非常重要,一般情况我们都是通过扩展这个接口来显示获取我们的用户信息,用户登录时传递的用户名和密码也是通过这里这查找出来的用户名和密码进行校验。
public interface UserDetailsService {UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
但是真正的校验不在这里,而是由 AuthenticationManager 以及 AuthenticationProvider 负责的,需要强调的是,如果用户不存在,不应返回 NULL,而要抛出异常 UsernameNotFoundException。
AuthenticationManager 是认证相关的核心接口,也是发起认证的出发点,因为在实际需求中,我们可能会允许用户使用用户名+密码登录,同时允许用户使用邮箱+密码,手机号码+密码登录,甚至,可能允许用户使用指纹登录。
public interface AuthenticationManager {Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
所以说 AuthenticationManager 一般不直接认证,AuthenticationManager 接口的常用实现类 ProviderManager 内部会维护一个 List
列表,存放多种认证方式,实际上这是委派器模式的应用(Delegate)。
核心的认证入口始终只有一个:AuthenticationManager,不同的认证方式有:用户名+密码,邮箱+密码,手机号码+密码登录,分别对应了三个 AuthenticationProvider。
AuthenticationProvider 接口最常用的一个实现便是 DaoAuthenticationProvider。
public interface AuthenticationProvider {Authentication authenticate(Authentication authentication) throws AuthenticationException;boolean supports(Class> authentication);
}
顾名思义,Dao 正是数据访问层的缩写,也暗示了这个身份认证器的实现思路。主要作用:它获取用户提交的用户名和密码,比对其正确性,如果正确,返回一个数据库中的用户信息(假设用户信息被保存在数据库中)。