spring-redis-session 自定義 key 和過期時間
對於分散式應用來說,最開始遇到的問題就是 session 的儲存了,解決方案大致有如下幾種
- 使用 spring-session 它可以把 session 儲存到你想儲存的位置,如 redis,mysql 等
- 使用 JWTs ,它使用演算法來驗證 token 的合法性,是否過期,並且 token 無法被偽造,資訊也是無法被篡改的
本文內容主要說 spring-session 使用 redis 來儲存 session ,實現原理,修改過期時間,自定義 key 等
spring-session 對於內部系統來說還是可以的,使用方便,但如果使用者量上來了的話,會使 redis 有很大的 session 儲存開銷,不太划算。
使用
使用起來比較簡單,簡單說一下,引包,配置,加註解 。如下面三步,就配置好了使用 redis-session
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> </dependency>
spring.redis.host=localhost
# 其它 超時,埠,庫,連線池,叢集,就自己去找了
@EnableRedisHttpSession(maxInactiveIntervalInSeconds= 1800)
測試:因為是在 getSession 的時候才會建立 Session ,所以我們必須在介面中呼叫一次才能看到效果
@GetMapping("/sessionId") public String sessionId(){ HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); HttpSession session = request.getSession(); session.setAttribute("user","sanri"); return session.getId(); }
它的儲存結果如下
hash spring:session:sessions:e3d4d84f-cc9f-44d5-9199-463cd9de8272
string spring:session:sessions:expires:e3d4d84f-cc9f-44d5-9199-463cd9de8272
set spring:session:expirations:1577615340000
第一個 hash 結構儲存了 session 的一些基本資訊和使用者設定的一些屬性資訊
creationTime 建立時間
lastAccessedTime 最後訪問時間
maxInactiveInterval 過期時長,預設是 30 分鐘,這裡儲存的秒值
sessionAttr:user 這是我通過 session.setAttribute 設定進去的屬性
第二個 string 結構,它沒有值,只有一個 ttl 資訊,標識這組 key 還能活多久,可以用 ttl 檢視
第三個 set 結構,儲存了所以需要過期的 key
實現原理
說明:這個實現沒多少難度,我就照著原始碼念一遍了,就是一個過濾器的應用而已。
首先從網上了解到,它是使用過濾器來實現把 session 儲存到 redis 的,然後每次請求都是從 redis 拿到 session 的,所以目標就是看它的過濾器是哪個,是怎麼儲存的,又是怎麼獲取的。
我們可以從它唯一的入口 @EnableRedisHttpSession
進入檢視,它引入了一個 RedisHttpSessionConfiguration
開啟了一個定時器,繼承自 SpringHttpSessionConfiguration
,可以留意到 RedisHttpSessionConfiguration
建立一個 Bean RedisOperationsSessionRepository
repository 是倉庫的意思,所以它就是核心類了,用於儲存 session ;那過濾器在哪呢,檢視SpringHttpSessionConfiguration
它屬於 spring-session-core 包,這是一個 spring 用來管理 session 的包,是一個抽象的概念,具體的實現由 spring-session-data-redis 來完成 ,那過濾器肯定在這裡建立的,果然可以看到它建立一個 SessionRepositoryFilter
的過濾器,下面分別看過濾器和儲存。
SessionRepositoryFilter
過濾器一定是有 doFilter 方法,檢視 doFilter 方法,spring 使用 OncePerRequestFilter
把 doFilter 包裝了一層,最終是呼叫 doFilterInternal 來實現的,檢視 doFilterInternal 方法
實現方式為使用了包裝者設計把 request 和 response 響應進行了包裝,我們一般拿 session 一般是從 request.getSession() ,所以包裝的 request 肯定要重寫 getSession ,所以可以看 getSession 方法來看是如何從 redis 獲取 session ;
前面都是已經存在 session 的判斷相關,關鍵資訊在這裡
S session = SessionRepositoryFilter.this.sessionRepository.createSession();
這裡的 sessionRepository 就是我們用來存取 session 的 RedisOperationsSessionRepository
檢視 createSession 方法
RedisOperationsSessionRepository
// 這裡儲存了在 redis 中 hash 結構能看到的資料
RedisSession redisSession = new RedisSession();
this(new MapSession());
this.delta.put(CREATION_TIME_ATTR, getCreationTime().toEpochMilli());
this.delta.put(MAX_INACTIVE_ATTR, (int) getMaxInactiveInterval().getSeconds());
this.delta.put(LAST_ACCESSED_ATTR, getLastAccessedTime().toEpochMilli());
this.isNew = true;
this.flushImmediateIfNecessary();
在 flushImmediateIfNecessary 方法中,如果 redisFlushMode 是 IMMEDIATE
模式,則會立即儲存 session 進 redis ,但預設配置的是 ON_SAVE ,那是在哪裡儲存進 redis 的呢,我們回到最開始的過濾器 doFilterInternal 方法中,在 finally 中有一句
wrappedRequest.commitSession();
就是在這裡將 session 儲存進 redis 的 ,我們跟進去看看,核心語句為這句
SessionRepositoryFilter.this.sessionRepository.save(session);
session.saveDelta();
if (session.isNew()) {
String sessionCreatedKey = getSessionCreatedChannel(session.getId());
this.sessionRedisOperations.convertAndSend(sessionCreatedKey, session.delta);
session.setNew(false);
}
進入 saveDelta ,在這裡進行了 hash 結構的設定
getSessionBoundHashOperations(sessionId).putAll(this.delta);
最後一行進行了過期時間的設定和把當前 key 加入 set ,讀者自行檢視
RedisOperationsSessionRepository.this.expirationPolicy
.onExpirationUpdated(originalExpiration, this);
修改一些引數
實際業務中,可能需要修改一些引數才能達到我們業務的需求,最常見的需求就是修改 session 的過期時間了,在 EnableRedisHttpSession
註解中,已經提供了一些基本的配置如
maxInactiveIntervalInSeconds 最大過期時間,預設 30 分鐘
redisNamespace 插入到 redis 的 session 名稱空間,預設是 spring:session
cleanupCron 過期 session 清理任務,預設是 1 分鐘清理一次
redisFlushMode 重新整理方式 ,其實在上面原理的 flushImmediateIfNecessary 方法中有用到,預設是 ON_SAVE
redisNamespace 是一定要修改的,這個不修改會影響別的專案,一般使用我們專案的名稱加關鍵字 session 做 key ,表明這是這個專案的 session 資訊。
不過這樣的配置明顯不夠,對於最大過期時間來說,有可能需要加到配置檔案中去,而不是寫在程式碼中,但是這裡沒有提供佔位符的功能,回到 RedisOperationsSessionRepository
的建立,最終配置的 maxInactiveIntervalInSeconds 還是要設定到這個 bean 中去的,我們可以把這個 bean 的建立過程覆蓋,重寫 maxInactiveIntervalInSeconds 的獲取過程,就解決了,程式碼如下
@Autowired
RedisTemplate sessionRedisTemplate;
@Autowired
ApplicationEventPublisher applicationEventPublisher;
@Value("${server.session.timeout}")
private int sessionTimeout = 1800;
@Primary // 使用 Primary 來覆蓋預設的 Bean
@Bean
public RedisOperationsSessionRepository sessionRepository() {
RedisOperationsSessionRepository sessionRepository = new RedisOperationsSessionRepository(sessionRedisTemplate);
// 這裡要把原來的屬性引用過來,避免出錯 ,可以引用原來的類並複製屬性 ;像 redisNamespace,redisFlushMode 都要複製過來
return sessionRepository;
}
還有一個就是 redis 的序列化問題,預設是使用的 jdk 的物件序列化,很容易出現加一個欄位或減少一個欄位出現不能反序列化,所以序列化方式是需要換的,如果專案中的快取就已經使用了物件序列化的話,那就面要為其單獨寫一個 redisTemplate 並設定進去,在構建 RedisOperationsSessionRepository
的時候設定 redisTemplate
還有一個就是生成在 redis 中的 key 值都是 uuid 的形式,根本沒辦法知道當前這個 key 是哪個使用者在哪裡登入的,我們其實可以修改它的 key 為 userId_ip_time 的形式,用來表明這個使用者什麼時間在哪個 ip 有登入過,我是這麼玩的(沒有在實際中使用過,雖然能改,但可能有坑):
經過前面的原始碼分析,建立 session 並儲存到 redis 的是 RedisOperationsSessionRepository
的 createSession 方法,但是這裡寫死了 RedisSession 使用空的構造,而且 RedisSession 是 final 的內部類,訪問許可權為預設,構造的時候 new MapSession 也是預設的,最終那個 id 為使用 UUID ,看起來一點辦法都沒有,其實在這裡建立完 session ,使用者不一定是登入成功的狀態,我們應該在登入成功才能修改 session 的 key ,好在 RedisOperationsSessionRepository
提供了一個方法 findById ,我們可以在這個上面做文章,先把 RedisSession 查出來,然後用反射得到 MapSession
,然後留意到 MapSession
是可以修改 id 的,它自己也提供了方法 changeSessionId ,我們完全可以在登入成功呼叫 setId 修改 sessionId ,然後再寫回去,這個程式碼一定要和 RedisSession 在同包 程式碼如下:
package org.springframework.session.data.redis;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.session.MapSession;
import org.springframework.stereotype.Component;
import org.springframework.util.ReflectionUtils;
import java.lang.reflect.Field;
@Component
public class SessionOperation {
@Autowired
private RedisOperationsSessionRepository redisOperationsSessionRepository;
public void loginSuccess(String userId){
String sessionId = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest().getSession().getId();
RedisOperationsSessionRepository.RedisSession redisSession = redisOperationsSessionRepository.findById(sessionId);
Field cached = ReflectionUtils.findField(RedisOperationsSessionRepository.RedisSession.class, "cached");
ReflectionUtils.makeAccessible(cached);
MapSession mapSession = (MapSession) ReflectionUtils.getField(cached, redisSession);
mapSession.setId("userId:1");
redisOperationsSessionRepository.save(redisSession);
}
}
原始碼地址: https://gitee.com/sanri/example/tree/master/test-redis-session
一點小推廣
創作不易,希望可以支援下我的開源軟體,及我的小工具,歡迎來 gitee 點星,fork ,提 bug 。
Excel 通用匯入匯出,支援 Excel 公式
部落格地址:https://blog.csdn.net/sanri1993/article/details/100601578
gitee:https://gitee.com/sanri/sanri-excel-poi
使用模板程式碼 ,從資料庫生成程式碼 ,及一些專案中經常可以用到的小工具
部落格地址:https://blog.csdn.net/sanri1993/article/details/98664034
gitee:https://gitee.com/sanri/sanri-tools-ma