Springboot2集成Shiro框架(八)使用redis管理session

1、爲什麼要使用redis管理session

很多公司會使用分佈式來部署項目,使用反向代理功能來分發請求,但是如果依舊採用session的方式來對登錄信息等進行管理,就會限制反向代理服務器的配置(根據ip,hash服務器地址),爲了解決這個問題,使用獨立的session服務器可以完美解決這個問題,如下圖所示 在這裏插入圖片描述
每個服務器都會從相同的redis裏讀取用戶登錄信息,如果用戶登錄認證是在服務器1上面完成的,服務器1會把相應的會話數據保存到redis中,當用戶訪問其他鏈接時,請求被分配到了服務器3,由於服務器是無狀態的,服務器3會到redis中查找該用戶狀態,查詢到已登錄後,處理數據,成功返回!

2、session的工作原理

在使用之前,我們先了解一下session的原理,session是一段會話,由於http是無狀態的,爲了保存會話狀態,纔有了session。session在服務器端是存放在內存中的,如果登錄用戶多會佔用很多內存資源,而session在客戶端時保存在cookie中的一段文本JSESSIONID,而這個id正式鏈接客戶端和服務器內存的憑證。

下圖模擬用戶登錄後修改密碼流程

在這裏插入圖片描述

3、session的生命週期

  1. session在用戶首次訪問系統時創建(訪問靜態資源不會創建)
  2. 生命週期可以由服務器手動配置,tomcat默認爲30分鐘
  3. 由於session對應的信息保存在cookie中,關閉瀏覽器並不會使session失效(除非改寫session)
  4. 服務器端可以使用 invalidate() 手動失效session

4、 shiro的session

4.1、官方說明

默認 SecurityManager 實現默認使用 DefaultSessionManager 開箱即用。該 DefaultSessionManager 實現提供了應用程序所需的所有企業級會話管理功能,例如會話驗證,孤立清理等。可在任何應用程序中使用。像所管理的所有其他組件一樣 SecurityManagerSessionManager 可以通過 Shiro 的所有默認 SecurityManager 實現( getSessionManager()/ setSessionManager() )上的JavaBeans風格的getter / setter方法來獲取或設置它們。每當創建或更新會話時,其數據都需要保留到存儲位置,以便應用程序稍後可以訪問它。類似地,當會話無效且已被更長時間使用時,需要將其從存儲中刪除,因此會話數據存儲空間不會耗盡。這些SessionManager實現將這些創建/讀取/更新/刪除(CRUD)操作委託給一個內部組件,該組件SessionDAO反映了數據訪問對象(DAO)設計模式。

SessionDAO的功能是您可以實現此接口以與所需的任何數據存儲進行通信。這意味着您的會話數據可以駐留在內存中,文件系統上,關係數據庫或NoSQL數據存儲區中,或您需要的任何其他位置。您可以控制持久性行爲。

您可以將任何SessionDAO實現配置爲默認SessionManager實例上的屬性。
我們可以根據下圖查看到shiro的session具體結構。在這裏插入圖片描述

4.2、shiro默認session的實現

  1. 在之前的配置中,我們知道了shiro的核心配置項是 SecurityManager 類,發現這個類是提供 setSessionManager() 方法讓我們自定義session管理器的。
	/**
	 * 注入 securityManager
	 */
	@Bean
	public SecurityManager securityManager() {
		DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
		securityManager.setRealm(myShiroRealm());
		securityManager.setRememberMeManager(rememberMeManager());
		securityManager.setCacheManager(myEhCacheManager());// 將緩存管理交給ehCache
		securityManager.setSessionManager(****);//設置session管理器
		return securityManager;
	}
  1. 所有的配置都會注入我們使用的 DefaultWebSecurityManager 中,查看該類發現在構建實例的時候,系統默認設置了 ServletContainerSessionManager() 爲session管理器
	class DefaultWebSecurityManager
	......
	//無參構造
    public DefaultWebSecurityManager() {
        super();
        ((DefaultSubjectDAO) this.subjectDAO).setSessionStorageEvaluator(new DefaultWebSessionStorageEvaluator());
        this.sessionMode = HTTP_SESSION_MODE;
        setSubjectFactory(new DefaultWebSubjectFactory());
        setRememberMeManager(new CookieRememberMeManager());
        setSessionManager(new ServletContainerSessionManager());
    }
  1. 繼續查看 ServletContainerSessionManager 類,在new的時候,會依次執行父類的構造,在 SessionsSecurityManager 中,我們發現瞭如下代碼,設置sessionManager爲 DefaultSessionManager
    在這裏插入圖片描述
    /**
     * Default no-arg constructor, internally creates a suitable default {@link SessionManager SessionManager} delegate
     * instance.
     */
    public SessionsSecurityManager() {
        super();
        this.sessionManager = new DefaultSessionManager();
        applyCacheManagerToSessionManager();
    }
  1. 而在 DefaultSessionManager 的構造中,我們找到了默認提供的sessionDao的實現 MemorySessionDAO
