Shiro權限管理框架(二):Shiro結合Redis實現分布式環境下的Session共享


本篇是Shiro系列第二篇,使用Shiro基於Redis實現分布式環境下的Session共享。在講Session共享之前先說一下為什么要做Session共享。

首發地址:https://www.guitu18.com/post/2019/07/28/44.html


為什么要做Session共享

什么是Session

我們都知道HTTP協議(1.1)是無狀態的,所以服務器在需要識別用戶訪問的時候,就要做相應的記錄用於跟蹤用戶操作,這個實現機制就是Session。當一個用戶第一次訪問服務器的時候,服務器就會為用戶創建一個Session,每個Session都有一個唯一的SessionId(應用級別)用於標識用戶。

Session通常不會單獨出現,因為請求是無狀態的,那么我們必須讓用戶在下次請求時帶上服務器為其生成的Session的ID,通常的做法時使用Cookie實現(當然你要非要在請求參數中帶上SessionId那也不是不行)。請求返回時會向瀏覽器的Cookie中寫入SessionID,通常使用的鍵是JSESSIONID,這樣下次用戶再請求這台服務器時,服務器就能從Cookie中取出SessionId識別出該次請求的用戶是誰。

舉個栗子:

img

左邊紅框部分是Cookie列表,當前服務器是:localhost:28080。右邊紅框部分從左到右依次是Cookie的鍵、值、主機、路徑和過期時間。路徑為/時表示全站有效,最后一個過期時間未設置的話是默認值為Session,表示瀏覽器關閉時該Cookie失效。我們也可以為Cookie指定過期時間,以做到會話保持。

什么是Session共享

通過Session和Cookie,我們使得無狀態的HTTP協議間接的變成了有狀態的了,可以實現保持登錄,存儲用戶信息,購物車等等功能。但是隨着服務訪問人數的增多,單台服務器已經不足以應付所有的請求了,必須部署集群環境。但是隨着集群環境的出現,追蹤用戶狀態的問題又開始出現問題,之前用戶在A服務器登錄,A服務器保存了用戶信息,但是下一次請求發送到B服務器去了,這時候B服務器是不知道用戶在A服務器登錄的事情的,它雖然也能拿到用戶請求Cookie中的SessionId,但是在B服務根據這個SessionId找不到對應的Session,B服務器就會認為用戶沒有登錄,需要用戶重新登錄,這對用戶來說是沒辦法接受的。

這時候常見的有兩種方式解決這個問題,第一種是讓這個用戶所有的請求都發送到A服務器,比如根據IP地址做一些列算法將所有用戶分配到不同的服務器上去,讓每個用戶只訪問其中的一台服務器。這種做法可行,但是后續也會產生其它問題,更好的做法是第二種,將所有的服務器上的Session都做成共享的,A服務能拿到B服務器上的所有Session,同理B服務器也能獲取A服務器所有的Session,這樣上面的問題就不存在了。


Shiro結合Redis實現Session共享

上一篇已經通過Shiro實現了用戶登錄和權限管理,Shiro的登錄也是基於Session的,默認情況下Session是保存在內存中。既然要做Session共享,那么肯定是將Session抽取出來,放到一個多個服務器都能訪問到的地方。

在集群環境下,我們僅僅需要繼承AbstractSessionDAO,實現一下Session的增刪改查等幾個方法就可以很方便的實現Session共享,Shiro已經將完整的流程都做好了。這里涉及到的設計模式是模板方法模式,我們僅需要參與部分業務就可以完善整個流程了,當然我們不參與這部分流程的話,Shiro也有默認的實現方式,那就是將Session管理在當前應用的內存中。

具體的Session管理(共享)怎么實現由我們自己決定,可以存放在數據庫,也可以通過網絡傳輸,甚至可以通過IO流寫入文件都行,但就性能來講,我們一般都將Session放入Redis中。Redis大法好!YES~

自定義RedisSessionDAO

理解了原理之后就很容易辦事了,繼承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

注入RedisSessionDao

上面只是我們自己實現的管理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類。

測試

將這套代碼用不同的端口跑兩套服務(理論上跑多少套都可以只要你的配置夠用),訪問兩台服務器獲取用戶信息的接口,未登錄狀態毫無疑問都會跳到登錄頁去:

img

在任意一台服務器上調用登錄接口登錄:

img

登錄成功后再次分別訪問兩台服務器獲取用戶信息的接口:

img

如此,分布式環境Session共享完美實現。最后繼續放上項目代碼,代碼還是很早之前的,部分代碼為了配合此篇筆記經過修改整理后上傳。

Gitee:https://gitee.com/guitu18/ShiroDemo

GitHub:https://github.com/guitu18/ShiroDemo


本篇結束,簡直不要太簡單是不是,其實這主要是因為大部分工作Shiro都幫我們做了,細節的東西都被Shiro隱藏起來,我們僅僅需要添加一些簡單的配置就可以實現強大的功能,這就是框架的好處。

但是作為一個程序員,僅僅調用一個方法或者添加一個注解就實現了一套很強大的功能,而我們卻看不到一個if判斷和for循環的時候心里應該是非常不踏實的。我們不僅要學會使用框架,更要去深入理解框架,至少要知道為什么我們就加了一個注解框架就能幫我們實現一大堆功能,只有這樣才能讓我們感到腳踏實地。下一篇,深入Shiro源碼看看,可能需要醞釀一下想想筆記怎么寫。



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM