关于Shiro的退出请求是如何关联到登录请求的思考

一、结论

先给出结论,是因为本身是很简单的道理。假设我们没有使用任何认证授权的框架,就简单的使用Cookie和HttpSession,那么用户登录后的每一个请求是如何关联上这个用户的呢?答案很简单,由于每个请求Tomcat使用一个单独线程来处理,但是Http请求时是有cookie的,那么一般来说是在cookie中加入sessionId,后台服务根据sessionId去查找HttpSession,这样就可以关联起来了。这本是很基础的内容,为什么在使用Shiro的时候还有这个疑问呢?

二、缘由

起初我的认知:

  1. shiro为每一个用户创建了一个Subject(这个实际并不是每一个用户,只是之前是这样认为的),这个Subject是使用ThreadLocal绑定的。
  2. shiro的退出是直接使用的subject.logout()方法,也可以通过subject获取session、token等认证授权信息。

以上两点是我之前对shiro有的认知,所以我以为一个用户登录之后会有一个Subject。因此我就发现这里就有一个疑问的地方,如果一个用户一个Subject,那Subject又是和线程绑定的,用户每一个请求都是一个单独的线程,那么用户的请求是如何与登录请求关联上获取到同一个Subject的呢?

三、关键点分析

这里就直接开始的跟踪源码分析,首先我想的是查看获取Subject相关的源码,跟踪到ThreadContext类中关键代码,两部分

public static Subject getSubject() {
        return (Subject) get(SUBJECT_KEY);
}

public static Object get(Object key) {
        if (log.isTraceEnabled()) {
            String msg = "get() - in thread [" + Thread.currentThread().getName() + "]";
            log.trace(msg);
        }

        Object value = getValue(key);
        if ((value != null) && log.isTraceEnabled()) {
            String msg = "Retrieved value of type [" + value.getClass().getName() + "] for key [" +
                    key + "] " + "bound to thread [" + Thread.currentThread().getName() + "]";
            log.trace(msg);
        }
        return value;
    }

//这里是最终获取到Subject的地方,resource为InheritableThreadLocalMap对象
private static Object getValue(Object key) {
        return resources.get().get(key);
}

另外一个地方

private static final Logger log = LoggerFactory.getLogger(ThreadContext.class);

    public static final String SECURITY_MANAGER_KEY = ThreadContext.class.getName() + "_SECURITY_MANAGER_KEY";
    public static final String SUBJECT_KEY = ThreadContext.class.getName() + "_SUBJECT_KEY";

    private static final ThreadLocal<Map<Object, Object>> resources = new InheritableThreadLocalMap<Map<Object, Object>>();

可以看到,Subject确实是从InheritableThreadLocalMap对象中取出来的。但是为什么是InheritableThreadLocalMap,这样的话,子线程是哪里产生的?
然后从shiro里层的过滤器开始跟踪代码,发现在AbstractShiroFilterdoFilterInternal方法中有关键代码:

try {
            final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain);
            final ServletResponse response = prepareServletResponse(request, servletResponse, chain);
            //关键部分1
            final Subject subject = createSubject(request, response);

            //noinspection unchecked
            //关键部分2
            subject.execute(new Callable() {
                public Object call() throws Exception {
                    updateSessionLastAccessTime(request, response);
                    executeChain(request, response, chain);
                    return null;
                }
            });
        } catch (ExecutionException ex) {
            t = ex.getCause();
        } catch (Throwable throwable) {
            t = throwable;
        }

在这里可以知道了,这里是另起线程来处理之后的事情。并且subject是每一次请求都会创建一个,那么请求之间的subject是如何关联起来的呢?跟踪createSubject方法,找到关键代码DefaultSecurityManagercreateSubject方法

public Subject createSubject(SubjectContext subjectContext) {
        //create a copy so we don't modify the argument's backing map:
        SubjectContext context = copy(subjectContext);

        //ensure that the context has a SecurityManager instance, and if not, add one:
        context = ensureSecurityManager(context);

        //Resolve an associated Session (usually based on a referenced session ID), and place it in the context before
        //sending to the SubjectFactory.  The SubjectFactory should not need to know how to acquire sessions as the
        //process is often environment specific - better to shield the SF from these details:
        context = resolveSession(context);

        //Similarly, the SubjectFactory should not require any concept of RememberMe - translate that here first
        //if possible before handing off to the SubjectFactory:
        context = resolvePrincipals(context);

        Subject subject = doCreateSubject(context);

        //save this subject for future reference if necessary:
        //(this is needed here in case rememberMe principals were resolved and they need to be stored in the
        //session, so we don't constantly rehydrate the rememberMe PrincipalCollection on every operation).
        //Added in 1.2:
        save(subject);

        return subject;
    }

在这个代码之前还有一些处理,主要的包括,将Request和Response对象放入之前的InheritableThreadLocalMap对象中。这里的代码主要是将SecurityManage实例、Session对象以及登录之后的认证信息Principals存入SubjectContext。然后在doCreateSubject方法中将这些内容都赋值给Subject。这样,虽然每一次请求都是一个新的Subject,但是subject里面的内容都是一致的。最后在subject.logout()方法中,删除掉session即可实现退出功能。

参考文章:https://blog.zlf.me/Shiro-web线程绑定解惑.html

原文地址:https://www.cnblogs.com/bencakes/p/9000280.html