本篇文章写于2021年02月04日,从公众号|掘金|CSDN手工同步过来(博客搬家),本篇为原创文章。
我们将原有项目的登录授权功能从Shiro
切换到接入SSO
单点登录服务并非一帆风顺,因为系统多了,总有一些让我们预想不到的骚操作。
比如这个,在处理请求的线程上启动一个线程,在这个新的线程中获取Session
,从Session
获取登录用户。
这真的是一个骚操作了!
由于未考虑到这种情况,上线就出现了Bug
,好在不是很严重的问题。
我们不讨论这样的骚操作有何不好,重要的如何解决问题。找到各种这样的骚操作修改过来?难!没有那么多时间慢慢去改,而且改完还需要走完一套测试流程。
这件事情搞得我跟同事争论了一番,毕竟之前是没有出现这个问题的,而就切换到SSO
(为方便接入SSO
服务而封装的SDK
)就出现问题,不是我的问题又是谁的问题呢!
上图的代码经测试确实没有问题,测试结果如下图所示。
首先我们要知道,Session ID
由服务端创建,并通过响应头cookie
响应给浏览器,浏览器将Session ID
存储在本地,会在下次请求自动带上Session ID
,通过cookie
请求头发送给服务端。
在服务端接收到客户端请求时,如果客户端有带上Session ID
,那么就根据Session ID
获取Session
,默认是从内存获取,如果是使用Shiro
框架并且使用Redis
存储Session
,那么就是从Redis
中获取。
如果Session
过期或者客户端没有传递Session ID
,则创建新的Session
,并会为新的Session
重新分配Session ID
。
Shrio
框架在接收到请求时就拿到Session ID
存储到ThreadLocal
中了,所以不需要通过HttpServletRequest
去拿Session
。
Shrio
之所以支持在异步线程中还能够获取到Session
,这其实是因为Shrio
使用的是InheritableThreadLocal
,而不是ThreadLocal
,实现了将Session ID
传给给子线程,因此实现了“异步上下文”传递Session
。
但这也是有局限的,要求这个异步线程必须是由当前处理请求的线程创建的,Session ID
才能通过InheritableThreadLocal
传递给子线程,如果是在线程池中,就不一定能获取到了。
这里留个思考题给大家:为什么说在线程池中不一定能获取到,而不是一定获取不到?要理解这个问题需要对线程池的工作源码、源码,以及InheritableThreadLocal
源码理解,因此本篇不展开分析。
所以,我的一行代码解决Bug
就是将ThreadLocal
换成InheritableThreadLocal
。并且通过方法拦截器(HandlerInterceptor
)或者过滤器(Filter
)实现set session
和remove session
操作,推荐后者。
另外,从webmcv
框架源码可以看出,RequestContextHolder#getRequestAttributes
也是支持InheritableThreadLocal
的,只是默认情况下不支持,需要修改配置。
getRequestAttributes
方法会尝试从InheritableThreadLocal
获取,源码如下。
但能不能获取得到由是否写入决定:
setRequestAttributes
方法由RequestContextFilter
过滤器调用,该过滤器由webmvc
框架自动配置,代码如下。
默认RequestContextFilter
并不会将ServletRequestAttributes
写入InheritableThreadLocal
,代码如下。
因此,我们是否可以替换默认注册的RequestContextFilter
,将threadContextInheritable
配置为true
,这样就能支持将Session ID
传递给子线程了,如下代码所示。
但,这并不是有效的,因为在RequestContextFilter
之后,DispatcherServlet
又调用了一次RequestContextHolder#setRequestAttributes
,并且传入的threadContextInheritable
为false
,清除了前面的写入,所以必须要修改DispatcherServlet
的threadContextInheritable
为true
才支持。但不建议在封装的SDK
中这样改动。
关于为什么threadContextInheritable
默认为false
,官方在RequestContextFilter
的API
文档给出了如下说明。
WARNING: Do not use inheritance for child threads if you are accessing a thread pool which is configured to potentially add new threads on demand (e.g. a JDK java.util.concurrent.ThreadPoolExecutor), since this will expose the inherited context to such a pooled thread.