class DefaultSessionManager
	......
    public DefaultSessionManager() {
        this.deleteInvalidSessions = true;
        this.sessionFactory = new SimpleSessionFactory();
        this.sessionDAO = new MemorySessionDAO();
    }
  1. MemorySessionDAO 關係圖,可以看到,他繼承了AbstractSessionDAO抽象類,而頂級接口則是SessionDao層,另外以外的發現了shiro爲我們提供的一個企業級session緩存的實現類 EnterpriseCacheSessionDAO
    在這裏插入圖片描述
    6.查看了一下 EnterpriseCacheSessionDAO 類發現,這個緩存是利用了shiro提供的 Cache 接口 下的 MapCache 實現類實現的,ConcurrentHashMap作爲value來保證線程安全!
public class EnterpriseCacheSessionDAO extends CachingSessionDAO {

    public EnterpriseCacheSessionDAO() {
        setCacheManager(new AbstractCacheManager() {
            @Override
            protected Cache<Serializable, Session> createCache(String name) throws CacheException {
                return new MapCache<Serializable, Session>(name, new ConcurrentHashMap<Serializable, Session>());
            }
        });
    }
......
  1. shiro 提供的Cache接口另外實現類
    在這裏插入圖片描述
    8.回到 MemorySessionDAO 我們查看這個類的相關代碼
public class MemorySessionDAO extends AbstractSessionDAO {

    private static final Logger log = LoggerFactory.getLogger(MemorySessionDAO.class);
	//保存session的容器
    private ConcurrentMap<Serializable, Session> sessions;
//構造
    public MemorySessionDAO() {
        this.sessions = new ConcurrentHashMap<Serializable, Session>();
    }
	//創建session
    protected Serializable doCreate(Session session) {
    //sessionid生成器
        Serializable sessionId = generateSessionId(session);
        assignSessionId(session, sessionId);
        storeSession(sessionId, session);
        return sessionId;
    }
//存儲會話
    protected Session storeSession(Serializable id, Session session) {
        if (id == null) {
            throw new NullPointerException("id argument cannot be null.");
        }
        //只有在key不存在或者key爲null的時候,value值纔會被覆蓋  jdk 1.8新特性
        return sessions.putIfAbsent(id, session);
        /**putIfAbsent 源碼展示
		    default V putIfAbsent(K key, V value) {
		        V v = get(key);
		        if (v == null) {
		            v = put(key, value);
		        }
	        	return v;
	    	}
		*/
    }
//根據sessionid獲取session對象
    protected Session doReadSession(Serializable sessionId) {
        return sessions.get(sessionId);
    }
//根據sessionid更新session對象
    public void update(Session session) throws UnknownSessionException {
        storeSession(session.getId(), session);
    }
//刪除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);
        }
    }
//獲取存活的session對象
    public Collection<Session> getActiveSessions() {
        Collection<Session> values = sessions.values();
        if (CollectionUtils.isEmpty(values)) {
            return Collections.emptySet();
        } else {
            return Collections.unmodifiableCollection(values);
        }
    }

}

可以看到,在默認情況下,session的操作全是有 MemorySessionDAO 類實現的,我們猜想,新增redis操作類,繼承 AbstractSessionDAO ,重寫其中的重要方法,是不是就可以實現redis管理session了?

5、使用redis管理session(配置)

redis的安裝就不介紹了,只講解如何在shiro中使用redis管理session!

5.1 、引入jar包

上面雖然已經發現如何替換session操作類來實現session的redis管理,但是已經有的輪子直接拿來即可,首先我們要引入redis的jar

<!-- shiro-redis -->
		<dependency>
			<groupId>org.crazycake</groupId> 
			<artifactId>shiro-redis</artifactId>
			<version>3.1.0</version>
		</dependency>

我在引入上面的包後會報錯,引入這個解決,未報錯不用引入,報錯原因未深究
err:Missing artifact com.sun:tools:jar:1.8.0

<!-- tools -->
		<dependency>
			<groupId>com.sun</groupId>
			<artifactId>tools</artifactId>
			<version>1.8.0</version>
			<scope>system</scope>
			<systemPath>${env.JAVA_HOME}/lib/tools.jar</systemPath>
			<optional>true</optional>
		</dependency>

