登录用户数据获取
登录成功之后,在后续的业务逻辑中,我们可能还需要获取登录成功用户的用户对象,如果我们不使用任何安全框架,我们可以将用户信息保存在HttpSession中,需要的话就从HttpSession中获取数据。在SpringSecurity中,用户登录信息本质上还是保存在HttpSession中,但是为了方便使用,SpringSecurity对HttpSession中的用户信息进行了封装,封装之后,我们在想获取用户登录数据就会有两种不同的思路:
- 从SecurityContextHolder中获取
- 从当前请求对象中获取。
这里列出的两种方式是主流的做法,当然也可以使用一些非主流的方法获取登录成功后的用户信息,例如直接从HttpSession中获取用户登录数据。。
无论是哪种获取方式,都离不开一个重要的对象:Authentication。在SpringSecurity中,Authentication对象主要有两个方面的功能:
(1)、作为AuthenticationManager的输入参数,提供用户身份认证的凭证,当它作为一个输入参数时,它的isAuthentication方法返回false,表示用户还未认证。
(2)、代表已经经过认证的用户,此时的Authentication可以从SecurityContext中获取。
一个Authentication对象主要包含三个方面的信息:
(1)、principal:定义认证的用户。如果用户使用用户名/密码的方式登录,principal通常就算一个UserDetails对象。
(2)、credentials:登录凭证,一般就是指密码,当用户登录成功之后,登录凭证会被自动擦除,以防止泄露。
(3)、authorities:用户被授予的权限信息。
Java中本身提供了Principal接口用来描述认证主题,Principal可以代表一个公司、个人或者登录ID,SpringSecurity中定义了Authentication接口用来规范登录用户信息,Authentication继承自Principal:
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
Object getCredentials();
Object getDetails();
Object getPrincipal();
boolean isAuthenticated();
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
这里接口中的定义方法都很好理解:
- getAuthorities方法:获取用户权限
- getCredentials方法:用来获取用户凭证,一般来说就是密码。
- getDetails方法:用来获取用户的详细信息,可能是当前的请求之类。
- getPrincipal方法:用来获取当前用户信息信息,可能是一个用户名,也可能是一个用户对象。
- isAuthenticated方法:当前用户是否认证成功。
可以看到,在SpringSecurity中,只要获取到Authentication对象,就可以获取到登录用户的详细信息。
不同的认证方式对应不同的Authentication实例,SpringSecurity中的Authentication实例类如下:
(1)、AbstractAuthenticationToken:该类实现了Authentication和CredentialsContainer两个接口,在AbstractAuthenticationToken中对Authentication接口定义的各个数据获取方法进行了实现,CredentialsContainer则提供了登录凭证擦除方法。一般登录成功后,为了防止用户信息泄露,可以将登录凭证(例如密码)擦除。
(2)、RememberMeAuthenticationToken:如果用户使用RememberMe的方式登录,登录信息封装在该类中。
(3)、TestingAuthenticationToken:单元测试时封装的用户对象。
(4)、AnonymousAuthenticationToken:匿名登录时封装的用户对象。
(5)、UsernamePasswordAuthenticationToken:表单登录时封装的用户对象。
(6)、RunAsUserToken:替换验证身份时封装的用户对象。
(7)、JaasAuthenticationToken:JAAS认证时封装的用户对象。
(8)、PreAuthenticatedAuthenticationToken:Pre-Authentication场景下封装的用户对象。
在这些Authentication的实例中,最常用的有两个:UsernamePasswordAuthenticationToken和RememberMeAuthenticationToken。在上几篇文章中我们案例的认证对象就是UsernamePasswordAuthenticationToken。
了解了Authentication对象之后,接下来我们来看一下如何在登录成功后获取用户的登录信息,即Authentication对象。
从SecurityContextHolder中获取
我们增加一个UserController,如下:
@RestController
public class UserController {
@GetMapping("/user")
public void userInfo() {
final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String name = authentication.getName();
final Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
System.out.println("name = "+ name);
System.out.println("authorities = " + authorities);
}
}
配完成之后,启动项目登录成功后访问/user接口,可以看到打印的信息如下。
这里为了方便,我们在Controller中获取登录用户信息,可以发现SecurityContextHolder.getContext()是一个静态方法,也就意味着我们随时随地都可以获取到登录用户信息,在service层也可以获取到登录用户信息(实际中,大部分情况也都是在service层获取)
获取登录用户信息的代码很简单,那么SecurityContextHolder到底是什么?它里面的数据又是从何来的?
SecurityContextHolder
SecurityContextHolder中存储的是SecurityContext,SecurityContext中存储的则是Authenticaion,三者的关系如下:
这里清楚的描述了三者之间的关系。
首先在SpringSecurityContextHolder中存放的是SecurityContext,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";
private static final String MODE_PRE_INITIALIZED = "MODE_PRE_INITIALIZED";
(1)、MODE_THREADLOCAL:这种存放策略是将SecurityContext存放在ThreadLocal中,大家知道ThreadLocal的特点是在哪个线程中存储就要在哪个线程中读取,这其实非常适合web应用,因为在默认情况下,一个请求无论经过多少Filter到达Servlet,都是由一个线程来处理的,这也是SecurityContextHolder默认的存储策略,这种存储策略意味着如果在具体的业务代码中,开启了子线程,在子线程去获取登录用户数据,就会获取不到。
(2)、MODE_INHERITABLETHREADLOCAL:这种存储模式适用于多线程环境,如果希望在子线程中也能够获取到登录用户数据,那么可以使用这种存储模式。
(3)、MODE_GLOBAL:这种存储模式实际上是将数据保存在一个静态变量中,在JAVAWeb开发中,这种模式很少使用到。
SpringSecurity中定义了SecurityContextHolderStrategy接口用来规范存储策略中的方法,我们来看下:
public interface SecurityContextHolderStrategy {
void clearContext();
SecurityContext getContext();
void setContext(SecurityContext context);
SecurityContext createEmptyContext();
}
接口中一共定义了四个方法:
(1)、clearContext:用来清除存储的SecurityContext对象。
(2)、getContext:用来获取存储SecurityContext对象。
(3)、setContext:用来设置存储的SecurityContext对象。
(4)、createEmptyContext:用来创建一个空的SecurityContext对象。
在SpringSecurity中,SecurityContextHolderStrategy接口一共有三个实现类,对应了三种不同存储策略。
每一个实现类都对应了不同的实现策略,我们先来看一下ThreadLocalSecurityContextHolderStrategy:
final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {
private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<>();
@Override
public void clearContext() {
contextHolder.remove();
}
@Override
public SecurityContext getContext() {
SecurityContext ctx = contextHolder.get();
if (ctx == null) {
ctx = createEmptyContext();
contextHolder.set(ctx);
}
return ctx;
}
@Override
public void setContext(SecurityContext context) {
Assert.notNull(context, "Only non-null SecurityContext instances are permitted");
contextHolder.set(context);
}
@Override
public SecurityContext createEmptyContext() {
return new SecurityContextImpl();
}
}
ThreadLocalSecurityContextHolderStrategy实现了SecurityContextHolderStrategy接口并实现了接口中的方法,存储数据的载体是一个ThreadLocal,所以针对SecurityContext的清空,获取以及存储,都是在ThreadLocal中进行操作,例如清空就是调用ThreadLocal的remove方法。SecurityContext是一个接口,它只有一个实现类SecurityContextImpl,所以创建就直接新建一个SecurityContextImpl对象即可。
在来看InheritableThreadLocalSecurityContextHolderStrategy:
final class InheritableThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {
private static final ThreadLocal<SecurityContext> contextHolder = new InheritableThreadLocal<>();
@Override
public void clearContext() {
contextHolder.remove();
}
@Override
public SecurityContext getContext() {
SecurityContext ctx = contextHolder.get();
if (ctx == null) {
ctx = createEmptyContext();
contextHolder.set(ctx);
}
return ctx;
}
@Override
public void setContext(SecurityContext context) {
Assert.notNull(context, "Only non-null SecurityContext instances are permitted");
contextHolder.set(context);
}
@Override
public SecurityContext createEmptyContext() {
return new SecurityContextImpl();
}
}
InheritableThreadLocalSecurityContextHolderStrategy和ThreadLocalSecurityContextHolderStrategy的实现策略基本一致,不同的是存储数据的载体变了,在InheritableThreadLocalSecurityContextHolderStrategy中存储数据的载体变成了InheritableThreadLocal。InheritableThreadLocal继承自ThreadLocal,但是多了一个特性,就是在子线程创建的一瞬间,会自动父线程数据复制到子线程中。该策略就是利用了这已特性,实现了在子线程中获取登录用户信息的功能。
最后再来看GlobalSecurityContextHolderStrategy
final class GlobalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {
private static SecurityContext contextHolder;
@Override
public void clearContext() {
contextHolder = null;
}
@Override
public SecurityContext getContext() {
if (contextHolder == null) {
contextHolder = new SecurityContextImpl();
}
return contextHolder;
}
@Override
public void setContext(SecurityContext context) {
Assert.notNull(context, "Only non-null SecurityContext instances are permitted");
contextHolder = context;
}
@Override
public SecurityContext createEmptyContext() {
return new SecurityContextImpl();
}
}
GlobalSecurityContextHolderStrategy的实现就更简单了,用一个静态变量来保存SecurityContext,所以它也可以在多线程下使用,但是一般在Web开发中,这种存储策略使用少。
最后我们在看一下SecurityContextHolder:
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";
private static final String MODE_PRE_INITIALIZED = "MODE_PRE_INITIALIZED";
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();
}
private static void initialize() {
initializeStrategy();
initializeCount++;
}
private static void initializeStrategy() {
if (MODE_PRE_INITIALIZED.equals(strategyName)) {
Assert.state(strategy != null, "When using " + MODE_PRE_INITIALIZED
+ ", setContextHolderStrategy must be called with the fully constructed strategy");
return;
}
if (!StringUtils.hasText(strategyName)) {
strategyName = MODE_THREADLOCAL;
}
if (strategyName.equals(MODE_THREADLOCAL)) {
strategy = new ThreadLocalSecurityContextHolderStrategy();
return;
}
if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) {
strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
return;
}
if (strategyName.equals(MODE_GLOBAL)) {
strategy = new GlobalSecurityContextHolderStrategy();
return;
}
try {
Class<?> clazz = Class.forName(strategyName);
Constructor<?> customStrategy = clazz.getConstructor();
strategy = (SecurityContextHolderStrategy) customStrategy.newInstance();
}
catch (Exception ex) {
ReflectionUtils.handleReflectionException(ex);
}
}
public static void clearContext() {
strategy.clearContext();
}
public static SecurityContext getContext() {
return strategy.getContext();
}
public static int getInitializeCount() {
return initializeCount;
}
public static void setContext(SecurityContext context) {
strategy.setContext(context);
}
public static void setStrategyName(String strategyName) {
SecurityContextHolder.strategyName = strategyName;
initialize();
}
public static void setContextHolderStrategy(SecurityContextHolderStrategy strategy) {
Assert.notNull(strategy, "securityContextHolderStrategy cannot be null");
SecurityContextHolder.strategyName = MODE_PRE_INITIALIZED;
SecurityContextHolder.strategy = strategy;
initialize();
}
public static SecurityContextHolderStrategy getContextHolderStrategy() {
return strategy;
}
public static SecurityContext createEmptyContext() {
return strategy.createEmptyContext();
}
@Override
public String toString() {
return "SecurityContextHolder[strategy='" + strategy.getClass().getSimpleName() + "'; initializeCount="
+ initializeCount + "]";
}
}
从这可以看到SecurityContextHolder定义了三个静态常量用来描述三种不同的存储策略,存储策略stragey会在静态代码块中进行初始化,根据不同的strategyName初始化不同的存储策略,strategyName变量表示目前正在使用的存储策略,我们可以通过配置系统变量或者调用setStrategyName来修改SecurityContextHolder中的存储策略,调用setStrategyName后会重新初始化strategy。
默认情况下,如果我们试图从子线程中获取当前登录用户数据,就会获取失败,如下:
我们来修改一下上面/user接口
@GetMapping("/user")
public void userInfo() {
final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String name = authentication.getName();
final Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
System.out.println("name = "+ name);
System.out.println("authorities = " + authorities);
new Thread(new Runnable() {
@Override
public void run() {
final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if(authentication == null){
System.out.println("获取用户失败");
return;
}
String name = authentication.getName();
final Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
final String threadName = Thread.currentThread().getName();
System.out.println(threadName + ":name = "+ name);
System.out.println(threadName + ":authorities = " + authorities);
}
}).start();
}
}
可以发现在子线程中获取信息失败。
子线程之所以获取不到用户登录信息,就是因为数据存储在ThreadLocal中,存储和读取不是同一个线程,所以获取不到,如果希望子线程中也能获取到用户信息,可以将SecurityContextHolder中的存储信息策略改为MODE_INHERITABLETHREADLOCAL,这样就支持多线程环境下获取登录用户信息了。
默认的存储策略是通过System.getProperties加载的,因此我们可以通过配置系统变量来修改默认的存储策略,在IDEA中,我们可以添加vm options参数。如下
-Dspring.security.strategy=MODE_INHERITABLETHREADLOCAL
然后可以看到控制台打印,在线程中获取到了登录用户信息如下:
这有一个问题,既然SecurityContextHolder默认是将用户信息存储在ThreadLocal中,在SpringBoot中不同的请求都是由不同的线程处理的,那为什么每一次请求都还能从SecurityContextHolder中获取到登录用户信息?这个问题有点长,我们在下一篇文章中进行讨论一下。
|