SpringBoot+Spring Session+Redis實現Session共享及踩坑記錄


    項目組一同事負責的一個小項目需要Session共享,記得我曾經看過標題如“一個注解搞定Session共享”的文章。我便把之前收藏的一篇Spring Session+ Redis實現session共享的文章發給了他。30分鍾后,本以為一切都順利,卻發現登錄時從session中取驗證碼的code值取不到。經過我的一番排查,終於解決了這個問題,順便寫下了本文。
    Spring Session + redis 實現session 共享 可以參考官網的玩法,也可以參考我下面的代碼。官網傳送門: https://docs.spring.io/spring-session/docs/current/reference/html5/guides/boot-redis.html    

一、Spring Session + Redis 整合標准套路

    優先說明:本次使用SpringBoot版為
     <version>2.1.17.RELEASE</version>

(1)pom.xml添加依賴

        <!-- springboot - Redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!--spring session 與redis應用基本環境配置,需要開啟redis后才可以使用,不然啟動Spring boot會報錯 -->
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>
        <!-- redis lettuce連接池 需要 commons-pool2-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>

 

(2)application.yml配置redis

spring:
    redis:
      host: 192.168.200.156
      port: 6379
      # 密碼 沒有則可以不填
      password: 123456
      # 數據庫索引(根據產品線配置)
      database: 1
      timeout: 1000ms
      # 集群配置(根據實際情況配置多節點)
      #    cluster:
      #      nodes:
      #        - 192.168.200.161:6379
      #      max-redirects: 2
      # lettuce連接池
      lettuce:
        pool:
          # 最大活躍連接數 默認8
          max-active: 32
          # 最大空閑連接數 默認8
          max-idle: 8
          # 最小空閑連接數 默認0
          min-idle: 5

 

(3)啟動類添加注解@EnableRedisHttpSession

    // session托管到redis
    // maxInactiveIntervalInSeconds單位:秒;
    // RedisFlushMode有兩個參數:ON_SAVE(表示在response commit前刷新緩存),IMMEDIATE(表示只要有更新,就刷新緩存)
    @EnableRedisHttpSession(maxInactiveIntervalInSeconds= 1800, redisFlushMode = RedisFlushMode.ON_SAVE,
                            redisNamespace = "newborn")
    @SpringBootApplication
    public class NewbornApplication {

        public static void main(String[] args) {
            SpringApplication.run(NewbornApplication.class, args);
        }
    }  

 

 

二、出現的問題

    問題描述: 用戶登錄頁面的驗證碼存在了Session中,但登錄時發現Session里面沒有驗證碼。也就是說生成驗證碼時的Session和登錄的Session不是一個。
    由於項目采用了前后端分離,因此用了nginx,nginx配置如下:
server{
    listen 8090;
    server_name  localhost;
    proxy_set_header X-Forwarded-Host $host;
    proxy_set_header X-Forwarded-Server $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $host:$server_port;

    # 后端服務反向代理
    location /newborn/ {
        proxy_pass   http://192.168.199.21:8088/newborn/;
        # 設置cookie path替換, 這里是將 /newborn 替換成/ ,將服務端/newborn 的cookie 寫到 / 下,方便前端訪問后端設置的cookie
        proxy_cookie_path /newborn /;   
        # 其實這里也可以不用設置cookie替換,只要后端項目content-path和設置代理location 后面的 代理路徑一樣就不用替換
     }

    # 前端文件
    location / {
        root /newborn;
        # alias /newborn;
        index index.html;
    }
}

 

    這個nginx配置沒有做任何修改,以前沒用Spring Session 時系統一切正常。那么這個問題肯定和Spring Session有關系
 

三、問題的解決過程

(1)發現Cookie path 的改變

    通過打開谷歌瀏覽器的調試模式,發現獲取驗證碼是Response Headers 有Set Cookie的動作,但是這個Path 有點奇怪,是 //。參考下圖
         
    然后由於這個path是// ,導致瀏覽器Cookie沒有寫入成功
         
 

(2)對比使用Spring Session 和 不使用 Spring Session 時Cookie的不同 

    為了排除nginx的干擾,直接通過訪問后端驗證碼的方式來對比二者的不同
    <1> 不使用Spring Session時,參考下圖
         
 
    <2> 使用Spring Session時,path 上多了一個斜杠 / , 參考下圖
         
  對比小結:不使用Spring Session時,Cookie Path 是 項目的ContextPath
         使用Spring Session時,Cookie Path 是 項目的ContextPath + /

(3)原因分析