5.2、新增redis配置

在boot配置文件中新增如下文件,我採用的是yml文件,其他格式需要自行轉換
application.yml

#redis
redis:
  #redis機器ip
  host: 127.0.0.1
  #redis端口
  port: 6379
  #redis密碼
  password: 123456
  #默認數據庫
  database: 10
  #redis超時時間(毫秒),如果不設置,取默認值2000
  timeout: 10000

5.3、ShiroConfig配置

  1. 新增讀取配置項屬性及redis實例和redisdao實例
    @Value("${redis.host}")
    private String host;

    @Value("${redis.port}")
    private int port;

    @Value("${redis.password}")
    private String password;

    @Value("${redis.database}")
    private int database;

    @Value("${redis.timeout}")
    private int timeout;


    @Bean
    public RedisManager redisManager() {
        RedisManager redisManager = new RedisManager();
        redisManager.setHost(host);// 主機地址
        redisManager.setPort(port);// 端口
        redisManager.setPassword(password);// 訪問密碼
        redisManager.setDatabase(database);// 默認數據庫
        redisManager.setTimeout(timeout);// 過期時間
        return redisManager;
    }

    /**
     * SessionDAO的作用是爲Session提供CRUD並進行持久化的一個shiro組件 MemorySessionDAO 直接在內存中進行會話維護
     * EnterpriseCacheSessionDAO
     * 提供了緩存功能的會話維護,默認情況下使用MapCache實現,內部使用ConcurrentHashMap保存緩存的會話。
     * 
     * @return
     */
    @Bean
    public RedisSessionDAO redisSessionDAO() {
        RedisSessionDAO redisSessionDao = new RedisSessionDAO();
        redisSessionDao.setKeyPrefix("shiro-session");//配置session前綴
        redisSessionDao.setSessionIdGenerator(sessionIdGenerator());
        redisSessionDao.setRedisManager(redisManager());
        // session在redis中的保存時間,最好大於session會話超時時間
        redisSessionDao.setExpire(timeout);
        return redisSessionDao;
    }
  1. 配置自定義sessionid生成器
    /**
     * 配置會話ID生成器
     * 
     * @return
     */
    @Bean
    public SessionIdGenerator sessionIdGenerator() {
        return new JavaUuidSessionIdGenerator();
    }
  1. 配置session監聽器
    /**
     * 配置session監聽
     * 
     * @return
     */
    @Bean
    public MySessionListener sessionListener() {
        MySessionListener sessionListener = new MySessionListener();
        return sessionListener;
    }

MySessionListener 類


import org.apache.shiro.session.Session;
import org.apache.shiro.session.SessionListener;
/**
 * 
 * @ClassName:  MySessionListener   
 * @Description 統計session數量
 * @version 
 * @author JH
 * @date 2019年9月2日 上午11:15:38
 */
public class MySessionListener implements SessionListener {

    private final AtomicInteger sessionCount = new AtomicInteger(0);

    /**
     * 登錄
     */
    @Override
    public void onStart(Session session) {
        sessionCount.incrementAndGet();
        System.out.println("登錄,有效session數量:"+sessionCount.get());
    }

    /**
     * 登出
     */
    @Override
    public void onStop(Session session) {
        sessionCount.decrementAndGet();
        System.out.println("登出,有效session數量:"+sessionCount.get());
    }

