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
.
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.
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.
But whether it can be obtained or not depends on whether it is written:
The setRequestAttributes
method is called by the RequestContextFilter
filter, which is automatically configured by the webmvc
framework, as shown in the following code.
By default, RequestContextFilter
does not write ServletRequestAttributes
to InheritableThreadLocal
, as shown in the following code.
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.
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.
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.