其实关于Shiro的一些学习笔记很早就该写了,由于懒癌和拖延症晚期一直没有落实,直到今天公司的一个项目碰到了在集群环境的单点登陆频繁掉线的问题,为了解决这个问题,Shiro相关的文档和教程没少翻。最后问题解决了,但我以为我也是时候来作一波Shiro学习笔记了。html
本篇是Shiro系列第四篇,Shiro中的过滤器初始化流程和实现原理。Shiro基于URL的权限控制是经过Filter实现的,本篇从咱们注入的ShiroFilterFactoryBean开始入手,翻看源码追寻Shiro中的过滤器的实现原理。java
首发地址:https://www.guitu18.com/post/2019/08/08/45.html缓存
Session服务器
SessionManagersession
咱们在配置Shiro时配置了一个DefaultWebSecurityManager,先来看下分布式
DefaultWebSecurityManageride
public DefaultWebSecurityManager() { super(); ((DefaultSubjectDAO) this.subjectDAO).setSessionStorageEvaluator(new DefaultWebSessionStorageEvaluator()); this.sessionMode = HTTP_SESSION_MODE; setSubjectFactory(new DefaultWebSubjectFactory()); setRememberMeManager(new CookieRememberMeManager()); setSessionManager(new ServletContainerSessionManager()); }
在它的构造方法中注入了一个ServletContainerSessionManageroop
public class ServletContainerSessionManager implements WebSessionManager { public Session getSession(SessionKey key) throws SessionException { if (!WebUtils.isHttp(key)) { String msg = "SessionKey must be an HTTP compatible implementation."; throw new IllegalArgumentException(msg); } HttpServletRequest request = WebUtils.getHttpRequest(key); Session session = null; HttpSession httpSession = request.getSession(false); if (httpSession != null) { session = createSession(httpSession, request.getRemoteHost()); } return session; } private String getHost(SessionContext context) { String host = context.getHost(); if (host == null) { ServletRequest request = WebUtils.getRequest(context); if (request != null) { host = request.getRemoteHost(); } } return host; } protected Session createSession(SessionContext sessionContext) throws AuthorizationException { if (!WebUtils.isHttp(sessionContext)) { String msg = "SessionContext must be an HTTP compatible implementation."; throw new IllegalArgumentException(msg); } HttpServletRequest request = WebUtils.getHttpRequest(sessionContext); HttpSession httpSession = request.getSession(); String host = getHost(sessionContext); return createSession(httpSession, host); } protected Session createSession(HttpSession httpSession, String host) { return new HttpServletSession(httpSession, host); } }
ServletContainerSessionManager自己并无论理会话,它最终操做的仍是HttpSession,因此只能在Servlet容器中起做用,它不能支持除使用HTTP协议的以外的任何会话。post
因此通常咱们配置Shiro都会配置一个DefaultWebSessionManager,它继承了DefaultSessionManager,看看DefaultSessionManager的构造方法:学习
public DefaultSessionManager() { this.deleteInvalidSessions = true; this.sessionFactory = new SimpleSessionFactory(); this.sessionDAO = new MemorySessionDAO(); }
这里的sessionDAO初始化了一个MemorySessionDAO,它其实就是一个Map,在内存中经过键值对管理Session。
public MemorySessionDAO() { this.sessions = new ConcurrentHashMap<Serializable, Session>(); }
HttpServletSession
public class HttpServletSession implements Session { public HttpServletSession(HttpSession httpSession, String host) { if (httpSession == null) { String msg = "HttpSession constructor argument cannot be null."; throw new IllegalArgumentException(msg); } if (httpSession instanceof ShiroHttpSession) { String msg = "HttpSession constructor argument cannot be an instance of ShiroHttpSession. This " + "is enforced to prevent circular dependencies and infinite loops."; throw new IllegalArgumentException(msg); } this.httpSession = httpSession; if (StringUtils.hasText(host)) { setHost(host); } } protected void setHost(String host) { setAttribute(HOST_SESSION_KEY, host); } public void setAttribute(Object key, Object value) throws InvalidSessionException { try { httpSession.setAttribute(assertString(key), value); } catch (Exception e) { throw new InvalidSessionException(e); } } }
Shiro的HttpServletSession只是对javax.servlet.http.HttpSession进行了简单的封装,因此在Web应用中对Session的相关操做最终都是对javax.servlet.http.HttpSession进行的,好比上面代码中的setHost()是将内容以键值对的形式保存在httpSession中。
先来看下这张图:
先了解上面这几个类的关系和做用,而后咱们想管理Shiro中的一些数据就很是方便了。
SessionDao是Session管理的顶层接口,定义了Session的增删改查相关方法。
public interface SessionDAO { Serializable create(Session session); Session readSession(Serializable sessionId) throws UnknownSessionException; void update(Session session) throws UnknownSessionException; void delete(Session session); Collection<Session> getActiveSessions(); }
AbstractSessionDao是一个抽象类,在它的构造方法中定义了JavaUuidSessionIdGenerator做为SessionIdGenerator用于生成SessionId。它虽然实现了create()和readSession()两个方法,但具体的流程调用的是它的两个抽象方法doCreate()和doReadSession(),须要它的子类去干活。
public abstract class AbstractSessionDAO implements SessionDAO { private SessionIdGenerator sessionIdGenerator; public AbstractSessionDAO() { this.sessionIdGenerator = new JavaUuidSessionIdGenerator(); } public Serializable create(Session session) { Serializable sessionId = doCreate(session); verifySessionId(sessionId); return sessionId; } protected abstract Serializable doCreate(Session session); public Session readSession(Serializable sessionId) throws UnknownSessionException { Session s = doReadSession(sessionId); if (s == null) { throw new UnknownSessionException("There is no session with id [" + sessionId + "]"); } return s; } protected abstract Session doReadSession(Serializable sessionId); }
看上面那张类图AbstractSessionDao的子类有三个,查看源码发现CachingSessionDAO是一个抽象类,它并无实现这两个方法。在它的子类EnterpriseCacheSessionDAO中实现了doCreate()和doReadSession(),但doReadSession()是一个空实现直接返回null。
public EnterpriseCacheSessionDAO() { setCacheManager(new AbstractCacheManager() { @Override protected Cache<Serializable, Session> createCache(String name) throws CacheException { return new MapCache<Serializable, Session>(name, new ConcurrentHashMap<Serializable, Session>()); } }); }
EnterpriseCacheSessionDAO依赖于它的父级CachingSessionDAO,在他的构造方法中向父类注入了一个AbstractCacheManager的匿名实现,它是一个基于内存的SessionDao,它所建立的MapCache就是一个Map。
咱们在Shiro配置类里经过 sessionManager.setSessionDAO(new EnterpriseCacheSessionDAO()); 来使用它。而后在CachingSessionDAO.getCachedSession() 打个断点测试一下,能够看到cache就是一个ConcurrentHashMap,在内存中以Key-Value的形式保存着JSESSIONID和Session的映射关系。
再来看AbstractSessionDao的第三个实现MemorySessionDAO,它就是一个基于内存的SessionDao,简单直接,构造方法直接new了一个ConcurrentHashMap。
public MemorySessionDAO() { this.sessions = new ConcurrentHashMap<Serializable, Session>(); }
那么它和EnterpriseCacheSessionDAO有啥区别,其实EnterpriseCacheSessionDAO只是CachingSessionDAO的一个默认实现,在CachingSessionDAO中cacheManager是没有默认值的,在EnterpriseCacheSessionDAO的构造方法将其初始化为一个ConcurrentHashMap。
若是咱们直接用EnterpriseCacheSessionDAO其实和MemorySessionDAO其实没有什么区别,都是基于Map的内存型SessionDao。可是CachingSessionDAO的目的是为了方便扩展的,用户能够继承CachingSessionDAO并注入本身的Cache实现,好比以Redis缓存Session的RedisCache。
其实际业务上若是须要Redis来管理Session,那么直接继承AbstractSessionDao更好,有Redis支撑中间的Cache就是多余的,这样还能够作分布式或集群环境的Session共享(分布式或集群环境若是中间还有一层Cache那么还要考虑同步问题)。
回到开篇提到的咱们公司项目集群环境下单点登陆频繁掉线的问题,其实就是中间那层Cache形成的。咱们的业务代码中RedisSessionDao是继承自EnterpriseCacheSessionDAO的,这样一来那在Redis之上还有一个基于内存的Cache层。此时用户的Session若是发生变动,虽然Redis中的Session是同步的,Cache层没有同步,致使的现象就是用户在一台服务器的Session是有效的,另外一台服务器Cache中的Session仍是旧的,而后用户就被迫下线了。