重点表示
登录用户数据,用户信息的获取,以及Srping Security为什么把用户信息放到SecurityContextHolder中的,又是如何做的。
多线程下,在Spring Security是如何获取到用户信息的?
匿名访问!
用户数据
获取登录成功后的数据,
用户数据存在Authenticatiion,这里就是我在配置中的Authentication。
存在threadLocal中,是线程级别。
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
点进去看一下:SecurityContextHolder,简单解释一下,根据源码逻辑,不指定strategyName的情况下,就是空的,默认给一个MODE_THREADLOCAL。
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 {
if (!StringUtils.hasText(strategyName)) {
strategyName = "MODE_THREADLOCAL";
}
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 {
try {
Class<?> clazz = Class.forName(strategyName);
Constructor<?> customStrategy = clazz.getConstructor();
strategy = (SecurityContextHolderStrategy)customStrategy.newInstance();
} catch (Exception var2) {
ReflectionUtils.handleReflectionException(var2);
}
}
}
}
这里有一点要注意,在我们的Web工程中,登陆之后,Tomcat从线程池中拿出一个线程处理,new 一个新线程的情况下,就拿不到用户信息了。
我们进入源码看一下,实际上,这块就是在SecurityContextHolderFilter里面,我们看它的doFilter方法。
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
if (request.getAttribute(FILTER_APPLIED) != null) {
chain.doFilter(request, response);
} else {
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
Supplier<SecurityContext> deferredContext = this.securityContextRepository.loadDeferredContext(request);
try {
this.securityContextHolderStrategy.setDeferredContext(deferredContext);
chain.doFilter(request, response);
} finally {
this.securityContextHolderStrategy.clearContext();
request.removeAttribute(FILTER_APPLIED);
}
}
}
这里简单解释一下:
使用Http Session,从session里面拿到值,然后存到ContextHolder中,最后请求结束的时候,又清理掉。
那为什么要这么弄呢,为什么不直接放在Http Session中呢,这是因为有些场景下,Http Session并不是那么容易获取的,甚至在一些场景中,根本就没用到Http Session,比如说
Service要获取到用户信息,那没有Http session,就要注入session,比较麻烦,最关键的是,在一些前后端分离登录中,可能服务端生成jwt,存在redis,不用session,所以,Spring Security一开始就替我们考虑到了这样的场景,直接存到SecurityContextHolder中,更灵活。
看一下从session中怎么拿:
@GetMapping("/hello")
public String hello(HttpSession httpSession){
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
SecurityContext securityContext =(SecurityContext)httpSession.getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY);
Authentication authentication1 = securityContext.getAuthentication();
System.out.println(authentication1 == authentication);
return "hello";
}
那如果一定要在子线程中获取到用户信息呢,Security也提供了相关的配置,如下
public static void main(String[] args) {
System.setProperty("spring.security.strategy", SecurityContextHolder.MODE_GLOBAL);
SpringApplication.run(Security02DemoApplication.class, args);
}
@GetMapping("/hello")
public String hello(HttpSession httpSession){
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
SecurityContext securityContext =(SecurityContext)httpSession.getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY);
Authentication authentication1 = securityContext.getAuthentication();
System.out.println(authentication1 == authentication);
new Thread(new Runnable() {
@Override
public void run() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
System.out.println(authentication.getPrincipal());
}
}).start();
return "hello";
}
就是在启动类前,加上这个全局配置,存到 private static SecurityContext contextHolder;这里面去,相当于静态变量保存,自然和线程就没啥关系了,不过这种方式是不推荐的。
还有一种获取Authentication和Principal的方法,直接放在参数中,可以直接拿到。
/**
* 这两个参数默认会被解析,类似于HttpSession,HttpServletRequest
* @param authentication
* @param principal
*/
@GetMapping("/user")
public void user(Authentication authentication, Principal principal){
}
还可以通过HttpServletRequest,来获取,如下
@GetMapping("/hello2")
public void hello2(HttpServletRequest request){
//本质上也是从ContextHolder里面拿的,所以子线程也是获取不到
//这里是说,获取用户名
String remoteUser = request.getRemoteUser();
//是否具备admin角色
boolean admin = request.isUserInRole("admin");
//当前用户登陆对象
Principal userPrincipal = request.getUserPrincipal();
}
我们可以进去看一下,
进入到SecurityContextHolderAwareRequestWrapper ,这里我们可以看到 this.securityContextHolderStrategy = SecurityContextHolder.getContextHolderStrategy();,这样就拿到了,本质上还是从ContextHolder中获取的。
匿名访问
//适用于静态页面放行,不会走security过滤器链,从contextHolder取不到值
@Bean
WebSecurityCustomizer webSecurityCustomizer(){
return web -> web.ignoring().requestMatchers("/hello");
}
/**
* 可以在这里修改过滤器
* @param httpSecurity 所有过滤器都可以通过这个来配置。保存的默认的过滤器
* @return
*/
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
//所有请求,都要登陆后才能访问
httpSecurity.authorizeRequests(a -> a
//登陆或不登陆都可以访问
.requestMatchers("/hello").permitAll()
//只能未认证访问
.requestMatchers("/hello").anonymous()
.anyRequest().authenticated())
这就是他们的区别,这一点一定要注意。
结语
攀登的过程,注定艰辛!