项目开发迭代过程中,Springboot升级到2.0,这其中带来了一些问题,这里主要讲一个由redis存储session过程中产生的问题以及解决方法。
为实现session信息存入redis, pom文件添加下面依赖
<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>
配置文件如下:
#session配置
spring.session.store-type=redis
spring.session.redis.flush-mode=IMMEDIATE
#使用lettuce, redis配置
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.timeout=60000
spring.redis.lettuce.pool.max-active=300
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.max-wait=-1
spring.redis.lettuce.pool.min-idle=0
spring.redis.lettuce.pool.timeBetweenEvictionRunsMillis=60000
spring.redis.lettuce.pool.minEvictableIdleTimeMillis=3600000
spring.redis.lettuce.pool.testOnCreate=false
spring.redis.lettuce.pool.testOnBorrow=false
spring.redis.lettuce.pool.testOnReturn=false
spring.redis.lettuce.pool.testWhileIdle=true
#cookie配置
server.servlet.session.cookie.domain=***.com
server.servlet.session.cookie.http-only=true
server.servlet.session.cookie.path=/
由于需要将sessionId存入cookie中供其他服务使用,因此进行了cookie配置,如果无这一需求,则将session存入redis中的功能基本完成,只需要引入 @EnableRedisHttpSession 注解就能实现功能了。添加配置类:
@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 3600)
public class RedisConfig {
@Bean
public ConfigureRedisAction configureRedisAction() {
return ConfigureRedisAction.NO_OP;
}
}
其中maxInactiveIntervalInSeconds参数表示session失效时间(秒)。基于上述配置,会发现cookie中存的sessionId和redis中存的不一样。
Springboot中通过SessionRepositoryFilter类进行session的管理,其中:
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryFilter.SessionRepositoryRequestWrapper(request, response, this.servletContext);
SessionRepositoryFilter.SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryFilter.SessionRepositoryResponseWrapper(wrappedRequest, response);
try {
filterChain.doFilter(wrappedRequest, wrappedResponse);
} finally {
wrappedRequest.commitSession();
}
}
这一方法从request中创建获取session并在最后一步执行了commitSession,接着往下看:
private void commitSession() {
SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper.HttpSessionWrapper wrappedSession = this.getCurrentSession();
if (wrappedSession == null) {
if (this.isInvalidateClientSession()) {
SessionRepositoryFilter.this.httpSessionIdResolver.expireSession(this, this.response);
}
} else {
S session = wrappedSession.getSession();
this.clearRequestedSessionCache();
SessionRepositoryFilter.this.sessionRepository.save(session);
String sessionId = session.getId();
if (!this.isRequestedSessionIdValid() || !sessionId.equals(this.getRequestedSessionId())) {
SessionRepositoryFilter.this.httpSessionIdResolver.setSessionId(this, this.response, sessionId);
}
}
}
可以看到当前方法对session的操作,最后一步判断session是否合法以及请求sessionId是否与当前获取到的sessionId一致,不满足条件更新session写入response,跟进代码setSessionId的实现类为CookieHttpSessionIdResolver,具体方法实现:
public void setSessionId(HttpServletRequest request, HttpServletResponse response, String sessionId) {
if (!sessionId.equals(request.getAttribute(WRITTEN_SESSION_ID_ATTR))) {
request.setAttribute(WRITTEN_SESSION_ID_ATTR, sessionId);
this.cookieSerializer.writeCookieValue(new CookieValue(request, response, sessionId));
}
}
最终实现writeCookieValue的类为DefaultCookieSerializer,实现代码如下:
public void writeCookieValue(CookieValue cookieValue) {
HttpServletRequest request = cookieValue.getRequest();
HttpServletResponse response = cookieValue.getResponse();
String requestedCookieValue = cookieValue.getCookieValue();
String actualCookieValue = this.jvmRoute != null ? requestedCookieValue + this.jvmRoute : requestedCookieValue;
Cookie sessionCookie = new Cookie(this.cookieName, this.useBase64Encoding ? this.base64Encode(actualCookieValue) : actualCookieValue);
sessionCookie.setSecure(this.isSecureCookie(request));
sessionCookie.setPath(this.getCookiePath(request));
String domainName = this.getDomainName(request);
...
sessionCookie.setMaxAge(cookieValue.getCookieMaxAge());
response.addCookie(sessionCookie);
}
其中新创建cookie的时候通过判断useBase64Encoding来决定是否对sessionId进行base64编码,也就是导致cookie中value与redis中不一致的原因,因此只需要将DefaultCookieSerializer中的useBase64Encoding默认值从true修改为false即可。
在springboot1.0中,该配置默认值为false,2.0升级导致该值默认值为true。因此如果使用场景有相似的同学,肯定也遇到过这个问题。解决方法是在RedisConfig中修改该值:
@Configuration
@EnableRedisHttpSession()
public class RedisConfig {
@Value("${server.servlet.session.cookie.domain}")
private String domain;
@Bean
public ConfigureRedisAction configureRedisAction() {
return ConfigureRedisAction.NO_OP;
}
@Bean
public <S extends Session> SessionRepositoryFilter<? extends Session> springSessionRepositoryFilter(SessionRepository<S> sessionRepository, ServletContext servletContext) {
SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter<S>(sessionRepository);
sessionRepositoryFilter.setServletContext(servletContext);
CookieHttpSessionIdResolver httpSessionStrategy = new CookieHttpSessionIdResolver();
httpSessionStrategy.setCookieSerializer(this.cookieSerializer());
sessionRepositoryFilter.setHttpSessionIdResolver(httpSessionStrategy);
return sessionRepositoryFilter;
}
private CookieSerializer cookieSerializer() {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setDomainName(domain);
serializer.setCookiePath("/");
serializer.setCookieMaxAge(3600);
serializer.setUseBase64Encoding(false);
return serializer;
}
}
通过自定义DefaultCookieSerializer修改默认值,并注入自定义SessionRepositoryFilter,重新debug代码发现,sessionId值一致了,另外一种处理办法就是在使用cookie中的sessionId的时候进行Base64 decode操作获取原值。