實戰,通過複寫shiro的SessionDAO來實現將session保存到redis集群中

2021-01-21 Java筆記蝦

作者:zifangsky

https://www.zifangsky.cn/889.html

如題所示,在分布式系統架構中需要解決的一個很重要的問題就是——如何保證各個應用節點之間的Session共享。現在通用的做法就是使用redis、memcached等組件獨立存儲所有應用節點的Session,以達到各個應用節點之間的Session共享的目的

在Java Web項目中實現session共享的一個很好的解決方案是:Spring Session+Spring Data Redis。關於這方面的內容可以參考我之前寫的這篇文章:https://www.zifangsky.cn/862.html

但是,如果在項目中使用到了shiro框架,並且不想使用Spring Session的話,那麼我們可以通過複寫shiro的SessionDAO同樣達到將shiro管理的session保存到redis集群的目的,以此解決分布式系統架構中的session共享問題

下面,我將詳細說明具體該如何來實現:

(1)配置Spring Data Redis環境:

關於Spring Data Redis環境的配置可以參考這篇文章:

https://www.zifangsky.cn/861.html

在這裡,我測試使用的是redis集群模式,當然使用redis單節點也可以

(2)複寫shiro的SessionDAO:

import java.io.Serializable;
import java.util.concurrent.TimeUnit;

import javax.annotation.Resource;

import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO;
import org.springframework.data.redis.core.BoundValueOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Repository;

@Repository("customShiroSessionDao")
public class CustomShiroSessionDao extends EnterpriseCacheSessionDAO {

    @Resource(name="redisTemplate")
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 創建session,保存到redis集群中
     */
    @Override
    protected Serializable doCreate(Session session) {
        Serializable sessionId = super.doCreate(session);
        System.out.println("sessionId: " + sessionId);

        BoundValueOperations<String, Object> sessionValueOperations = redisTemplate.boundValueOps("shiro_session_" + sessionId.toString());
        sessionValueOperations.set(session);
        sessionValueOperations.expire(30, TimeUnit.MINUTES);

        return sessionId;
    }

    /**
     * 獲取session
     * @param sessionId
     * @return
     */
    @Override
    protected Session doReadSession(Serializable sessionId) {
        Session session = super.doReadSession(sessionId);

        if(session == null){
            BoundValueOperations<String, Object> sessionValueOperations = redisTemplate.boundValueOps("shiro_session_" + sessionId.toString());
            session = (Session) sessionValueOperations.get();
        }

        return session;
    }

    /**
     * 更新session
     * @param session
     */
    @Override
    protected void doUpdate(Session session) {
        super.doUpdate(session);

        BoundValueOperations<String, Object> sessionValueOperations = redisTemplate.boundValueOps("shiro_session_" + session.getId().toString());
        sessionValueOperations.set(session);
        sessionValueOperations.expire(30, TimeUnit.MINUTES);
    }

    /**
     * 刪除失效session
     */
    @Override
    protected void doDelete(Session session) {
        redisTemplate.delete("shiro_session_" + session.getId().toString());
        super.doDelete(session);
    }

}

具體含義可以參考注釋,這裡就不多做解釋了

(3)在shiro的配置文件中添加sessionManager:

    <!-- 使用redis存儲管理session -->
    <bean id="sessionManager"  
        class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager"> 
        <!-- 刪除失效session -->
        <property name="sessionValidationSchedulerEnabled" value="true" />  
        <!-- session失效時間(毫秒) --> 
        <property name="globalSessionTimeout" value="1800000" />
        <property name="sessionDAO" ref="customShiroSessionDao" />  
    </bean>

然後在securityManager中使用該sessionManager:

    <!-- Shiro安全管理器 -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="realm" ref="customRealm" />
        <property name="sessionManager" ref="sessionManager" />
        <property name="cacheManager" ref="cacheManager" />
    </bean>

