会话保持必须要前后端的配合,前端需要传递一个会话的唯一标识,后端才能取出会话。这个规则在任何场景、任何应用中都是一样的。
在web应用中,前端的这个唯一标识可以是cookie,这样的话,很多人会忽略这一点,这是非常不应该的,这样的话,前端在会话上不需要写代码,直接依赖浏览器的cookie能力即可。在非web应用中,没有浏览器的cookie能力,就需要前端自行解决会话唯一标识的保存和传递。
在shiro中也是一样,使用在web应用中时,简单架构上可能不需要我们管会话唯一标识,但你不能忽略cookie的存在。对于web应用而言,会话接口通常封装在Request中,以Tomcat为例,HttpServletRequest接口就定义了getSession()方法,用于获取创建会话。
在shiro中,有一个ShiroHttpServletRequest,它继承了HttpServletRequestWrapper,对于HttpServletRequestWrapper我们必须要了解,否则就很难解释后面的内容了。javax.servlet.ServletRequest这个接口是tomcat对Servlet标准中request的顶级接口定义,javax.servlet.http.HttpServletRequest接口继承了ServletRequest接口,javax.servlet.ServletRequestWrapper类实现了接口ServletRequest,javax.servlet.http.HttpServletRequestWrapper继承了ServletRequestWrapper且实现了HttpServletRequest。这种类层次设计思路很常见,一定要学会。顶层接口有一个实现类,顶层接口也会有第二层接口,第二层接口的实现类往往会继承顶层接口的实现类,这样就不用把顶层接口里的接口方法全部重新实现一遍。
HttpServletRequestWrapper是一个功能齐全的Servlet请求类,所以如果你的框架需要重新定义ServletReqeust的行为,那么继承HttpServletRequestWrapper是一个非常正确的决定,因为很多框架就是这个做的。shiro也不例外。在shiro中,有一个org.apache.shiro.web.servlet.ShiroHttpServletRequest:
/**
* A {@code ShiroHttpServletRequest} wraps the Servlet container's original {@code ServletRequest} instance, but ensures
* that all {@link HttpServletRequest} invocations that require Shiro's support ({@link #getRemoteUser getRemoteUser},
* {@link #getSession getSession}, etc) can be executed first by Shiro as necessary before allowing the underlying
* Servlet container instance's method to be invoked.
*
* @since 0.2
*/
public class ShiroHttpServletRequest extends HttpServletRequestWrapper {
...
}
shiro对此的解释是:ShiroHttpServletRequest包装了Servlet容器的原始请求实例,但是保证了需要shiro支持的方法能在容器实例方法调用之前被调用,如:getRemoteUser(),getSession()等。
看到这里,就应该知道了,shiro会插手原生的会话方法。下面我们就来看看shiro会话管理的逻辑。
在shiro中,可以使用原生的session,也可以使用shiro的session,取决于ShiroHttpServletRequest#getSession()方法:
public class ShiroHttpServletRequest extends HttpServletRequestWrapper {
public HttpSession getSession(boolean create) {
HttpSession httpSession;
if (isHttpSessions()) {
httpSession = super.getSession(false);
if (httpSession == null && create) {
//Shiro 1.2: assert that creation is enabled (SHIRO-266):
if (WebUtils._isSessionCreationEnabled(this)) {
httpSession = super.getSession(create);
} else {
throw newNoSessionCreationException();
}
}
} else {
boolean existing = getSubject().getSession(false) != null;
if (this.session == null || !existing) {
Session shiroSession = getSubject().getSession(create);
if (shiroSession != null) {
this.session = new ShiroHttpSession(shiroSession, this, this.servletContext);
if (!existing) {
setAttribute(REFERENCED_SESSION_IS_NEW, Boolean.TRUE);
}
} else if (this.session != null) {
this.session = null;
}
}
httpSession = this.session;
}
return httpSession;
}
public HttpSession getSession() {
return getSession(true);
}
}
需要说明的是,使用shiro的session时,session是存储到subject中的,其中的关键判断是isHttpSessions(),这个方法追溯到最上面是:org.apache.shiro.web.mgt.DefaultWebSecurityManager#isHttpSessionMode()
public class DefaultWebSecurityManager extends DefaultSecurityManager implements WebSecurityManager {
public boolean isHttpSessionMode() {
SessionManager sessionManager = getSessionManager();
return sessionManager instanceof WebSessionManager && ((WebSessionManager)sessionManager).isServletContainerSessions();
}
}
在web环境中我们使用的SessionManager通常是WebSessionManager的子类,所以第一个条件通常是满足的,第二个条件取决于具体的子类
public class DefaultWebSessionManager extends DefaultSessionManager implements WebSessionManager {
/**
* This is a native session manager implementation, so this method returns {@code false} always.
*
* @return {@code false} always
* @since 1.2
*/
public boolean isServletContainerSessions() {
return false;
}
}
public class ServletContainerSessionManager implements WebSessionManager {
/**
* This implementation always delegates to the servlet container for sessions, so this method returns
* {@code true} always.
*
* @return {@code true} always
* @since 1.2
*/
public boolean isServletContainerSessions() {
return true;
}
}