通過(2)的 cookie 對比,發現Cookie 的Path的變化是這個問題產生的根本原因。
由於使用了nginx,而且nginx中配置了cookie path替換,配置為: proxy_cookie_path /newborn /;   
在使用Spring Session后,后端返回的cookie path 為 /newborn/,經過替換后變成了 //。
也就找到了為何上文中使用nginx后Cookie path為// 的原因

(4)源碼分析

    通過源碼分析,發現org.springframework.session.web.http.DefaultCookieSerializer類里面有個獲取Cookie path的方法,方法內容如下:
    private String getCookiePath(HttpServletRequest request) {
        if (this.cookiePath == null) {
            // 在沒有設置cookiePath 情況下默認取 ContextPath + /
            return request.getContextPath() + "/";
        }
        return this.cookiePath;
    }

 

    在沒有設置cookiePath 情況下默認取 ContextPath + /

(5) 最終解決方案

自己實例化一個自定義DefaultCookieSerializer的到Spring容器中,覆蓋默認的DefaultCookieSerializer。
因此在啟動類中添加下面代碼
    @Autowired
    private ServerProperties serverProperties;

    @Bean
    public CookieSerializer cookieSerializer() {
        // 解決cookiePath會多一個"/" 的問題
        DefaultCookieSerializer serializer = new DefaultCookieSerializer();
        String contextPath = Optional.ofNullable(serverProperties).map(ServerProperties::getServlet)
                .map(ServerProperties.Servlet::getContextPath).orElse(null);
        // 當配置了context path 時設置下cookie path ; 防止cookie path 變成 contextPath + /
        if (!StringUtils.isEmpty(contextPath)) {
            serializer.setCookiePath(contextPath);
        }
        serializer.setUseHttpOnlyCookie(true);
        serializer.setUseSecureCookie(false);
        // 干掉 SameSite=Lax
        serializer.setSameSite(null);
        return serializer;
    }

 

 

四、當沒有Redis時快捷的關閉Spring Session的實現方案

方案:在application.yml 添加個配置項,1開 0關

(1)yml中添加如下內容

# redis session 共享開關,1開 0 關, 沒有redis時請關閉(即 設為0)
redis:
  session:
    share:
      enable: 1

 

(2)添加2個配置類

     將@EnableRedisHttpSession和自定義CookieSerializer從啟動類移動到下面的配置類
RedisSessionShareOpenConfig 代碼
    /**
     * 啟用 Redis Session 共享
     * @author ZENG.XIAO.YAN
     * @version 1.0
     * @Date 2020-09-23
     */
    @Slf4j
    @Configuration
    @ConditionalOnProperty(name = "redis.session.share.enable", havingValue = "1")
    @EnableRedisHttpSession(maxInactiveIntervalInSeconds= 1800,
            redisFlushMode = RedisFlushMode.ON_SAVE, redisNamespace = "newborn")
    public class RedisSessionShareOpenConfig {

        public RedisSessionShareOpenConfig() {
            log.info("<<< Redis Session share open.... >>>");
        }

        @Autowired
        private ServerProperties serverProperties;

        @Bean
        public CookieSerializer cookieSerializer() {
            // 解決cookiePath會多一個"/" 的問題
            DefaultCookieSerializer serializer = new DefaultCookieSerializer();
            String contextPath = Optional.ofNullable(serverProperties).map(ServerProperties::getServlet)
                    .map(ServerProperties.Servlet::getContextPath).orElse(null);
            // 當配置了context path 時設置下cookie path ; 防止cookie path 變成 contextPath + /
            if (!StringUtils.isEmpty(contextPath)) {
                serializer.setCookiePath(contextPath);
            }
            serializer.setUseHttpOnlyCookie(true);
            serializer.setUseSecureCookie(false);
            // 干掉 SameSite=Lax
            serializer.setSameSite(null);
            return serializer;
        }
    }

 

 
RedisSessionShareCloseConfig 代碼
    /**
     * 關閉Redis Session 共享
     * <p> 通過排除Redis的自動配置來達到去掉Redis Session共享功能 </p>
     * @author ZENG.XIAO.YAN
     * @version 1.0
     * @Date 2020-09-23
     */
    @Configuration
    @ConditionalOnProperty(name = "redis.session.share.enable", havingValue = "0")
    @EnableAutoConfiguration(exclude = {RedisAutoConfiguration.class})
    @Slf4j
    public class RedisSessionShareCloseConfig {

        public RedisSessionShareCloseConfig() {
            log.info("<<< Redis Session share close.... >>>");
        }
    }

 

 

五、總結

(1)在使用Spring Session + Redis 共享Session時,默認的Cookie Path 會變成  ContextPath + /
(2)如果存在 nginx 配置了Cookie Path 替換的情況,則一定要注意,防止出現替換后變成了// 的情況


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM