背景
Shiro的SecurityUtils.getSubject()是如何获取到正确的用户信息的呢?昨日在测试接口时使用apifox发送请求时,我发现先使用A用户登录,再使用B用户登录,再使用A的登录凭证(jwt)去访问某个接口,使用的却是B的登录信息。这让我非常困扰。
结论
可以把apifox当成一个浏览器,它去访问一个网站的这些接口,后端给你的JSESSIONID是共同享有的,就是你使用这个软件去访问后端接口用的一个JSESSIONID,也就是一个Session。而用户信息(也就是登录缓存信息)它是放在这个session里面的,每次调用SecurityUtils.getSubject()方法,它会去当前所处线程获取用户信息,组装subject返回。如果是没有登录过的,他就会为当前访问的JSESSIONID创建一个subject,只是这个的就是未登录状态。
排查过程
环境搭建
由于本次仅研究调用SecurityUtils.getSubject()如何获取到正确的subject(用户)。相关的realm配置自行参考我上一章或者其他博客搭建,注意这里我是使用的shiro+jwt方式,其实其他方式shiro获取subject的内容也是一样的,只是认证方式不同,所以我这里去认证接口是会带上一个jwt值(header里面的Authorization)。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!--springboot对shiro集成-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>1.6.0</version>
</dependency>
我的登录接口
@PostMapping("/login")
public Result login(@Validated @RequestBody UserLogin userLogin,
HttpServletRequest request,
HttpServletResponse response){
Subject subject = SecurityUtils.getSubject();
if(subject.isAuthenticated()){
subject.logout();
}
//登录操作,获得jwt
String jwt = userService.login(userLogin);
userService.setAuthorization(response,jwt); //将jwt放在header里面
userService.recordLogin();
return Result.success();
}
排查
从ThreadContext获取subject
首先我们去看SecurityUtils.getSubject()的源码,里面的第一行就是:
Subject subject = ThreadContext.getSubject();
我们能发现shiro是维护了一个线程上下文的ThreadContext,在里面获取subject。那么ThreadContext的subject是从哪儿来的呢?
这里的SUBJECT_KEY是一个固定的值,用来标识线程上下文中当前的subject。
public static final String SUBJECT_KEY = ThreadContext.class.getName() + "_SUBJECT_KEY";
我们发现最后这个subject是从ThreadContext维护的resources中获取的map集合,再使用key:SUBJECT_KEY的值去map集合中获取subject
get的代码:其实就是获取当前线程的map集合(不懂的可以去了解下ThreadLocal)
最后我们知道了subject的获取首先会去当前的线程上下文(ThreadContext)中获取,而最后都是指向ThreadContext维护的resources中。但是resources里面的subject又是谁去设置的呢。
resources中的subject如何产生
我们在ThreadContext中查找resouces相关方法,发现有一个put方法。于是我们可以在这里面打上断点,查看到底是谁在执行put方法。
我们按照执行栈往回找,查看有用的信息,我发现这里有一个
Subject subject = this.createSubject(request, response);
以及方法上面有两个方法对servletRequest, servletResponse包装,其中servletRequest被封装成SHiroHttpServletRequest返回,其实这里就是对servletRequest绑定了线程上下文(ThreadLocal)。
我们的重心回到subject创建,我们发现这里createSubject是将manager以及request,response都获取去创建
这个Builder的构造方法去设置了request,response。其实他就是将这两个值绑定到了当前用户上下文中,方便后续使用
重点:现在到获取的重点了,这里依次去调用,最后我们发现生产subject的核心就在这个createSubject中
这里它首先去拷贝了上下文中的内容,哎?这里干嘛要拷贝一份啊?不能直接用么。当然不能啊!
你看拷贝后面依次执行的是什么嘛:
this.ensureSecurityManager(context)去查看context中的SecurityManager有没有设置,如果没有就重新设置默认SecurityManager返回。 context = this.resolveSession(context);去解析session,将session放入对象上下文context中。注意这里就是从我们之前设置的request中获取session,session里面就是有登录信息的哦(不知道session是啥的伙伴自行查找)。这一步就相当于是把request的session绑定到对象上下文中。
context = this.resolvePrincipals(context);就是获取登录信息,放入contxet中。从哪儿获取呢?他会先去看如果你是认证过的,我就直接从认证信息中拿。如果没有,我就去看看当前的subject中有没有,最后都没有我就去看看session里面存不存在
Subject subject = this.doCreateSubject(context);然后就根据这些对象上下文信息去创建subject。
this.save(subject);最后save保存一下。啊?保存啥啊?其实他就是去将subject放入整个shiro维护的session中(也包括什么合并登录信息,登录状态之类,其实就是保证数据一致),不然你创建了谁能获取到啊。
回到之前,我们为什么需要拷贝啊?我的理解是因为在创建subject的过程中随时会发生错误,在创建成功前需要保留之前的subject来表明正确的当前用户。
到此subject就算是创建完成了。最后将subject放入resources就算完成了。
觉得文章写得有问题,欢迎指正!