SecurityContextHolder看名知义,他是一个holder,用于持有的是安全上下文(security context)的信息。
SecurityContextHolder记录如下信息:当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色或权限等等。
在典型的web应用程序中,用户登录一次,然后由其会话ID标识。服务器缓存持续时间会话的主体信息。在Spring Security中,在请求之间存储SecurityContext的责任落在SecurityContextPersistenceFilter上,默认情况下,该上下文将上下文存储为HTTP请求之间的HttpSession属性。它会为每个请求恢复上下文SecurityContextHolder,并且最重要的是,在请求完成时清除SecurityContextHolder。SecurityContextHolder是一个类,他的功能方法都是静态的(static)。
一共包含3种策略:
1 存储在线程中
2 存储在线程中,但子线程可以获取到父线程中的SecurityContext。
3 在所有线程中都相同。
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 = null
private 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();
}
...
}
安全上下文,用户通过Spring Security 的校验之后,验证信息存储在SecurityContext中,SecurityContext接口只定义了两个方法,实际上其主要作用就是设置、获取Authentication对象。
public interface SecurityContext extends Serializable {
Authentication getAuthentication();
void setAuthentication(Authentication authentication);
}
Authentication 直译过来是“认证”的意思,在Spring Security 中Authentication用来表示当前用户是谁,一般来讲你可以理解为authentication就是一组用户名密码信息。
Authentication包括:
1 principal:用于标识用户当通过username和password认证用户时,principal通常是一个UserDetails的实现类对象。
2 credentials:通常是密码,在很多场景,如果用户已经被认证,那么此项将被清除以防止密码泄露。
3 authorities:用户具有的权限或角色。
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;
}
案例:获取已认证用户的用户名。
//getAuthentication()返回了认证信息
//getPrincipal()返回了身份信息,UserDetails便是Spring对身份信息封装的一个接口。
Object principal =SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}
在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。
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
AuthenticationManager是认证相关的核心接口,也是发起认证的出发点,因为在实际需求中,我们可能会允许用户使用用户名+密码登录,同时允许用户使用邮箱+密码,手机号码+密码登录,甚至,可能允许用户使用指纹登录。
所以说AuthenticationManager一般不直接认证,AuthenticationManager接口的常用实现类ProviderManager内部会维护一个List
列表,存放多种认证方式,实际上这是委托者模式的应用(Delegate)。
核心的认证入口始终只有一个:AuthenticationManager,不同的认证方式有:用户名+密码,邮箱+密码,手机号码+密码登录,分别对应了三个AuthenticationProvider。
AuthenticationProvider接口
public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
boolean supports(Class<?> authentication);
}
AuthenticationProvider接口最常用的一个实现便是DaoAuthenticationProvider。
顾名思义,Dao正是数据访问层的缩写,也暗示了这个身份认证器的实现思路。主要作用:它获取用户提交的用户名和密码,比对其正确性,如果正确,返回一个数据库中的用户信息(假设用户信息被保存在数据库中)。
UserDetails接口代表了最详细的用户信息,这个接口包含了一些必要的用户信息字段,我们一般都需要对它进行必要的扩展。
它和Authentication接口很类似,比如它们都拥有username,authorities。
Authentication的getCredentials()与UserDetails中的getPassword()需要被区分对待,前者是用户提交的密码凭证,后者是用户正确的密码,认证器其实就是对这两者的比对。
Authentication中的getAuthorities()实际是由UserDetails的getAuthorities()传递而形成的。还记得Authentication接口中的getUserDetails()方法吗?其中的UserDetails用户详细信息便是经过了AuthenticationProvider之后被填充的。
UserDetailsService和AuthenticationProvider两者的职责常常被人们搞混,UserDetailsService只负责从特定的地方加载用户信息,可以是数据库、redis缓存、接口等。
(1)用户名和密码被过滤器获取到,封装成Authentication,通常情况下是UsernamePasswordAuthenticationToken实现类。
(2)AuthenticationManager 身份管理器负责验证这个Authentication
(3)认证成功后,AuthenticationManager身份管理器返回一个被填充满了信息的(包括上面提到的权限信息,身份信息,细节信息,但密码通常会被移除)Authentication实例。
(4)SecurityContextHolder安全上下文容器将第3步填充了信息的Authentication,通过SecurityContextHolder.getContext().setAuthentication()方法,设置到其中。
参考:
1 spring security系列一:核心组件
2 Spring Security Authentication Architecture