运营人员反映,经常出现提示登录超时的情况;第一反映应该是session保存超时;由于项目中使用redis保存session,并且设置了超时时间(想法是,session的过期交由redis超时控制);查看配置没发现问题;于是就从http请求流程配合shiro源码追踪问题;
1、shiro过滤器。每次请求都会经过该过滤器,且一次请求只执行一次;AbstractShiroFilter主要代码
protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain)
throws ServletException, IOException {
Throwable t = null;
try {
final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain);//把容器生成的HttpServletRequest封装成ShiroHttpServletRequest
final ServletResponse response = prepareServletResponse(request, servletResponse, chain);
final Subject subject = createSubject(request, response);//每次都生成subject;
//noinspection unchecked
subject.execute(new Callable() {
public Object call() throws Exception {
updateSessionLastAccessTime(request, response);//更新session的最后更新时间
executeChain(request, response, chain);
return null;
}
});
...
protected WebSubject createSubject(ServletRequest request, ServletResponse response) {
return new WebSubject.Builder(getSecurityManager(), request, response).buildWebSubject();
}
Builder方法是根据SecurityManager生成一个SubjectContext实例,该类继承MapContext---该请求过长的securityManager、Request、Response、Session等都会被保存在改map中(可能是为了内部跳转不用再创建subject);
2、创建subject; SecurityManager中的createSubject
/**
* This implementation functions as follows:
* <p/>
* <ol>
* <li>Ensures the {@code SubjectContext} is as populated as it can be, using heuristics to acquire
* data that may not have already been available to it (such as a referenced session or remembered principals).</li>
* <li>Calls {@link #doCreateSubject(org.apache.shiro.subject.SubjectContext)} to actually perform the
* {@code Subject} instance creation.</li>
* <li>calls {@link #save(org.apache.shiro.subject.Subject) save(subject)} to ensure the constructed
* {@code Subject}'s state is accessible for future requests/invocations if necessary.</li>
* <li>returns the constructed {@code Subject} instance.</li>
* </ol>
*
* @param subjectContext any data needed to direct how the Subject should be constructed.
* @return the {@code Subject} instance reflecting the specified contextual data.
* @see #ensureSecurityManager(org.apache.shiro.subject.SubjectContext)
* @see #resolveSession(org.apache.shiro.subject.SubjectContext)
* @see #resolvePrincipals(org.apache.shiro.subject.SubjectContext)
* @see #doCreateSubject(org.apache.shiro.subject.SubjectContext)
* @see #save(org.apache.shiro.subject.Subject)
* @since 1.0
*/
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);//确保SecurityManager是有的,如果没有把自身设置给subjectContext
//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);//根据sessionID获取session(也就是从redis中获取)。并设置到subjectContext的Map中;
//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);//开始创建subject
//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);//保存subject,其实就是保存session
return subject;
}
3、创建web相关subject;具体在DefaultWebSubjectFactory中
public Subject createSubject(SubjectContext context) {
if (!(context instanceof WebSubjectContext)) {//判断是否是web环境
return super.createSubject(context);
}
WebSubjectContext wsc = (WebSubjectContext) context;
SecurityManager securityManager = wsc.resolveSecurityManager();//其实就是从subjectContext的map中获取
Session session = wsc.resolveSession();//其实就是从subjectContext的map中获取
boolean sessionEnabled = wsc.isSessionCreationEnabled();
PrincipalCollection principals = wsc.resolvePrincipals();
boolean authenticated = wsc.resolveAuthenticated();//重点关注着个句代码
String host = wsc.resolveHost();
ServletRequest request = wsc.resolveServletRequest();
ServletResponse response = wsc.resolveServletResponse();
return new WebDelegatingSubject(principals, authenticated, host, session, sessionEnabled,
request, response, securityManager);
}
4、wsc.resolveAuthenticated()
public boolean resolveAuthenticated() {
Boolean authc = getTypedValue(AUTHENTICATED, Boolean.class);//这里既是从map中取;如当前请求是登录请求那么会有值,否则是null;
if (authc == null) {
//see if there is an AuthenticationInfo object. If so, the very presence of one indicates a successful
//authentication attempt:
AuthenticationInfo info = getAuthenticationInfo();
authc = info != null;
}
if (!authc) {
//fall back to a session check:
Session session = resolveSession();
if (session != null) {
Boolean sessionAuthc = (Boolean) session.getAttribute(AUTHENTICATED_SESSION_KEY);//从session中获取是否已经认证
authc = sessionAuthc != null && sessionAuthc;
}
}
return authc;
}
subject.Login的时候回执行如下方法
protected Subject createSubject(AuthenticationToken token, AuthenticationInfo info, Subject existing) {
SubjectContext context = createSubjectContext();
context.setAuthenticated(true);//登录成功后设置为ture
context.setAuthenticationToken(token);
context.setAuthenticationInfo(info);
if (existing != null) {
context.setSubject(existing);
}
return createSubject(context);
}
分析到这里都没发现为何authenticated=false; 除非已经退出登录;
无奈转战ShiroSession本身;session的过期验证方法在自身类;
SimpleSession看着看着发现有个,在AbstractValidatingSessionManager中有这么个setSessionValidationSchedulerEnabled方法,默认为true; 也就是说在不设置SessionValidationScheduler定时校验任务的情况下,shiro会启动一个默认的任务校验session;而项目中的校验方式交由redis了,不是session自身的validate方法了。导致SimpleSession中的lastAccessTime永远等于startTimestamp;。。。。那么问题就应该找到了