本文内容来自王松老师的《深入浅出Spring Security》,自己在学习的时候为了加深理解顺手抄录的,有时候还会写一些自己的想法。
登录成功之后,在后续的业务逻辑中开发者可能还需要获取到登录成功的用户对象,如果不是用任何安全管理框架那么可以将用户信息保存在HttpSession中,如果以后需要的话可以从HttpSession中获取数据。在Spring Security汇总,用户登录信息本质上还是保存在HttpSession中的,但是为了方便使用Spring Security对HttpSession中的用户信息进行了封装。
获取登录用户的信息离不开一个重要的对象:Authentication。在Spring Security中,Authentication对象主要由两方面功能:
一个Authentication对象主要包含三个方面的信息:
Java中本身提供了一个Principal接口来描述认证主体,Principal可以代表一个公司、个人或者登陆者ID。Spring Security中定义了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;
}
这里接口中定义的方法都很好理解:
可以看到在Spring Security中,只要获得Authentication对象就能获取到登录用户的详细信息。
不同的认证方式对应不同的Authentication实例。在Spring Security中的Authentication实例类如下图:
这些实现类看起来可能比较陌生,在后续的学习中这些实现类基本上都会涉及。在这些Authentication的实例中,最常用的的两个类:UsernamePasswordAuthenticationToken和RememberMeAuthentication。这里我们先混个眼熟,后面的学习中我们会学习到。大致了解了Authentication对象之后,接下来我们看下如何在登录成功后获取用户登录信息,即Authentication对象。
在前面的学习项目中增加一个UserController,内容如下:
/**
* @author tlh
* @date 2022/11/17 21:11
*/
@RestController
public class UserController {
@GetMapping("/user")
public Object userInfo() throws JsonProcessingException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String name = authentication.getName();
Collection extends GrantedAuthority> authorities = authentication.getAuthorities();
ObjectMapper mapper = new ObjectMapper();
Map userMap = new HashMap(2);
userMap.put("userName", name);
userMap.put("authorities", authorities);
return mapper.writeValueAsString(userMap);
}
}
配置成功后启动项目,登录成功后访问/user接口,会返回用户名字和用户所拥有的的权限信息:
这里看到SecurityContextHolder.getContext( )是一个静态方法,也就意味着我们随时可以获取到登录的用户信息,在service层中也可以轻松获取到。获取用户登录信息的代码很简单,那么SecurityContextHolder到底是什么?它里面的数据又是从何而来的?
SecurityContextHolder中存储的是SecurityContext,SecurityContext中存储的是Authentication。三者的关系如下图:
在SecurityContextHolder中存储SecurityContext定义了三种存储模式。使用的典型的策略模式。
我们看到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("spring.security.strategy");
private static SecurityContextHolderStrategy strategy;
private static int initializeCount = 0;
public SecurityContextHolder() {
}
private static void initialize() {
initializeStrategy();
++initializeCount;
}
//根据 strategyName 来加载对应的SecurityContextHolderStrategy(定义了各种保存SecurityContext的策略接口)
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");
} else {
//默认采用ThreadLocal保存SecurityContext
if (!StringUtils.hasText(strategyName)) {
strategyName = "MODE_THREADLOCAL";
}
if (strategyName.equals("MODE_THREADLOCAL")) {
strategy = new ThreadLocalSecurityContextHolderStrategy();
} else if (strategyName.equals("MODE_INHERITABLETHREADLOCAL")) {
//InheritableThreadLocal不同于ThreadLoca在于在创建子线程时会将父线程中的数据复制到子线程中
strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
} else if (strategyName.equals("MODE_GLOBAL")) {
//采用一个静态变量保存SecurityContext
strategy = new GlobalSecurityContextHolderStrategy();
} else {
try {
Class> clazz = Class.forName(strategyName);
Constructor> customStrategy = clazz.getConstructor();
strategy = (SecurityContextHolderStrategy)customStrategy.newInstance();
} catch (Exception var2) {
ReflectionUtils.handleReflectionException(var2);
}
}
}
}
//省略。。。
}