• Spring Security 从入门到精通之获取认证用户信息(四)


    1. 前言

    当用户认证通过后,SpringSecurity会将当前用户信息保存起来,以供后续业务流程使用,下面将简单介绍两种获取认证用户的信息。

    2 通过SecurityContextHolder获取用户信息

    当用户认证通过后,在后续的业务逻辑中还会获取到当前认证的用户信息,在未使用安全框架前,一般是将认证成功的用户存储在HttpSession中,在后续业务逻辑中再从HttpSession中获取用户即可。实际上Spring Security依然是将用户存储在HttpSession中。不过Spring Security在此基础上还对HttpSession中的用户信息作了进一步的封装,Spring Security是将认证的用户信息保存在SecurityContextHolder 对象中。下面我们演示如何从这个类中获取用户信息:

     @GetMapping("/user")
        public ResponseEntity<String> getUser() throws JsonProcessingException {
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            Map<String,Object> map = new HashMap<>();
            map.put("authentication",authentication);
            String value = objectMapper.writeValueAsString(map);
            return ResponseEntity.ok(value);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    当认证成功后,在浏览器中输入http://127.0.0.1:8001/user,并返回如下信息
    在这里插入图片描述

    2.1. SecurityContextHolder获取用户信息源码分析

    首先需要说明的是SecurityContextHolder 中存储的是SecurityContext对象,然后在这个对象中存储了认证用户Authentication对象,下面是三者的关系图:
    在这里插入图片描述
    首先在SecurityContext在初始化是依靠 SecurityContextHolderStrategy 接口,根据不同的策略生成相应的SecurityContext,在SecurityContextHolder类加载的时候就需要初始化当前策略,我们可以在源码中找到答案:
    在这里插入图片描述
    SecurityContextHolder 中通过实例化 SecurityContextHolderStrategy 接口不同子类以满足不同的策略需要,
    SecurityContextHolderStrategy 接口定义如下:
    在这里插入图片描述
    SecurityContextHolderStrategy 接口是用来保存SecurityContext对象里,而这个SecurityContext对象里则保存了用户认证Authentication对象,SecurityContext接口定义如下:
    在这里插入图片描述
    SecurityContextHolderStrategy接口实现类有如下四个:
    在这里插入图片描述
    SecurityContextHolderinitializeStrategy方法中默认设置了MODE_THREADLOCAL 策略即 ThreadLocalSecurityContextHolderStrategy 类,这个类源码如下所示:
    在这里插入图片描述
    可以看到这个类是通过ThreadLocal存储、获取、清空操作的,我们都知道ThreadLocal都是在单个线程中进行读写的,所以在多线程环境下ThreadLocal是线程安全的。这也意味着如果在一个线程中开启了子线程,那么在子线程中是无法获取认证用户信息的。为了解决这个问题SecurityContextHolder 提供了 MODE_INHERITABLETHREADLOCAL策略对应InheritableThreadLocalSecurityContextHolderStrategy 实现,此类定义如下:
    在这里插入图片描述
    InheritableThreadLocalThreadLocal子类,与 ThreadLocal 不同的是在子线程创建的一瞬间,会自动将父线程中的数据复制到子线程中。所以此策略适用于多线程环境下业务场景。我们可以用以下代码检验一下是否是这样?

        @GetMapping("/user")
        public ResponseEntity<String> getUser() throws JsonProcessingException {
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            Map<String,Object> map = new HashMap<>();
            map.put("authentication",authentication);
            String value = objectMapper.writeValueAsString(map);
            // 开启子线程获取认证用户信息
            new Thread(()-> {
                Authentication authentication1 = SecurityContextHolder.getContext().getAuthentication();
                if(authentication1 ==null) {
                    System.out.println("获取用户信息失败!");
                    return;
                }
                System.out.println("子线程获取当前登陆用户名称:"+authentication1.getName());
            }).start();
            return ResponseEntity.ok(value);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    项目重启用户认证成功后,浏览器输入:http://127.0.0.1:8001/user 接口控制台输出如下结果:
    在这里插入图片描述
    可以看到在默认策略下,是无法在子线程中获取用户信息的,如果想要在子线程中获取用户信息,可修改SecurityContextHolder类默认策略,默认的策略是通过System.getProperty进行加载的,在源码中我们不难发现这一点如下所示:
    在这里插入图片描述
    我们可以在项目启动类上加上VM参数:-Dspring.security.strategy=MODE_INHERITABLETHREADLOCAL
    在这里插入图片描述
    再次重启重新认证就发现子线程可以获取到当前用户认证的信息了如下所示:
    在这里插入图片描述
    如果对当前使用场景要求简单的情况下,还可以使用GlobalSecurityContextHolderStrategy,此类使用静态变量来保存SecurityContext对象,不过在web开发中这种策略使用场景不多,此类定义如下所示:
    在这里插入图片描述

    3. 从HttpServletRequest获取用户信息

      /**
         * 登陆成功后可以直接从HttpServletRequest获取当前已经认证成功的对象
         * @param httpServletRequest HttpServletRequest
         * @return ResponseEntity
         * @throws JsonProcessingException JsonProcessingException
         */
        @GetMapping("/getUserFromServlet")
        public ResponseEntity<String> getUserFromServlet(HttpServletRequest httpServletRequest) throws JsonProcessingException {
            String user = httpServletRequest.getRemoteUser();
            Authentication userPrincipal = (Authentication) httpServletRequest.getUserPrincipal();
            Map<String, Object> map = new HashMap<>();
            map.put("authentication", userPrincipal);
            map.put("user", user);
            String value = objectMapper.writeValueAsString(map);
            return ResponseEntity.ok(value);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    3.1 HttpServletRequest获取用户信息源码分析

    首先我们看一下HttpServletRequest 类层级关系如下图所示:
    在这里插入图片描述
    下面我们重点查看一下SecurityContextHolderAwareRequestWrapper#getUserPrincipal 方法
    在这里插入图片描述
    可以很清晰的看到HttpServletRequest获取的用户认证信息,底层还是通过SecurityContextHolder类进行获取的。

    4. SecurityContextHolder 保存认证用户信息流程

    Spring Security 将用户登陆成功的用户信息保存在session中,为了能够更好的获取到用户信息,Spring Security 提供了一个SecurityContextHolder类,用来保存用户登陆信息。
    具体原理是:当用户登陆成功后,Spring Security会将用户信息保存在SecurityContextHolder中,SecurityContextHolder中的数据保存默认是通过ThreadLocal来实现的,ThreadLocal这个类想必大家都不陌生,我们经常会使用到它来保存一些线程隔离的、全局的变量信息。使用ThreadLocal维护变量时,每个线程都会获得该线程独享一份变量副本。这里通过ThreadLocal 实现了请求线程与用户数据绑定(每一个请求线程都有自己的用户数据),当登陆请求处理完毕后,Spring Security会将SecurityContextHolder中保存用户信息取出并保存在Session中,同时删除SecurityContextHolder保存的用户信息,待下一次请求时,再从session中取出用户信息并保存在SecurityContextHolder中,然后在请求结束时,又会将用户信息从SecurityContextHolder取出并保存在session中并清除SecurityContextHolder中的用户信息。这样做的好处就是可以很方便在Controller或者service中随时获取到用户信息(通过SecurityContextHolder),但是在子线程中获取父线程用户数据就异常麻烦。

    5 总结

    这里我们学习了如何在Spring Security中获取认证成功的用户信息,下一章我们将继续深入学习Spring Security的整个认证流程,敬请期待

  • 相关阅读:
    《爆肝整理》保姆级系列教程-玩转Charles抓包神器教程(13)-Charles如何进行Mock和接口测试
    数学建模准备知识
    springboot罗亚方舟考研资料库网站设计与实现毕业设计源码302302
    FastAPI 学习之路(二十)接口文档配置相关
    Unix Network Programming Episode 59
    华为认证HCIA H12-811 Datacom数通考试真题题库【带答案刷题必过】【第五部分】
    如何理解进程的 no_new_privs 属性?
    100天精通风控建模(原理+Python实现)——第7天:风控建模中归一化是什么?
    Dubbo:Nacos作为注册中心
    Shiro安全(一):Shiro-550反序列化
  • 原文地址:https://blog.csdn.net/javaee_gao/article/details/126699444