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 替换的情况,则一定要注意,防止出现替换后变成了// 的情况
作者:zeng1994
出处:http://www.cnblogs.com/zeng1994/
本文版权归作者和博客园共有,欢迎转载!但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接!