註:完整的shiro的配置文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jee="http://www.springframework.org/schema/jee"
    xsi:schemaLocation="http://www.springframework.org/schema/beans 
            http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
            http://www.springframework.org/schema/jee 
            http://www.springframework.org/schema/jee/spring-jee-4.0.xsd
            http://www.springframework.org/schema/aop 
            http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
            http://www.springframework.org/schema/context 
            http://www.springframework.org/schema/context/spring-context-4.0.xsd
            http://www.springframework.org/schema/tx 
            http://www.springframework.org/schema/tx/spring-tx-4.0.xsd"
    xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx"
    xmlns:aop="http://www.springframework.org/schema/aop">
    <description>Shiro 配置</description>

    <context:component-scan base-package="cn.zifangsky.manager.impl" />
    <context:component-scan base-package="cn.zifangsky.shiro" />

    <bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
        <property name="cacheManager" ref="cacheManagerFactory" />
        <property name="cacheManagerConfigFile" value="classpath:ehcache.xml" />
    </bean>

    <!-- 使用redis存儲管理session -->
    <bean id="sessionManager"  
        class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager"> 
        <!-- 刪除失效session -->
        <property name="sessionValidationSchedulerEnabled" value="true" />  
        <!-- session失效時間(毫秒) --> 
        <property name="globalSessionTimeout" value="1800000" />
        <property name="sessionDAO" ref="customShiroSessionDao" />  
    </bean>

    <!-- Shiro安全管理器 -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="realm" ref="customRealm" />
        <property name="sessionManager" ref="sessionManager" />
        <property name="cacheManager" ref="cacheManager" />
    </bean>

    <!-- 自定義Realm -->
    <bean id="customRealm" class="cn.zifangsky.shiro.CustomRealm" />

    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <property name="securityManager" ref="securityManager" />
        <property name="loginUrl" value="/user/user/login.html" />
        <!-- <property name="successUrl" value="/login/loginSuccessFull" /> -->
        <property name="unauthorizedUrl" value="/error/403.jsp" />
        <!-- <property name="filters">
            <map>
                <entry key="auth" value-ref="userFilter"/>
            </map>  
        </property> -->
        <property name="filterChainDefinitions">
            <value>
                /error/* = anon
                /scripts/* = anon
                /user/user/check.html = anon
                /user/user/verify.html = anon
                /user/user/checkVerifyCode.html = anon
                /user/user/logout.html = logout
                /**/*.htm* = authc
        /**/*.json* = authc
            </value>
        </property>
    </bean>

    <!-- <bean id="cleanFilter" class="cn.zifangsky.shiro.CleanFilter"/> -->
    <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>

</beans>

(4)測試:i)本地簡單測試:

將同一項目部署到本地兩個不同埠的Tomcat中,然後在其中一個Tomcat登錄,接著直接在另一個Tomcat上訪問登錄後的URL,觀察是否可以直接訪問還是跳轉到登錄頁面

ii)完整分布式環境測試:

首先將測試項目部署到兩個伺服器上的Tomcat中,我這裡的訪問路徑分別是:

192.168.1.30:9080
192.168.1.31:9080

接著配置nginx訪問(PS:nginx所在IP是:192.168.1.31):

server {
    server_name  localhost;
    listen 7888;

    location /WebSocketDemo
        {       
              proxy_redirect off;
              proxy_set_header        Host $host:7888;
              proxy_set_header        X-Real-IP $remote_addr;
              proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;
              proxy_pass http://demo/WebSocketDemo;
              proxy_set_header   Cookie $http_cookie;
        }

    #access_log  on;
    limit_conn perip 1000;  #同一ip並發數為50,超過會返回503
    access_log logs/access_test.log zifangsky_log;
 }

對應的upstream是:

upstream demo {
  server 192.168.1.30:9080;
  server 192.168.1.31:9080;
}

從上面可以看出,這裡設置的nginx的負載均衡策略是默認策略——輪詢

最後在瀏覽器中訪問:http://192.168.1.31:7888/WebSocketDemo/user/user/login.html

登錄之後,不斷刷新頁面並觀察nginx的日誌:

