• 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 替换的情况,则一定要注意,防止出现替换后变成了// 的情况
  • 相关阅读:
    记druid 在配置中心下的一个大坑: cpu 达到 100%
    常见java日志系统的搭配详解:关于slf4j log4j log4j2 logback jul jcl commons-logging jdk-logging
    HTML一片空白, 无法渲染: Empty tag doesn't work in some browsers
    再见:org.apache.catalina.connector.ClientAbortException: java.io.IOException: Connection reset by peer
    spring boot tomcat 打本地包成war,通过Tomcat启动时出现问题: ZipException: error in opening zip file
    Maven 错误:Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.1:compile (default-compile) on project appservice-common: Fatal error compiling: 无效的目标发行版: 1.8
    Maven 错误 :The POM for com.xxx:jar:0.0.1-SNAPSHOT is invalid, transitive dependencies (if any) will not be available
    LocalVariableTable之 Slot 复用
    一些常见的Java面试题 & 面试感悟
    spring 2.5.6 错误:Context namespace element 'component-scan' and its parser class [org.springframework.context.annotation.ComponentScanBeanDefinitionParser] are only available on JDK 1.5 and higher
  • 原文地址:https://www.cnblogs.com/zeng1994/p/19669864cd411c365762073661ca1f01.html
Copyright © 2020-2023  润新知