本篇是Shiro系列第二篇,使用Shiro基於Redis實現分佈式環境下的Session共享。在講Session共享以前先說一下爲何要作Session共享。html
首發地址:https://www.guitu18.com/post/2019/07/28/44.htmljava
咱們都知道HTTP協議(1.1)是無狀態的,因此服務器在須要識別用戶訪問的時候,就要作相應的記錄用於跟蹤用戶操做,這個實現機制就是Session。當一個用戶第一次訪問服務器的時候,服務器就會爲用戶建立一個Session,每一個Session都有一個惟一的SessionId(應用級別)用於標識用戶。git
Session一般不會單獨出現,由於請求是無狀態的,那麼咱們必須讓用戶在下次請求時帶上服務器爲其生成的Session的ID,一般的作法時使用Cookie實現(固然你要非要在請求參數中帶上SessionId那也不是不行)。請求返回時會向瀏覽器的Cookie中寫入SessionID,一般使用的鍵是JSESSIONID
,這樣下次用戶再請求這臺服務器時,服務器就能從Cookie中取出SessionId識別出該次請求的用戶是誰。程序員
舉個栗子:github
左邊紅框部分是Cookie列表,當前服務器是:localhost:28080。右邊紅框部分從左到右依次是Cookie的鍵、值、主機、路徑和過時時間。路徑爲/
時表示全站有效,最後一個過時時間未設置的話是默認值爲Session,表示瀏覽器關閉時該Cookie失效。咱們也能夠爲Cookie指定過時時間,以作到會話保持。redis
經過Session和Cookie,咱們使得無狀態的HTTP協議間接的變成了有狀態的了,能夠實現保持登陸,存儲用戶信息,購物車等等功能。可是隨着服務訪問人數的增多,單臺服務器已經不足以應付全部的請求了,必須部署集羣環境。可是隨着集羣環境的出現,追蹤用戶狀態的問題又開始出現問題,以前用戶在A服務器登陸,A服務器保存了用戶信息,可是下一次請求發送到B服務器去了,這時候B服務器是不知道用戶在A服務器登陸的事情的,它雖然也能拿到用戶請求Cookie中的SessionId,可是在B服務根據這個SessionId找不到對應的Session,B服務器就會認爲用戶沒有登陸,須要用戶從新登陸,這對用戶來講是沒辦法接受的。算法
這時候常見的有兩種方式解決這個問題,第一種是讓這個用戶全部的請求都發送到A服務器,好比根據IP地址作一些列算法將全部用戶分配到不一樣的服務器上去,讓每一個用戶只訪問其中的一臺服務器。這種作法可行,可是後續也會產生其它問題,更好的作法是第二種,將全部的服務器上的Session都作成共享的,A服務能拿到B服務器上的全部Session,同理B服務器也能獲取A服務器全部的Session,這樣上面的問題就不存在了。spring
上一篇已經經過Shiro實現了用戶登陸和權限管理,Shiro的登陸也是基於Session的,默認狀況下Session是保存在內存中。既然要作Session共享,那麼確定是將Session抽取出來,放到一個多個服務器都能訪問到的地方。數據庫
在集羣環境下,咱們僅僅須要繼承AbstractSessionDAO,實現一下Session的增刪改查等幾個方法就能夠很方便的實現Session共享,Shiro已經將完整的流程都作好了。這裏涉及到的設計模式是模板方法模式,咱們僅須要參與部分業務就能夠完善整個流程了,固然咱們不參與這部分流程的話,Shiro也有默認的實現方式,那就是將Session管理在當前應用的內存中。設計模式
具體的Session管理(共享)怎麼實現由咱們本身決定,能夠存放在數據庫,也能夠經過網絡傳輸,甚至能夠經過IO流寫入文件都行,但就性能來說,咱們通常都將Session放入Redis中。Redis大法好!YES~
理解了原理以後就很容易辦事了,繼承AbstractSessionDAO後實現Session增刪改查的幾個方法,而後再分佈式系統中全部的項目再須要存儲或獲取Session時都會走Redis操做,這樣就作到了集羣環境的Session共享了。代碼很是簡單:
@Component public class RedisSessionDao extends AbstractSessionDAO { @Value("${session.redis.expireTime}") private long expireTime; @Autowired private RedisTemplate redisTemplate; @Override protected Serializable doCreate(Session session) { Serializable sessionId = this.generateSessionId(session); this.assignSessionId(session, sessionId); redisTemplate.opsForValue().set(session.getId(), session, expireTime, TimeUnit.SECONDS); return sessionId; } @Override protected Session doReadSession(Serializable sessionId) { return sessionId == null ? null : (Session) redisTemplate.opsForValue().get(sessionId); } @Override public void update(Session session) throws UnknownSessionException { if (session != null && session.getId() != null) { session.setTimeout(expireTime * 1000); redisTemplate.opsForValue().set(session.getId(), session, expireTime, TimeUnit.SECONDS); } } @Override public void delete(Session session) { if (session != null && session.getId() != null) { redisTemplate.opsForValue().getOperations().delete(session.getId()); } } @Override public Collection<Session> getActiveSessions() { return redisTemplate.keys("*"); } }
配置文件中添加上面用到的配置
###redis鏈接配置 spring.redis.host=localhost spring.redis.port=6379 spring.redis.password=foobared ### Session過時時間(秒) session.redis.expireTime=3600
上面只是咱們本身實現的管理Session的方式,如今須要將其注入SessionManager中,並設置過時時間等相關參數。
@Bean public DefaultWebSessionManager defaultWebSessionManager(RedisSessionDao redisSessionDao) { DefaultWebSessionManager sessionManager = new DefaultWebSessionManager(); sessionManager.setGlobalSessionTimeout(expireTime * 1000); sessionManager.setDeleteInvalidSessions(true); sessionManager.setSessionDAO(redisSessionDao); sessionManager.setSessionValidationSchedulerEnabled(true); sessionManager.setDeleteInvalidSessions(true); /** * 修改Cookie中的SessionId的key,默認爲JSESSIONID,自定義名稱 */ sessionManager.setSessionIdCookie(new SimpleCookie("JSESSIONID")); return sessionManager; }
再將SessionManager注入Shiro的安全管理器SecurityManager中,前面說過,咱們圍繞安全相關的全部操做,都須要與SecurityManager打交道,這位纔是Shiro中真正的老大哥。
@Bean public SecurityManager securityManager(UserAuthorizingRealm userRealm, RedisSessionDao redisSessionDao) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(userRealm); // 取消Cookie中的RememberMe參數 securityManager.setRememberMeManager(null); securityManager.setSessionManager(defaultWebSessionManager(redisSessionDao)); return securityManager; }
OK,至此基於Redis實現的Session共享就完成了,是否是簡單得難以想象。
注意:基於網絡傳輸的對象請實現Serializable序列化接口,好比User類。
將這套代碼用不一樣的端口跑兩套服務(理論上跑多少套均可以只要你的配置夠用),訪問兩臺服務器獲取用戶信息的接口,未登陸狀態毫無疑問都會跳到登陸頁去:
在任意一臺服務器上調用登陸接口登陸:
登陸成功後再次分別訪問兩臺服務器獲取用戶信息的接口:
如此,分佈式環境Session共享完美實現。最後繼續放上項目代碼,代碼仍是很早以前的,部分代碼爲了配合此篇筆記通過修改整理後上傳。
Gitee:https://gitee.com/guitu18/ShiroDemo
GitHub:https://github.com/guitu18/ShiroDemo
本篇結束,簡直不要太簡單是否是,其實這主要是由於大部分工做Shiro都幫咱們作了,細節的東西都被Shiro隱藏起來,咱們僅僅須要添加一些簡單的配置就能夠實現強大的功能,這就是框架的好處。
可是做爲一個程序員,僅僅調用一個方法或者添加一個註解就實現了一套很強大的功能,而咱們卻看不到一個if判斷和for循環的時候內心應該是很是不踏實的。咱們不只要學會使用框架,更要去深刻理解框架,至少要知道爲何咱們就加了一個註解框架就能幫咱們實現一大堆功能,只有這樣才能讓咱們感到腳踏實地。下一篇,深刻Shiro源碼看看,可能須要醞釀一下想一想筆記怎麼寫。