关于Shiro的退出请求是如何关联到登录请求的思考
一、结论
先给出结论,是因为本身是很简单的道理。假设我们没有使用任何认证授权的框架,就简单的使用Cookie和HttpSession,那么用户登录后的每一个请求是如何关联上这个用户的呢?答案很简单,由于每个请求Tomcat使用一个单独线程来处理,但是Http请求时是有cookie的,那么一般来说是在cookie中加入sessionId,后台服务根据sessionId去查找HttpSession,这样就可以关联起来了。这本是很基础的内容,为什么在使用Shiro的时候还有这个疑问呢?
二、缘由
起初我的认知:
- shiro为每一个用户创建了一个Subject(这个实际并不是每一个用户,只是之前是这样认为的),这个Subject是使用ThreadLocal绑定的。
- 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里层的过滤器开始跟踪代码,发现在AbstractShiroFilter
的doFilterInternal
方法中有关键代码:
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方法,找到关键代码DefaultSecurityManager
的createSubject
方法
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即可实现退出功能。