spring-redis-session 自定義 key 和過(guò)期時(shí)間
對(duì)于分布式應(yīng)用來(lái)說(shuō),最開(kāi)始遇到的問(wèn)題就是 session 的存儲(chǔ)了,解決方案大致有如下幾種
- 使用 spring-session 它可以把 session 存儲(chǔ)到你想存儲(chǔ)的位置,如 redis,mysql 等
- 使用 JWTs ,它使用算法來(lái)驗(yàn)證 token 的合法性,是否過(guò)期,并且 token 無(wú)法被偽造,信息也是無(wú)法被篡改的
本文內(nèi)容主要說(shuō) spring-session 使用 redis 來(lái)存儲(chǔ) session ,實(shí)現(xiàn)原理,修改過(guò)期時(shí)間,自定義 key 等
spring-session 對(duì)于內(nèi)部系統(tǒng)來(lái)說(shuō)還是可以的,使用方便,但如果用戶(hù)量上來(lái)了的話(huà),會(huì)使 redis 有很大的 session 存儲(chǔ)開(kāi)銷(xiāo),不太劃算。
使用
使用起來(lái)比較簡(jiǎn)單,簡(jiǎn)單說(shuō)一下,引包,配置,加注解 。如下面三步,就配置好了使用 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 # 其它 超時(shí),端口,庫(kù),連接池,集群,就自己去找了
@EnableRedisHttpSession(maxInactiveIntervalInSeconds= 1800)
測(cè)試:因?yàn)槭窃?getSession 的時(shí)候才會(huì)創(chuàng)建 Session ,所以我們必須在接口中調(diào)用一次才能看到效果
@GetMapping("/sessionId")
public String sessionId(){
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
HttpSession session = request.getSession();
session.setAttribute("user","sanri");
return session.getId();
}
它的存儲(chǔ)結(jié)果如下
hash spring:session:sessions:e3d4d84f-cc9f-44d5-9199-463cd9de8272
string spring:session:sessions:expires:e3d4d84f-cc9f-44d5-9199-463cd9de8272
set spring:session:expirations:1577615340000
第一個(gè) hash 結(jié)構(gòu)存儲(chǔ)了 session 的一些基本信息和用戶(hù)設(shè)置的一些屬性信息
creationTime 創(chuàng)建時(shí)間
lastAccessedTime 最后訪(fǎng)問(wèn)時(shí)間
maxInactiveInterval 過(guò)期時(shí)長(zhǎng),默認(rèn)是 30 分鐘,這里保存的秒值
sessionAttr:user 這是我通過(guò) session.setAttribute 設(shè)置進(jìn)去的屬性
第二個(gè) string 結(jié)構(gòu),它沒(méi)有值,只有一個(gè) ttl 信息,標(biāo)識(shí)這組 key 還能活多久,可以用 ttl 查看
第三個(gè) set 結(jié)構(gòu),保存了所以需要過(guò)期的 key
實(shí)現(xiàn)原理
說(shuō)明:這個(gè)實(shí)現(xiàn)沒(méi)多少難度,我就照著源碼念一遍了,就是一個(gè)過(guò)濾器的應(yīng)用而已。
首先從網(wǎng)上了解到,它是使用過(guò)濾器來(lái)實(shí)現(xiàn)把 session 存儲(chǔ)到 redis 的,然后每次請(qǐng)求都是從 redis 拿到 session 的,所以目標(biāo)就是看它的過(guò)濾器是哪個(gè),是怎么存儲(chǔ)的,又是怎么獲取的。
我們可以從它唯一的入口 @EnableRedisHttpSession 進(jìn)入查看,它引入了一個(gè) RedisHttpSessionConfiguration 開(kāi)啟了一個(gè)定時(shí)器,繼承自 SpringHttpSessionConfiguration ,可以留意到 RedisHttpSessionConfiguration 創(chuàng)建一個(gè) Bean RedisOperationsSessionRepository repository 是倉(cāng)庫(kù)的意思,所以它就是核心類(lèi)了,用于存儲(chǔ) session ;那過(guò)濾器在哪呢,查看SpringHttpSessionConfiguration 它屬于 spring-session-core 包,這是一個(gè) spring 用來(lái)管理 session 的包,是一個(gè)抽象的概念,具體的實(shí)現(xiàn)由 spring-session-data-redis 來(lái)完成 ,那過(guò)濾器肯定在這里創(chuàng)建的,果然可以看到它創(chuàng)建一個(gè) SessionRepositoryFilter 的過(guò)濾器,下面分別看過(guò)濾器和存儲(chǔ)。
SessionRepositoryFilter
過(guò)濾器一定是有 doFilter 方法,查看 doFilter 方法,spring 使用 OncePerRequestFilter 把 doFilter 包裝了一層,最終是調(diào)用 doFilterInternal 來(lái)實(shí)現(xiàn)的,查看 doFilterInternal 方法
實(shí)現(xiàn)方式為使用了包裝者設(shè)計(jì)把 request 和 response 響應(yīng)進(jìn)行了包裝,我們一般拿 session 一般是從 request.getSession() ,所以包裝的 request 肯定要重寫(xiě) getSession ,所以可以看 getSession 方法來(lái)看是如何從 redis 獲取 session ;
前面都是已經(jīng)存在 session 的判斷相關(guān),關(guān)鍵信息在這里
S session = SessionRepositoryFilter.this.sessionRepository.createSession();
這里的 sessionRepository 就是我們用來(lái)存取 session 的 RedisOperationsSessionRepository 查看 createSession 方法
RedisOperationsSessionRepository
// 這里保存了在 redis 中 hash 結(jié)構(gòu)能看到的數(shù)據(jù) 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 模式,則會(huì)立即保存 session 進(jìn) redis ,但默認(rèn)配置的是 ON_SAVE ,那是在哪里保存進(jìn) redis 的呢,我們回到最開(kāi)始的過(guò)濾器 doFilterInternal 方法中,在 finally 中有一句
wrappedRequest.commitSession();
就是在這里將 session 存儲(chǔ)進(jìn) redis 的 ,我們跟進(jìn)去看看,核心語(yǔ)句為這句
SessionRepositoryFilter.this.sessionRepository.save(session);
session.saveDelta();
if (session.isNew()) {
String sessionCreatedKey = getSessionCreatedChannel(session.getId());
this.sessionRedisOperations.convertAndSend(sessionCreatedKey, session.delta);
session.setNew(false);
}
進(jìn)入 saveDelta ,在這里進(jìn)行了 hash 結(jié)構(gòu)的設(shè)置
getSessionBoundHashOperations(sessionId).putAll(this.delta);
最后一行進(jìn)行了過(guò)期時(shí)間的設(shè)置和把當(dāng)前 key 加入 set ,讀者自行查看
RedisOperationsSessionRepository.this.expirationPolicy
.onExpirationUpdated(originalExpiration, this);
修改一些參數(shù)
實(shí)際業(yè)務(wù)中,可能需要修改一些參數(shù)才能達(dá)到我們業(yè)務(wù)的需求,最常見(jiàn)的需求就是修改 session 的過(guò)期時(shí)間了,在 EnableRedisHttpSession 注解中,已經(jīng)提供了一些基本的配置如
maxInactiveIntervalInSeconds 最大過(guò)期時(shí)間,默認(rèn) 30 分鐘
redisNamespace 插入到 redis 的 session 命名空間,默認(rèn)是 spring:session
cleanupCron 過(guò)期 session 清理任務(wù),默認(rèn)是 1 分鐘清理一次
redisFlushMode 刷新方式 ,其實(shí)在上面原理的 flushImmediateIfNecessary 方法中有用到,默認(rèn)是 ON_SAVE
redisNamespace 是一定要修改的,這個(gè)不修改會(huì)影響別的項(xiàng)目,一般使用我們項(xiàng)目的名稱(chēng)加關(guān)鍵字 session 做 key ,表明這是這個(gè)項(xiàng)目的 session 信息。
不過(guò)這樣的配置明顯不夠,對(duì)于最大過(guò)期時(shí)間來(lái)說(shuō),有可能需要加到配置文件中去,而不是寫(xiě)在代碼中,但是這里沒(méi)有提供占位符的功能,回到 RedisOperationsSessionRepository 的創(chuàng)建,最終配置的 maxInactiveIntervalInSeconds 還是要設(shè)置到這個(gè) bean 中去的,我們可以把這個(gè) bean 的創(chuàng)建過(guò)程覆蓋,重寫(xiě) maxInactiveIntervalInSeconds 的獲取過(guò)程,就解決了,代碼如下
@Autowired
RedisTemplate sessionRedisTemplate;
@Autowired
ApplicationEventPublisher applicationEventPublisher;
@Value("${server.session.timeout}")
private int sessionTimeout = 1800;
@Primary // 使用 Primary 來(lái)覆蓋默認(rèn)的 Bean
@Bean
public RedisOperationsSessionRepository sessionRepository() {
RedisOperationsSessionRepository sessionRepository = new RedisOperationsSessionRepository(sessionRedisTemplate);
// 這里要把原來(lái)的屬性引用過(guò)來(lái),避免出錯(cuò) ,可以引用原來(lái)的類(lèi)并復(fù)制屬性 ;像 redisNamespace,redisFlushMode 都要復(fù)制過(guò)來(lái)
return sessionRepository;
}
還有一個(gè)就是 redis 的序列化問(wèn)題,默認(rèn)是使用的 jdk 的對(duì)象序列化,很容易出現(xiàn)加一個(gè)字段或減少一個(gè)字段出現(xiàn)不能反序列化,所以序列化方式是需要換的,如果項(xiàng)目中的緩存就已經(jīng)使用了對(duì)象序列化的話(huà),那就面要為其單獨(dú)寫(xiě)一個(gè) redisTemplate 并設(shè)置進(jìn)去,在構(gòu)建 RedisOperationsSessionRepository 的時(shí)候設(shè)置 redisTemplate
還有一個(gè)就是生成在 redis 中的 key 值都是 uuid 的形式,根本沒(méi)辦法知道當(dāng)前這個(gè) key 是哪個(gè)用戶(hù)在哪里登錄的,我們其實(shí)可以修改它的 key 為 userId_ip_time 的形式,用來(lái)表明這個(gè)用戶(hù)什么時(shí)間在哪個(gè) ip 有登錄過(guò),我是這么玩的(沒(méi)有在實(shí)際中使用過(guò),雖然能改,但可能有坑):
經(jīng)過(guò)前面的源碼分析,創(chuàng)建 session 并保存到 redis 的是 RedisOperationsSessionRepository 的 createSession 方法,但是這里寫(xiě)死了 RedisSession 使用空的構(gòu)造,而且 RedisSession 是 final 的內(nèi)部類(lèi),訪(fǎng)問(wèn)權(quán)限為默認(rèn),構(gòu)造的時(shí)候 new MapSession 也是默認(rèn)的,最終那個(gè) id 為使用 UUID ,看起來(lái)一點(diǎn)辦法都沒(méi)有,其實(shí)在這里創(chuàng)建完 session ,用戶(hù)不一定是登錄成功的狀態(tài),我們應(yīng)該在登錄成功才能修改 session 的 key ,好在 RedisOperationsSessionRepository 提供了一個(gè)方法 findById ,我們可以在這個(gè)上面做文章,先把 RedisSession 查出來(lái),然后用反射得到 MapSession ,然后留意到 MapSession 是可以修改 id 的,它自己也提供了方法 changeSessionId ,我們完全可以在登錄成功調(diào)用 setId 修改 sessionId ,然后再寫(xiě)回去,這個(gè)代碼一定要和 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
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- SpringSession+Redis實(shí)現(xiàn)集群會(huì)話(huà)共享的方法
- Java簡(jiǎn)單實(shí)現(xiàn)session保存到redis的方法示例
- spring boot整合redis實(shí)現(xiàn)shiro的分布式session共享的方法
- 詳解springboot中redis的使用和分布式session共享問(wèn)題
- 如何使用Redis保存用戶(hù)會(huì)話(huà)Session詳解
- Spring Boot高級(jí)教程之使用Redis實(shí)現(xiàn)session共享
- SpringBoot使用redis實(shí)現(xiàn)session共享功能
- redis中session會(huì)話(huà)共享的三種方案
相關(guān)文章
SpringBoot整合JWT框架,解決Token跨域驗(yàn)證問(wèn)題
Json web token (JWT), 是為了在網(wǎng)絡(luò)應(yīng)用環(huán)境間傳遞聲明而執(zhí)行的一種基于JSON的開(kāi)放標(biāo)準(zhǔn)((RFC 7519).定義了一種簡(jiǎn)潔的,自包含的方法用于通信雙方之間以JSON對(duì)象的形式安全的傳遞信息。2021-06-06
基于SpringAI+DeepSeek實(shí)現(xiàn)流式對(duì)話(huà)功能
一般來(lái)說(shuō)大模型的響應(yīng)速度通常是很慢的,為了避免用戶(hù)用戶(hù)能夠耐心等待輸出的結(jié)果,我們通常會(huì)使用流式輸出一點(diǎn)點(diǎn)將結(jié)果輸出給用戶(hù),那么問(wèn)題來(lái)了,想要實(shí)現(xiàn)流式結(jié)果輸出,后端和前端要如何配合?下來(lái)本文給出具體的實(shí)現(xiàn)代碼,需要的朋友可以參考下2025-02-02
spring boot實(shí)現(xiàn)圖片上傳和下載功能
這篇文章主要為大家詳細(xì)介紹了spring boot實(shí)現(xiàn)圖片上傳和下載功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-02-02
Spring?Boot?利用?XML?方式整合?MyBatis
這篇文章主要介紹了Spring?Boot?利用?XML?方式整合?MyBatis,文章圍繞主題的相關(guān)資料展開(kāi)詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,組要的小伙伴可以參考一下2022-05-05
通過(guò)Java實(shí)現(xiàn)設(shè)置Word文檔頁(yè)邊距的方法詳解
頁(yè)邊距是指頁(yè)面的邊線(xiàn)到文字的距離。通??稍陧?yè)邊距內(nèi)部的可打印區(qū)域中插入文字和圖形等。今天這篇文章將為您展示如何通過(guò)編程方式,設(shè)置Word?文檔頁(yè)邊距,感興趣的可以了解一下2023-02-02

