SpringBoot+SpringSession+Redis实现Session共享及踩坑记录项⽬组⼀同事负责的⼀个⼩项⽬需要Session共享,记得我曾经看过标题如“⼀个注解搞定Session共享”的⽂章。我便把之前收藏的⼀篇
Spring Session+ Redis实现session共享的⽂章发给了他。30分钟后,本以为⼀切都顺利,却发现登录时从session中取验证码的code值取不到。经过我的⼀番排查,终于解决了这个问题,顺便写下了本⽂。
Spring Session + redis 实现session 共享可以参考官⽹的玩法,也可以参考我下⾯的代码。官⽹传送门:
⼀、Spring Session + Redis 整合标准套路
优先说明:本次使⽤SpringBoot版为
<version>2.1.17.RELEASE</version>
(1)l添加依赖
<!-- 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.apachemons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
(2)l配置redis
spring:
redis:
host: 192.168.200.156
port: 6379
# 密码没有则可以不填
password: 123456
# 数据库索引(根据产品线配置)
database: 1
timeout: 1000ms
session怎么记忆
# 集配置(根据实际情况配置多节点)
#    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  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 (kiePath == null) {
// 在没有设置cookiePath 情况下默认取 ContextPath + /
ContextPath() + "/";
}
kiePath;
}
在没有设置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的实现⽅案
⽅案:在l 添加个配置项,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.able", havingValue = "1")
@EnableRedisHttpSession(maxInactiveIntervalInSeconds= 1800,
redisFlushMode = RedisFlushMode.ON_SAVE, redisNamespace = "newborn")
public class RedisSessionShareOpenConfig {
public RedisSessionShareOpenConfig() {
log.info("<<< Redis Session >>>");
}
@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.able", havingValue = "0")
@EnableAutoConfiguration(exclude = {RedisAutoConfiguration.class})
@Slf4j
public class RedisSessionShareCloseConfig {
public RedisSessionShareCloseConfig() {
log.info("<<< Redis Session >>>");
}
}
五、总结
(1)在使⽤Spring Session + Redis 共享Session时,默认的Cookie Path 会变成  ContextPath + /(2)如果存在 nginx 配置了Cookie Path 替换的情况,则⼀定要注意,防⽌出现替换后变成了// 的情况

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。