天天看点

Springboot 2.0 redis存储session

项目开发迭代过程中,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操作获取原值。

下一篇: SQLPLUS总结

继续阅读