spring-redis-session 自定義 key 和過時時間

對於分佈式應用來講,最開始遇到的問題就是 session 的存儲了,解決方案大體有以下幾種java

  • 使用 spring-session 它能夠把 session 存儲到你想存儲的位置,如 redis,mysql 等
  • 使用 JWTs ,它使用算法來驗證 token 的合法性,是否過時,而且 token 沒法被僞造,信息也是沒法被篡改的

本文內容主要說 spring-session 使用 redis 來存儲 session ,實現原理,修改過時時間,自定義 key 等mysql

spring-session 對於內部系統來講仍是能夠的,使用方便,但若是用戶量上來了的話,會使 redis 有很大的 session 存儲開銷,不太划算。git

使用

使用起來比較簡單,簡單說一下,引包,配置,加註解 。以下面三步,就配置好了使用 redis-sessionredis

<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();
}

它的存儲結果以下spring

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 的一些基本信息和用戶設置的一些屬性信息sql

creationTime 建立時間數據庫

lastAccessedTime 最後訪問時間緩存

maxInactiveInterval 過時時長,默認是 30 分鐘,這裏保存的秒值session

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-maven

相關文章
相關標籤/搜索