    /**
     * session過期
     */
    @Override
    public void onExpiration(Session session) {
        sessionCount.decrementAndGet();
        System.out.println("session過期,有效session數量:"+sessionCount.get());
    }

}
  1. 配置自定義session的cookie,替換JSESSIONID
    /**
     * 配置保存sessionId的cookie 注意:這裏的cookie 不是上面的記住我 cookie 記住我需要一個cookie session管理
     * 也需要自己的cookie 默認爲: JSESSIONID 問題: 與SERVLET容器名衝突,重新定義爲sid
     * 
     * @return
     */
    @Bean("sessionIdCookie")
    public SimpleCookie sessionIdCookie() {
        // 這個參數是cookie的名稱
        SimpleCookie simpleCookie = new SimpleCookie("REDIS-SESSION");
        // setcookie的httponly屬性如果設爲true的話,會增加對xss防護的安全係數。它有以下特點:

        // setcookie()的第七個參數
        // 設爲true後,只能通過http訪問,javascript無法訪問
        // 防止xss讀取cookie
        simpleCookie.setHttpOnly(true);
        simpleCookie.setPath("/");
        // maxAge=-1表示瀏覽器關閉時失效此Cookie
        simpleCookie.setMaxAge(-1);
        return simpleCookie;
    }
  1. 配置會話管理器
    /**
     * 配置會話管理器,設定會話超時及保存
     * 
     * @return
     */
    @Bean
    public SessionManager sessionManager() {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        Collection<SessionListener> listeners = new ArrayList<SessionListener>();
        // 配置監聽
        listeners.add(sessionListener());
        sessionManager.setSessionListeners(listeners);
        sessionManager.setSessionIdCookie(sessionIdCookie());
        sessionManager.setSessionDAO(redisSessionDAO());
        
        // 全局會話超時時間(單位毫秒),默認30分鐘 暫時設置爲10秒鐘 用來測試
        sessionManager.setGlobalSessionTimeout(1800000);//單位毫秒
        // 是否開啓刪除無效的session對象 默認爲true
        sessionManager.setDeleteInvalidSessions(true);
        // 是否開啓定時調度器進行檢測過期session 默認爲true
        sessionManager.setSessionValidationSchedulerEnabled(true);
        // 設置session失效的掃描時間, 清理用戶直接關閉瀏覽器造成的孤立會話 默認爲 1個小時
        // 設置該屬性 就不需要設置 ExecutorServiceSessionValidationScheduler
        // 底層也是默認自動調用ExecutorServiceSessionValidationScheduler
        sessionManager.setSessionValidationInterval(3600000);//單位毫秒
        // 取消url 後面的 JSESSIONID,設置爲false爲取消
        sessionManager.setSessionIdUrlRewritingEnabled(false);
        return sessionManager;

    }
  1. 將會話管理器交給 securityManager 管理
    /**
     * 注入 securityManager
     */
    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(myShiroRealm());
        securityManager.setRememberMeManager(rememberMeManager());
        securityManager.setCacheManager(myEhCacheManager());// 將緩存管理交給ehCache
        securityManager.setSessionManager(sessionManager());//將session管理交給reids
        return securityManager;
    }

至此,shiro的session交給redis管理已經配置完成了,很簡單啊!

6、驗證猜想

  • 6.1、在之前的源碼跟蹤中,我們猜想重寫一個類繼承 AbstractSessionDAO* ,替換 MemorySessionDAO ,在引入 shiro-redis jar包後,查看是用的redisSessionDAO關係圖,驗證猜想!
    在這裏插入圖片描述
  • 6.2、部分 RedisSessionDAO 源碼
	@Override
	protected Serializable doCreate(Session session) {
		if (session == null) {
			logger.error("session is null");
			throw new UnknownSessionException("session is null");
		}
		Serializable sessionId = this.generateSessionId(session);  
        this.assignSessionId(session, sessionId);
        this.saveSession(session);
		return sessionId;
	}

	@Override
	protected Session doReadSession(Serializable sessionId) {
		if (sessionId == null) {
			logger.warn("session id is null");
			return null;
		}
		Session s = getSessionFromThreadLocal(sessionId);

		if (s != null) {
			return s;
		}

		logger.debug("read session from redis");
		try {
			s = (Session) valueSerializer.deserialize(redisManager.get(keySerializer.serialize(getRedisSessionKey(sessionId))));
			setSessionToThreadLocal(sessionId, s);
		} catch (SerializationException e) {
			logger.error("read session error. settionId=" + sessionId);
		}
		return s;
	}

	private void setSessionToThreadLocal(Serializable sessionId, Session s) {
		Map<Serializable, SessionInMemory> sessionMap = (Map<Serializable, SessionInMemory>) sessionsInThread.get();
		if (sessionMap == null) {
            sessionMap = new HashMap<Serializable, SessionInMemory>();
            sessionsInThread.set(sessionMap);
        }
		SessionInMemory sessionInMemory = new SessionInMemory();
		sessionInMemory.setCreateTime(new Date());
		sessionInMemory.setSession(s);
		sessionMap.put(sessionId, sessionInMemory);
	}

  • 6.3 redis集羣
    在使用shiro-redis工具包的時候,驚訝的發現了它已經爲我們提供了redis集羣的支持,後續會進行測試。
    在這裏插入圖片描述

7、測試

  1. 打開登錄頁面,查看cookie是否被修改(已修改爲我們設置的名稱)
    在這裏插入圖片描述
  2. session監聽器控制檯打印人數
    在這裏插入圖片描述
    3.redis可視化工具(成功將session寫入redis中)
    在這裏插入圖片描述

8、源碼

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章