開心一刻html
老師對小明說:"乳就是小的意思,好比乳豬就是小豬,乳名就是小名,請你用乳字造個句"
小明:"我家很窮,只能住在40平米的乳房"
老師:"..., 這個不行,換一個"
小明:"我天天上學都要跳過我家門口的一條乳溝"
老師:"......, 這個也不行,再換一個"
小明:"老師,我想不出來了,把個人乳頭都想破了!"java
路漫漫其修遠兮,吾將上下而求索!git
github:https://github.com/youzhibinggithub
碼雲(gitee):https://gitee.com/youzhibingredis
shiro的session建立與session的查詢、更新、過時、刪除中,shiro對session的操做基本都講到了,但還缺一個session共享沒有講解;session共享的原理其實在自定義session管理一文已經講過了,本文不講原理,只看看shiro的session共享的實現。spring
若是是單機應用,那麼談不上session共享,session放哪都無所謂,不在意放到默認的servlet容器中,仍是抽出來放到單獨的地方;數據庫
也就是說session共享是針對集羣(或分佈式、或分佈式集羣)的;若是不作session共享,仍然採用默認的方式(session存放到默認的servlet容器),當咱們的應用是以集羣的方式發佈的時候,同個用戶的請求會被分發到不一樣的集羣節點(分發依賴具體的負載均衡規則),那麼每一個處理同個用戶請求的節點都會從新生成該用戶的session,這些session之間是毫無關聯的。那麼同個用戶的請求會被當成多個不一樣用戶的請求,這確定是不行的。apache
實現方式其實有不少,甚至能夠不作session共享,具體有哪些,你們自行去查資料。本文提供一種方式:redis實現session共享,就是將session從servlet容器抽出來,放到redis中存儲,全部集羣節點都從redis中對session進行操做。後端
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(); }
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); }
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(); } } }
是應用層與持久化層之間的緩存層,不用頻繁請求持久化層以提高效率。重寫了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); } } }
將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. } }
設置了默認的緩存管理器(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共享實際上是比較簡單的,重寫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; } }
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; } }
自定義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; } }
最後將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); } } }
上圖中,我能夠看到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的流程須要你們自行去跟,或者閱讀我以前的博文
一、當以集羣方式對外提供服務的時候,不作session共享也是能夠的
能夠經過ip_hash的機制將同個ip的請求定向到同一臺後端,這樣保證用戶的請求始終是同一臺服務處理,與單機應用基本一致了;但這有不少方面的缺陷(具體就不詳說了),不推薦使用。
二、servlet容器之間作session同步也是能夠實現session共享的
一個servlet容器生成session,其餘節點的servlet容器今後servlet容器進行session同步,以達到session信息一致。這個也不推薦,某個時間點會有session不一致的問題,畢竟同步過程受到各方面的影響,不能保證session實時一致。
三、session共享實現的原理其實都是同樣的,都是filter + HttpServletRequestWrapper,只是實現細節會有所區別;有興趣的能夠看下spring-session的實現細節。
四、若是咱們採用的spring集成shiro,其實能夠將緩存管理器交由spring管理,至關於由spring統一管理緩存。
五、shiro的CacheManager不僅是管理session緩存,還管理着身份認證緩存、受權緩存,shiro的緩存都是CacheManager管理。可是身份認證緩存默認是關閉的,我的也不推薦開啓。
六、shiro的session建立時機是在登陸認證以前,而不是第一次調用getSession()時。
《跟我學shiro》