可以發現,經過nginx的反向代理之後,雖然每次訪問的實際服務地址都不一樣,但是我們的登錄狀態並沒有丟失——並沒有跳轉到登錄頁面。這就證明了這個測試小集群的session的確實現了共享,也就是說session保存到了redis集群中,並不受具體的業務伺服器的更改而發生改變

當然,我們也可以登錄到redis集群,查詢一下該sessionId對應的session值:

參考:

http://www.cnblogs.com/sunshine-2015/p/5686750.html

Java面試題專欄



歡迎長按下圖關注公眾號後端技術精選

相關焦點

  • Shiro整合redis
    在實際落地過程中,會發現如果出現集群部署服務,那麼單個服務的shiro無法認證另一個服務下面的session.
  • Shiro 權限校驗分析
    使用 Shiro 的易於理解的 API,您可以快速、輕鬆地獲得任何應用程式,從最小的行動應用程式到最大的網絡和企業應用程式,特別是今天對權限校驗和管理特別嚴格,大家有必要對shiro 有一個基本的認識和學習。
  • Shiro入門這篇就夠了【Shiro的基礎知識、回顧URL攔截】
    2.1如何實現粗粒度權限管理?粗粒度權限管理比較容易將權限管理的代碼抽取出來在系統架構級別統一處理。比如:通過springmvc的攔截器實現授權。2.1.1基於URL攔截 基於url攔截的方式實現在實際開發中比較常用的一種方式。對於web系統,通過filter過慮器實現url攔截,也可以springmvc的攔截器實現基於url的攔截。2.2.2使用權限管理框架實現 對於粗粒度權限管理,建議使用優秀權限管理框架來實現,節省開發成功,提高開發效率。
  • 實戰|單點登錄系統原理與實現(全套流程圖+源碼)
    伺服器在內存中保存會話對象,瀏覽器怎麼保存會話id呢?你可能會想到兩種方式。1、請求參數 2、cookie將會話id作為每一個請求的參數,伺服器接收請求自然能解析參數獲得會話id,並藉此判斷是否來自同一會話,很明顯,這種方式不靠譜。
  • SpringSecurity + JWT前後端分離架構實現
    比如:集群應用,同一個應用部署甲、乙、丙三個主機上,實現負載均衡應用,其中一個掛掉了其他的還能負載工作。要知道session是保存在伺服器內存裡面的,三個主機一定是不同的內存。那麼你登錄的時候訪問甲,而獲取接口數據的時候訪問乙,就無法保證session的唯一性和共享性。當然以上的這些情況我們都有方案(如redis共享session等),可以繼續使用session來保存狀態。
  • localStorage、sessionStorage有什麼區別?
    對比cookie:cookie會與伺服器通信;storage只存在客服端,不參與伺服器通信;同樣受同源策略影響,只有在域名一致的情況下才能查看到對應的數據;navigator.cookieEnabled檢測是否啟用了cookie,也就說cookie
  • ...Session of Canton Fair scheduled online from October 15th...
    第128屆廣交會將於10月15-24日 在網上舉辦(The 128th Session of Canton Fair scheduled online from October 15th to 24th)2020年9月10日,中華人民共和國商務部召開例行新聞發布會。商務部新聞發言人高峰在發布會上通報了第128屆廣交會有關情況。
  • 歡迎參加第127屆廣交會(Welcome to 127th session of Canton Fair)
    Welcome to 127th session of Canton Fair The 127th session of the China Import and Export Fair, also known as the Canton Fair, will be held online from June 15 to 24
  • 如何實現redis主從複製?
    主從複製,主要優勢在於實現了數據備份(主機和從機數據同步一致)、讀寫分離(主機主要負責寫入數據,從機讀數據,在讀大於寫的項目中提高了性能)。最後也為後續集成哨兵機制和集群的實現提供了依據。/redis-cli -a "123456"在1兩臺從機上分別連接redis後。執行get k1可以看到結果為主機上寫入的name1的值,表示主從複製配置正確。或者通過info replication 指定來查看主從配置信息主節點中 cli中執行 info replication.
  • 程式設計師必備|面試中常被問到的redis持久化的問題
    「那不會,redis支持持久化,通過rdb或者aof這兩種方式可以將數據持久化到硬碟上」少年暗喜,題目太簡單「持久化?通俗點解釋下」「就是把內存的數據刷到磁碟中的文件裡面」「嗯~,那你先來說說rdb是什麼?」
  • Redis是如何實現點讚、取消點讚的?
    一個牛逼的多級緩存實現方案基於redis分布式鎖實現「秒殺」(含代碼)百億數據量下,掌握這些Redis技巧你就能Hold全場9個提升逼格的redis命令get個新技能:redis實現自動補全利用 Redis 實現「附近的人」功能!
  • 30 分鐘學會如何使用 Shiro
    Shiro本身已經實現了所有的細節,用戶可以完全把它當做一個黑盒來使用。SecurityUtils對象,本質上就是一個工廠類似Spring中的ApplicationContext。Subject是初學者比較難於理解的對象,很多人以為它可以等同於User,其實不然。Subject中文翻譯:項目,而正確的理解也恰恰如此。它是你目前所設計的需要通過Shiro保護的項目的一個抽象概念。
  • Pika 3.4.0 發布,從單機到集群
    pika 是 360 公司發布一個可持久化的大容量 redis 存儲服務,兼容 string、hash、list、zset、set 的絕大部分接口,解決 redis 由於存儲數據量巨大而導致內存不夠用的容量瓶頸。360 推出原生分布式 pika 集群,發布 pika 3.4.0,pika 原生集群不再需要額外部署 codis-proxy 模塊。
  • Spring Boot與Shiro整合實現用戶認證
    -- shiro與spring整合依賴 --><dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-spring</artifactId><version
  • 30分鐘學會如何使用Shiro,看這篇文章就夠了!
    因此Realm是整個框架中為數不多的必須由設計者自行實現的模塊,當然Shiro提供了多種實現的途徑,本文只介紹最常見也最重要的一種實現方式——資料庫查詢。如此一來,當設計人員對項目中的某一個url路徑設置了只允許某個角色或具有某種權限才可以訪問的控制約束的時候,Shiro就可以通過以上兩個對象來判斷。說到這裡,大家可能還比較困惑。先不要著急,繼續往後看就自然會明白了。二、實現Realm如何實現Realm是本文的重頭戲,也是比較費事的部分。
  • 知乎技術分享:從單機到2000萬QPS並發的Redis高性能緩存實踐之路
    而在集群(Cluster)實例類型中,當實例需要的容量超過 20G 或要求的吞吐量超過 20萬請求每秒時,我們會使用集群(Cluster)實例來承擔流量。集群是通過中間件(客戶端或中間代理等)將流量分散到多個 Redis 實例上的解決方案。知乎的 Redis 集群方案經歷了兩個階段:客戶端分片(2015年前使用的方案)與 Twemproxy 代理(2015年至今使用的方案)。
  • Shiro權限管理框架(一):Shiro的基本使用
    這裡的對上面用到的兩個過濾器做一下簡單說明,篇幅控制其他過濾器請參閱相關文檔:* authc:配置的url都必須認證通過才可以訪問,它是Shiro內置的一個過濾器* 對應的實現類 @see org.apache.shiro.web.filter.authc.FormAuthenticationFilter* anon:也是Shiro內置的,它對應的過濾器裡面是空的
  • redis電商應用場景專題及常見問題 - CSDN
    各種計數,商品維度計數和用戶維度計數說起電商,肯定離不開商品,而附帶商品有各種計數(喜歡數,評論數,鑑定數,瀏覽數,etc),Redis的命令都是原子性的,你可以輕鬆地利用INCR,DECR等命令來計數。商品維度計數(喜歡數,評論數,鑑定數,瀏覽數,etc)採用Redis 的類型: Hash.