替换Shiro框架后,上线就Bug了,异步线程获取不到Session

原创 吴就业 187 0 2021-02-04

本文为博主原创文章,未经博主允许不得转载。

本文链接:https://wujiuye.com/article/0183c93320e641c59c21dfb28cf5da67

作者:吴就业
链接:https://wujiuye.com/article/0183c93320e641c59c21dfb28cf5da67
来源:吴就业的网络日记
本文为博主原创文章,未经博主允许不得转载。

本篇文章写于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 sessionremove session操作,推荐后者。

另外,从webmcv框架源码可以看出,RequestContextHolder#getRequestAttributes也是支持InheritableThreadLocal的,只是默认情况下不支持,需要修改配置。

getRequestAttributes方法会尝试从InheritableThreadLocal获取,源码如下。

但能不能获取得到由是否写入决定:

setRequestAttributes方法由RequestContextFilter过滤器调用,该过滤器由webmvc框架自动配置,代码如下。

默认RequestContextFilter并不会将ServletRequestAttributes写入InheritableThreadLocal,代码如下。

因此,我们是否可以替换默认注册的RequestContextFilter,将threadContextInheritable配置为true,这样就能支持将Session ID传递给子线程了,如下代码所示。

但,这并不是有效的,因为在RequestContextFilter之后,DispatcherServlet又调用了一次RequestContextHolder#setRequestAttributes,并且传入的threadContextInheritablefalse,清除了前面的写入,所以必须要修改DispatcherServletthreadContextInheritabletrue才支持。但不建议在封装的SDK中这样改动。

关于为什么threadContextInheritable默认为false,官方在RequestContextFilterAPI文档给出了如下说明。

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.

#后端

声明:公众号、CSDN、掘金的曾用名:“Java艺术”,因此您可能看到一些早期的文章的图片有“Java艺术”的水印。

文章推荐

Java文件的简单读写、随机读写、NIO读写与使用MappedByteBuffer读写

面向文件编程的重要性;简单文件读写;随机访问文件读写;NIO文件读写-FileChannel;使用MappedByteBuffer读写文件。

浅谈面向文件编程(文件读写)的重要性

在我印象中,似乎很少有关于文件操作的面试题,而大多数面试题都围绕着多线程、网络编程、RPC、数据库,但其实掌握文件操作也同等重要。

InheritableThreadLocal异步传递数据实现原理

继《反向理解ThreadLocal,或许这样更容易理解》,本篇介绍InheritableThreadLocal异步传递数据的实现原理。

如何实现SSO单点登录

随着公司业务的发展,子系统越来越多,实现SSO单点登录的需求就愈加迫切。本篇介绍笔者如何实现SSO单点登录系统。

如何并行消费Kafka拉取的数据库Binlog,提升吞吐量

本篇介绍如何并行消费Kafka拉取的数据库Binlog,以及使用Kafka订阅Binlog字段值获取防坑指南(阿里云DTS)。

深入浅出反应式编程原理,反应式编程入门

反应式编程不适用于业务开发,特别是复杂业务系统的开发,这或许就是反应式编程从推出到现在依然不温不火的原因吧。