前言 開心一刻 老師對小明說:"乳就是小的意思,比如乳豬就是小豬,乳名就是小名,請你用乳字造個句" 小明:"我家很窮,只能住在40平米的乳房" 老師:"..., 這個不行,換一個" 小明:"我每天上學都要跳過我家門口的一條乳溝" 老師:"......, 這個也不行,再換一個" 小明:"老師,我想不出 ...
前言
開心一刻
老師對小明說:"乳就是小的意思,比如乳豬就是小豬,乳名就是小名,請你用乳字造個句"
小明:"我家很窮,只能住在40平米的乳房"
老師:"..., 這個不行,換一個"
小明:"我每天上學都要跳過我家門口的一條乳溝"
老師:"......, 這個也不行,再換一個"
小明:"老師,我想不出來了,把我的乳頭都想破了!"
路漫漫其修遠兮,吾將上下而求索!
github:https://github.com/youzhibing
碼雲(gitee):https://gitee.com/youzhibing
前情回顧
shiro的session創建與session的查詢、更新、過期、刪除中,shiro對session的操作基本都講到了,但還缺一個session共用沒有講解;session共用的原理其實在自定義session管理一文已經講過了,本文不講原理,只看看shiro的session共用的實現。
為何需要session共用
如果是單機應用,那麼談不上session共用,session放哪都無所謂,不在乎放到預設的servlet容器中,還是抽出來放到單獨的地方;
也就是說session共用是針對集群(或分散式、或分散式集群)的;如果不做session共用,仍然採用預設的方式(session存放到預設的servlet容器),當我們的應用是以集群的方式發佈的時候,同個用戶的請求會被分發到不同的集群節點(分發依賴具體的負載均衡規則),那麼每個處理同個用戶請求的節點都會重新生成該用戶的session,這些session之間是毫無關聯的。那麼同個用戶的請求會被當成多個不同用戶的請求,這肯定是不行的。
如何實現session共用
實現方式其實有很多,甚至可以不做session共用,具體有哪些,大家自行去查資料。本文提供一種方式:redis實現session共用,就是將session從servlet容器抽出來,放到redis中存儲,所有集群節點都從redis中對session進行操作。
SessionDAO
SessionDAO其實是用於session持久化的,但裡面有緩存部分,具體細節我們往下看
shiro已有SessionDAO的實現如下
SessionDAO介面提供的方法如下
package org.apache.shiro.session.mgt.eis; import org.apache.shiro.session.Session; import org.apache.shiro.session.UnknownSessionException; import java.io.Serializable; import java.util.Collection; /** * 從EIS操作session的規範(EIS:例如關係型資料庫, 文件系統, 持久化緩存等等, 具體依賴DAO實現) * 提供了典型的CRUD的方法:create, readSession, update, delete */ public interface SessionDAO { /** * 插入一個新的sesion記錄到EIS */ Serializable create(Session session); /** * 根據會話ID獲取會話 */ Session readSession(Serializable sessionId) throws UnknownSessionException; /** * 更新session; 如更新session最後訪問時間/停止會話/設置超時時間/設置移除屬性等會調用 */ void update(Session session) throws UnknownSessionException; /** * 刪除session; 當會話過期/會話停止(如用戶退出時)會調用 */ void delete(Session session); /** * 獲取當前所有活躍session, 所有狀態不是stopped/expired的session * 如果用戶量多此方法影響性能 */ Collection<Session> getActiveSessions(); }View Code
SessionDAO給出了從持久層(一般而言是關係型資料庫)操作session的標準。
AbstractSessionDAO提供了SessionDAO的基本實現,如下
package org.apache.shiro.session.mgt.eis; import org.apache.shiro.session.Session; import org.apache.shiro.session.UnknownSessionException; import org.apache.shiro.session.mgt.SimpleSession; import java.io.Serializable; /** * SessionDAO的抽象實現, 在會話創建和讀取時做一些健全性檢查,併在需要時允許可插入的會話ID生成策略. * SessionDAO的update和delete則留給子類來實現 * EIS需要子類自己實現 */ public abstract class AbstractSessionDAO implements SessionDAO { /** * sessionId生成器 */ private SessionIdGenerator sessionIdGenerator; public AbstractSessionDAO() { this.sessionIdGenerator = new JavaUuidSessionIdGenerator(); // 指定JavaUuidSessionIdGenerator為預設sessionId生成器 } /** * 獲取sessionId生成器 */ public SessionIdGenerator getSessionIdGenerator() { return sessionIdGenerator; } /** * 設置sessionId生成器 */ public void setSessionIdGenerator(SessionIdGenerator sessionIdGenerator) { this.sessionIdGenerator = sessionIdGenerator; } /** * 生成一個新的sessionId, 並將它應用到session實例 */ protected Serializable generateSessionId(Session session) { if (this.sessionIdGenerator == null) { String msg = "sessionIdGenerator attribute has not been configured."; throw new IllegalStateException(msg); } return this.sessionIdGenerator.generateId(session); } /** * SessionDAO中create實現; 將創建的sesion保存到EIS. * 子類doCreate方法的代理,具體的細節委托給了子類的doCreate方法 */ public Serializable create(Session session) { Serializable sessionId = doCreate(session); verifySessionId(sessionId); return sessionId; } /** * 保證從doCreate返回的sessionId不是null,並且不是已經存在的. * 目前只實現了null校驗,是否已存在是沒有校驗的,可能shiro的開發者會在後續補上吧. */ private void verifySessionId(Serializable sessionId) { if (sessionId == null) { String msg = "sessionId returned from doCreate implementation is null. Please verify the implementation."; throw new IllegalStateException(msg); } } /** * 分配sessionId給session實例 */ protected void assignSessionId(Session session, Serializable sessionId) { ((SimpleSession) session).setId(sessionId); } /** * 子類通過實現此方法來持久化Session實例到EIS. */ protected abstract Serializable doCreate(Session session); /** * SessionDAO中readSession實現; 通過sessionId從EIS獲取session對象. * 子類doReadSession方法的代理,具體的獲取細節委托給了子類的doReadSession方法. */ 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; } /** * 子類通過實現此方法從EIS獲取session實例 */ protected abstract Session doReadSession(Serializable sessionId); }View Code
SessionDao的基本實現,實現了SessionDao的create、readSession(具體還是依賴AbstractSessionDAO子類的doCreate、doReadSession實現);同時加入了自己的sessionId生成器,負責sessionId的操作。
CachingSessionDAO提供了session緩存的功能,如下
package org.apache.shiro.session.mgt.eis; import org.apache.shiro.cache.Cache; import org.apache.shiro.cache.CacheManager; import org.apache.shiro.cache.CacheManagerAware; import org.apache.shiro.session.Session; import org.apache.shiro.session.UnknownSessionException; import org.apache.shiro.session.mgt.ValidatingSession; import java.io.Serializable; import java.util.Collection; import java.util.Collections; /** * 應用層與持久層(EIS,如關係型資料庫、文件系統、NOSQL)之間的緩存層實現 * 緩存著所有激活狀態的session * 實現了CacheManagerAware,會在shiro載入的過程中調用此對象的setCacheManager方法 */ public abstract class CachingSessionDAO extends AbstractSessionDAO implements CacheManagerAware { /** * 激活狀態的sesion的預設緩存名 */ public static final String ACTIVE_SESSION_CACHE_NAME = "shiro-activeSessionCache"; /** * 緩存管理器,用來獲取session緩存 */ private CacheManager cacheManager; /** * 用來緩存session的緩存實例 */ private Cache<Serializable, Session> activeSessions; /** * session緩存名, 預設是ACTIVE_SESSION_CACHE_NAME. */ private String activeSessionsCacheName = ACTIVE_SESSION_CACHE_NAME; public CachingSessionDAO() { } /** * 設置緩存管理器 */ public void setCacheManager(CacheManager cacheManager) { this.cacheManager = cacheManager; } /** * 獲取緩存管理器 */ public CacheManager getCacheManager() { return cacheManager; } /** * 獲取緩存實例的名稱,也就是獲取activeSessionsCacheName的值 */ public String getActiveSessionsCacheName() { return activeSessionsCacheName; } /** * 設置緩存實例的名稱,也就是設置activeSessionsCacheName的值 */ public void setActiveSessionsCacheName(String activeSessionsCacheName) { this.activeSessionsCacheName = activeSessionsCacheName; } /** * 獲取緩存實例 */ public Cache<Serializable, Session> getActiveSessionsCache() { return this.activeSessions; } /** * 設置緩存實例 */ public void setActiveSessionsCache(Cache<Serializable, Session> cache) { this.activeSessions = cache; } /** * 獲取緩存實例 * 註意:不會返回non-null值 * * @return the active sessions cache instance. */ private Cache<Serializable, Session> getActiveSessionsCacheLazy() { if (this.activeSessions == null) { this.activeSessions = createActiveSessionsCache(); } return activeSessions; } /** * 創建緩存實例 */ protected Cache<Serializable, Session> createActiveSessionsCache() { Cache<Serializable, Session> cache = null; CacheManager mgr = getCacheManager(); if (mgr != null) { String name = getActiveSessionsCacheName(); cache = mgr.getCache(name); } return cache; } /** * AbstractSessionDAO中create的重寫 * 調用父類(AbstractSessionDAO)的create方法, 然後將session緩存起來 * 返回sessionId */ public Serializable create(Session session) { Serializable sessionId = super.create(session); // 調用父類的create方法 cache(session, sessionId); // 以sessionId作為key緩存session return sessionId; } /** * 從緩存中獲取session; 若sessionId為null,則返回null */ protected Session getCachedSession(Serializable sessionId) { Session cached = null; if (sessionId != null) { Cache<Serializable, Session> cache = getActiveSessionsCacheLazy(); if (cache != null) { cached = getCachedSession(sessionId, cache); } } return cached; } /** * 從緩存中獲取session */ protected Session getCachedSession(Serializable sessionId, Cache<Serializable, Session> cache) { return cache.get(sessionId); } /** * 緩存session,以sessionId作為key */ protected void cache(Session session, Serializable sessionId) { if (session == null || sessionId == null) { return; } Cache<Serializable, Session> cache = getActiveSessionsCacheLazy(); if (cache == null) { return; } cache(session, sessionId, cache); } protected void cache(Session session, Serializable sessionId, Cache<Serializable, Session> cache) { cache.put(sessionId, session); } /** * AbstractSessionDAO中readSession的重寫 * 先從緩存中獲取,若沒有則調用父類的readSession方法獲取session */ public Session readSession(Serializable sessionId) throws UnknownSessionException { Session s = getCachedSession(sessionId); // 從緩存中獲取 if (s == null) { s = super.readSession(sessionId); // 調用父類readSession方法獲取 } return s; } /** * SessionDAO中update的實現 * 更新session的狀態 */ public void update(Session session) throws UnknownSessionException { doUpdate(session); // 更新EIS中的session if (session instanceof ValidatingSession) { if (((ValidatingSession) session).isValid()) { cache(session, session.getId()); // 更新緩存中的session } else { uncache(session); // 移除緩存中的sesson } } else { cache(session, session.getId()); } } /** * 由子類去實現,持久化session到EIS */ protected abstract void doUpdate(Session session); /** * SessionDAO中delete的實現 * 刪除session */ public void delete(Session session) { uncache(session); // 從緩存中移除 doDelete(session); // 從EIS中刪除 } /** * 由子類去實現,從EIS中刪除session */ protected abstract void doDelete(Session session); /** * 從緩存中移除指定的session */ protected void uncache(Session session) { if (session == null) { return; } Serializable id = session.getId(); if (id == null) { return; } Cache<Serializable, Session> cache = getActiveSessionsCacheLazy(); if (cache != null) { cache.remove(id); } } /** * SessionDAO中getActiveSessions的實現 * 獲取所有的存活的session */ public Collection<Session> getActiveSessions() { Cache<Serializable, Session> cache = getActiveSessionsCacheLazy(); if (cache != null) { return cache.values(); } else { return Collections.emptySet(); } } }View Code
是應用層與持久化層之間的緩存層,不用頻繁請求持久化層以提升效率。重寫了AbstractSessionDAO中的create、readSession方法,實現了SessionDAO中的update、delete、getActiveSessions方法,預留doUpdate和doDelele給子類去實現(doXXX方法操作的是持久層)
MemorySessionDAO,SessionDAO的簡單記憶體實現,如下
package org.apache.shiro.session.mgt.eis; import org.apache.shiro.session.Session; import org.apache.shiro.session.UnknownSessionException; import org.apache.shiro.util.CollectionUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.Serializable; import java.util.Collection; import java.util.Collections; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; /** * 基於記憶體的SessionDao的簡單實現,所有的session存在ConcurrentMap中 * DefaultSessionManager預設用的MemorySessionDAO */ public class MemorySessionDAO extends AbstractSessionDAO { private static final Logger log = LoggerFactory.getLogger(MemorySessionDAO.class); private ConcurrentMap<Serializable, Session> sessions; // 存放session的容器 public MemorySessionDAO() { this.sessions = new ConcurrentHashMap<Serializable, Session>(); } // AbstractSessionDAO 中doCreate的重寫; 將session存入sessions protected Serializable doCreate(Session session) { Serializable sessionId = generateSessionId(session); // 生成sessionId assignSessionId(session, sessionId); // 將sessionId賦值到session storeSession(sessionId, session); // 存儲session到sessions return sessionId; } // 存儲session到sessions protected Session storeSession(Serializable id, Session session) { if (id == null) { throw new NullPointerException("id argument cannot be null."); } return sessions.putIfAbsent(id, session); } // AbstractSessionDAO 中doReadSession的重寫; 從sessions中獲取session protected Session doReadSession(Serializable sessionId) { return sessions.get(sessionId); } // SessionDAO中update的實現; 更新sessions中指定的session public void update(Session session) throws UnknownSessionException { storeSession(session.getId(), session); } // SessionDAO中delete的實現; 從sessions中移除指定的session public void delete(Session session) { if (session == null) { throw new NullPointerException("session argument cannot be null."); } Serializable id = session.getId(); if (id != null) { sessions.remove(id); } } // SessionDAO中SessionDAO中delete的實現的實現; 獲取sessions中全部session public Collection<Session> SessionDAO中delete的實現() { Collection<Session> values = sessions.values(); if (CollectionUtils.isEmpty(values)) { return Collections.emptySet(); } else { return Collections.unmodifiableCollection(values); } } }View Code
將session保存在記憶體中,存儲結構是ConcurrentHashMap;項目中基本不用,即使我們不實現自己的SessionDAO,一般用的也是EnterpriseCacheSessionDAO。
EnterpriseCacheSessionDAO,提供了緩存功能的session維護,如下
package org.apache.shiro.session.mgt.eis; import org.apache.shiro.cache.AbstractCacheManager; import org.apache.shiro.cache.Cache; import org.apache.shiro.cache.CacheException; import org.apache.shiro.cache.MapCache; import org.apache.shiro.session.Session; import java.io.Serializable; import java.util.concurrent.ConcurrentHashMap; public class EnterpriseCacheSessionDAO extends CachingSessionDAO { public EnterpriseCacheSessionDAO() { // 設置預設緩存器,並實例化MapCache作為cache實例 setCacheManager(new AbstractCacheManager() { @Override protected Cache<Serializable, Session> createCache(String name) throws CacheException { return new MapCache<Serializable, Session>(name, new ConcurrentHashMap<Serializable, Session>()); } }); } // AbstractSessionDAO中doCreate的重寫; protected Serializable doCreate(Session session) { Serializable sessionId = generateSessionId(session); assignSessionId(session, sessionId); return sessionId; } // AbstractSessionDAO中doReadSession的重寫 protected Session doReadSession(Serializable sessionId) { return null; //should never execute because this implementation relies on parent class to access cache, which //is where all sessions reside - it is the cache implementation that determines if the //cache is memory only or disk-persistent, etc. } // CachingSessionDAO中doUpdate的重寫 protected void doUpdate(Session session) { //does nothing - parent class persists to cache. } // CachingSessionDAO中doDelete的重寫 protected void doDelete(Session session) { //does nothing - parent class removes from cache. } }View Code
設置了預設的緩存管理器(AbstractCacheManager)和預設的緩存實例(MapCache),實現了緩存效果。從父類繼承的持久化操作方法(doXXX)都是空實現,也就說EnterpriseCacheSessionDAO是沒有實現持久化操作的,僅僅只是簡單的提供了緩存實現。當然我們可以繼承EnterpriseCacheSessionDAO,重寫doXXX方法來實現持久化操作。
總結下:SessionDAO定義了從持久層操作session的標準;AbstractSessionDAO提供了SessionDAO的基礎實現,如生成會話ID等;CachingSessionDAO提供了對開發者透明的session緩存的功能,只需要設置相應的 CacheManager 即可;MemorySessionDAO直接在記憶體中進行session維護;而EnterpriseCacheSessionDAO提供了緩存功能的session維護,預設情況下使用 MapCache 實現,內部使用ConcurrentHashMap保存緩存的會話。因為shiro不知道我們需要將session持久化到哪裡(關係型資料庫,還是文件系統),所以只提供了MemorySessionDAO持久化到記憶體(聽起來怪怪的,記憶體中能說成持久層嗎)
shiro session共用
共用實現
shiro的session共用其實是比較簡單的,重寫CacheManager,將其操作指向我們的redis,然後實現我們自己的CachingSessionDAO定製緩存操作和緩存持久化。
自定義CacheManager
ShiroRedisCacheManager
package com.lee.shiro.config; import org.apache.shiro.cache.Cache; import org.apache.shiro.cache.CacheException; import org.apache.shiro.cache.CacheManager; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class ShiroRedisCacheManager implements CacheManager { @Autowired private Cache shiroRedisCache; @Override public <K, V> Cache<K, V> getCache(String s) throws CacheException { return shiroRedisCache; } }View Code
ShiroRedisCache
package com.lee.shiro.config; import org.apache.shiro.cache.Cache; import org.apache.shiro.cache.CacheException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import java.util.Collection; import java.util.Set; import java.util.concurrent.TimeUnit; @Component public class ShiroRedisCache<K,V> implements Cache<K,V>{ @Autowired private RedisTemplate<K,V> redisTemplate; @Value("${spring.redis.expireTime}") private long expireTime; @Override public V get(K k) throws CacheException { return redisTemplate.opsForValue().get(k); } @Override public V put(K k, V v) throws CacheException { redisTemplate.opsForValue().set(k,v,expireTime, TimeUnit.SECONDS); return null; } @Override public V remove(K k) throws CacheException { V v = redisTemplate.opsForValue().get(k); redisTemplate.opsForValue().getOperations().delete(k); return v; } @Override public void clear() throws CacheException { } @Override public int size() { return 0; } @Override public Set<K> keys() { return null; } @Override public Collection<V> values() { return null; } }View Code
自定義CachingSessionDAO
繼承EnterpriseCacheSessionDAO,然後重新設置其CacheManager(替換掉預設的記憶體緩存器),這樣也可以實現我們的自定義CachingSessionDAO,但是這是優選嗎;如若我們實現持久化,繼承EnterpriseCacheSessionDAO是優選,但如果只是實現session緩存,那麼CachingSessionDAO是優選,自定義更靈活。那麼我們還是繼承CachingSessionDAO來實現我們的自定義CachingSessionDAO
ShiroSessionDAO
package com.lee.shiro.config; import org.apache.shiro.session.Session; import org.apache.shiro.session.mgt.eis.CachingSessionDAO; import org.springframework.stereotype.Component; import java.io.Serializable; @Component public class ShiroSessionDAO extends CachingSessionDAO { @Override protected void doUpdate(Session session) { } @Override protected void doDelete(Session session) { } @Override protected Serializable doCreate(Session session) { // 這裡綁定sessionId到session,必須要有 Serializable sessionId = generateSessionId(session); assignSessionId(session, sessionId); return sessionId; } @Override protected Session doReadSession(Serializable sessionId) { return null; } }View Code
最後將ShiroSessionDAO實例賦值給SessionManager實例,再講SessionManager實例賦值給SecurityManager實例即可
具體代碼請參考spring-boot-shiro
源碼解析
底層還是利用Filter + HttpServletRequestWrapper將對session的操作接入到自己的實現中來,而不走預設的servlet容器,這樣對session的操作完全由我們自己掌握。
shiro的session創建中其實講到了shiro中對session操作的基本流程,這裡不再贅述,沒看的朋友可以先去看看再回過頭來看這篇。本文只講shiro中,如何將一個請求的session接入到自己的實現中來的;shiro中有很多預設的filter,我會單獨開一篇來講shiro的filter,這篇我們先不糾結這些filter。
OncePerRequestFilter中doFilter方法如下
public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException { String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName(); if ( request.getAttribute(alreadyFilteredAttributeName) != null ) { // 當前filter已經執行過了,進行下一個filter log.trace("Filter '{}' already executed. Proceeding without invoking this filter.", getName()); filterChain.doFilter(request, response); } else //noinspection deprecation if (/* added in 1.2: */ !isEnabled(request, response) || /* retain backwards compatibility: */ shouldNotFilter(request) ) { // 當前filter未被啟用或忽略此filter,則進行下一個filter;shouldNotFilter已經被廢棄了 log.debug("Filter '{}' is not enabled for the current request. Proceeding without invoking this filter.", getName()); filterChain.doFilter(request, response); } else { // Do invoke this filter... log.trace("Filter '{}' not yet executed. Executing now.", getName()); request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE); try { // 執行當前filter doFilterInternal(request, response, filterChain); } finally { // 一旦請求完成,我們清除當前filter的"已經過濾"的狀態 request.removeAttribute(alreadyFilteredAttributeName); } } }View Code
上圖中,我可以看到AbstractShiroFilter的doFilterInternal放中將request封裝成了shiro自定義的ShiroHttpServletRequest,將response也封裝成了shiro自定義的ShiroHttpServletResponse。既然Filter中將request封裝了ShiroHttpServletRequest,那麼到我們應用的request就是ShiroHttpServletRequest類型,也就是說我們對session的操作最終都是由shiro完成的,而不是預設的servlet容器。
另外補充一點,shiro的session創建不是懶創建的。servlet容器中的session創建是第一次請求session(第一調用request.getSession())時才創建。shiro的session創建如下圖
此時,還沒登錄,但是subject、session已經創建了,只是subject的認證狀態為false,說明還沒進行登錄認證的。至於session創建過程已經保存到redis的流程需要大家自行去跟,或者閱讀我之前的博文
<