After replacing the Shiro framework, there is a bug when it goes online, and the asynchronous thread cannot obtain the Session.

Translation wujiuye 141 0 2021-02-04

This article is a translation of the original text, which can be found at the following link: https://www.wujiuye.com/article/0183c93320e641c59c21dfb28cf5da67

Author: wujiuye
Link: https://www.wujiuye.com/article/0183c93320e641c59c21dfb28cf5da67
Source: 吴就业的网络日记
This article is an original work by the blogger and is not allowed to be reproduced without the blogger's permission.

Switching the login authorization feature from Shiro to integrating with SSO (Single Sign-On) services in our existing project was not a smooth sail, as with more systems in place, there are always some unexpected tricks up our sleeves.

Take this one, for instance: launching a thread within the request-handling thread, acquiring a Session in this new thread, and retrieving the logged-in user from the Session.

Image of unexpected operation

This is really an unexpected trick!

Since we didn’t consider this scenario, a Bug popped up as soon as we went live. Fortunately, it’s not a serious issue.

Let’s not discuss the downsides of such unexpected tricks; what’s important is how to solve the problem. Finding and fixing all these unexpected tricks? That’s tough! We don’t have the luxury of time to slowly make changes, and after making changes, we still need to go through a full testing process.

This situation led to a debate with my colleagues. After all, this problem didn’t occur before, but it emerged after switching to SSO (an SDK encapsulated for easy integration with SSO services). If it’s not my fault, then whose fault is it?

The code in the above image has been tested and confirmed to be problem-free. The test results are shown in the figure below.

Test results image

Firstly, we need to understand that the Session ID is created by the server and responded to the browser through the response header cookie. The browser stores the Session ID locally and will automatically carry it in the next request, sending it to the server via the cookie request header.

When the server receives a client request, if the client carries a Session ID, it retrieves the Session based on the Session ID, which is retrieved from memory by default. If the Shiro framework is used and Session is stored in Redis, then it is retrieved from Redis.

If the Session has expired or the client does not pass the Session ID, a new Session is created, and a new Session ID is assigned for the Session.

The Shiro framework retrieves the Session ID upon receiving a request and stores it in ThreadLocal, so there is no need to obtain the Session through HttpServletRequest.

The reason why Shiro supports obtaining the Session in asynchronous threads is that it uses InheritableThreadLocal instead of ThreadLocal, achieving the transmission of Session ID to child threads and thus realizing the “asynchronous context” transmission of Session.

However, this also has its limitations. It requires that the asynchronous thread must be created by the current thread handling the request for the Session ID to be passed to the child thread through InheritableThreadLocal. If it’s in a thread pool, it may not be accessible.

Here’s a thought-provoking question for everyone: Why is it said that it may not be accessible in the thread pool, rather than definitely not accessible? Understanding this question requires an understanding of the thread pool’s working code, source code, and the source code of InheritableThreadLocal. Therefore, this article will not analyze it further.

So, my one-liner solution to the Bug was to replace ThreadLocal with InheritableThreadLocal. And implement set session and remove session operations through method interceptors (HandlerInterceptor) or filters (Filter), with the latter being recommended.

In addition, from the source code of the webmvc framework, it can be seen that RequestContextHolder#getRequestAttributes also supports InheritableThreadLocal, but it is not supported by default and requires configuration modification.

The getRequestAttributes method will try to obtain from InheritableThreadLocal, as shown in the following source code.

Source code of getRequestAttributes

But whether it can be obtained or not depends on whether it is written:

Determination of obtainability

The setRequestAttributes method is called by the RequestContextFilter filter, which is automatically configured by the webmvc framework, as shown in the following code.

Automatic configuration of RequestContextFilter

By default, RequestContextFilter does not write ServletRequestAttributes to InheritableThreadLocal, as shown in the following code.

Default behavior of RequestContextFilter

So, can we replace the default registered RequestContextFilter and set threadContextInheritable to true, so as to support the transmission of Session ID to child threads, as shown in the following code.

Code modification for threadContextInheritable

However, this is not effective because after RequestContextFilter, DispatcherServlet calls RequestContextHolder#setRequestAttributes again, and the passed threadContextInheritable is false, which clears the previous write. Therefore, it is necessary to modify DispatcherServlet’s threadContextInheritable to true to support it. However, it is not recommended to make such changes in the encapsulated SDK.

Regarding why threadContextInheritable is set to false by default, the official API documentation of RequestContextFilter provides the following explanation.

Official explanation for default value

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.