spring session + redis实现共享session

一、代码

  1.pom.xml

    <!--spring session-->
    <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>

  2.配置application.properties

server.port=8080
spring.redis.host=localhost
spring.redis.port=6379

#spring session使用存储类型,默认就是redis所以可以省略
spring.session.store-type=redis

  3.启动类

@EnableCaching
@EnableRedisHttpSession
@SpringBootApplication
public class SpringsessionApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringsessionApplication.class, args);
    }

}

  4.控制类

@RestController
public class IndexController {

    @RequestMapping("/show")
    public String show(HttpServletRequest request){
        return "I'm " + request.getLocalPort();
    }


    @RequestMapping(value = "/session")
    public String getSession(HttpServletRequest request) {
        request.getSession().setAttribute("userName", "admin");
        return "I'm " + request.getLocalPort() + " save session " + request.getSession().getId();
    }

    @RequestMapping(value = "/get")
    public String get(HttpServletRequest request) {
        String userName = (String) request.getSession().getAttribute("userName");
        return "I'm " + request.getLocalPort() + " userName :" + userName;
    }
    
}

启动两个端口服务,就能测试session共享成功。

(问题,1.不能解决sso单点登录

    2.redis保存的数据没有序列化,导致很乱)

二、实现原理

  看了上面的配置,我们知道开启 Redis Session 的“秘密”在 @EnableRedisHttpSession 这个注解上。打开 @EnableRedisHttpSession 的源码:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(RedisHttpSessionConfiguration.class)
@Configuration
public @interface EnableRedisHttpSession {
    //Session默认过期时间,秒为单位,默认30分钟
    int maxInactiveIntervalInSeconds() default MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS;
    //配置key的namespace,默认的是spring:session,如果不同的应用共用一个redis,应该为应用配置不同的namespace,这样才能区分这个Session是来自哪个应用的
    String redisNamespace() default RedisOperationsSessionRepository.DEFAULT_NAMESPACE;
    //配置刷新Redis中Session的方式,默认是ON_SAVE模式,只有当Response提交后才会将Session提交到Redis
    //这个模式也可以配置成IMMEDIATE模式,这样的话所有对Session的更改会立即更新到Redis
    RedisFlushMode redisFlushMode() default RedisFlushMode.ON_SAVE;
    //清理过期Session的定时任务默认一分钟一次。
   String cleanupCron() default RedisHttpSessionConfiguration.DEFAULT_CLEANUP_CRON;
}

  这个注解的作用是注册一个sessionRepositoryFilter,这个Filter会拦截所有的的请求,对session进行操作,注入sessionRepositoryFilter的代码在RedisHttpSessionConfiguration中

@Configuration
@EnableScheduling
public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration
        implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware,
        SchedulingConfigurer {
            ...
}

RedisHttpSessionConfiguration继承了springHttpSessionConfiguration,

SpringHttpSessionConfiguration中注册了SessionRepositoryFilter。

@Configuration
public class SpringHttpSessionConfiguration implements ApplicationContextAware {
    ...
    @Bean
    public <S extends Session> SessionRepositoryFilter<? extends Session> springSessionRepositoryFilter(
            SessionRepository<S> sessionRepository) {
        SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter<>(sessionRepository);
        sessionRepositoryFilter.setServletContext(this.servletContext);
        sessionRepositoryFilter.setHttpSessionIdResolver(this.httpSessionIdResolver);
        return sessionRepositoryFilter;
    }
     ...
}

我们发现注册SessionRepositoryFilter时需要一个SessionRepository参数,这个参数是在RedisHttpSessionConfiguration中被注入进去的

@Configuration
@EnableScheduling
public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration
        implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware,SchedulingConfigurer {

  @Bean
    public RedisOperationsSessionRepository sessionRepository() {
        RedisTemplate<Object, Object> redisTemplate = createRedisTemplate();
        RedisOperationsSessionRepository sessionRepository = new RedisOperationsSessionRepository(redisTemplate);
        sessionRepository.setApplicationEventPublisher(this.applicationEventPublisher);
        if (this.defaultRedisSerializer != null) {
            sessionRepository.setDefaultSerializer(this.defaultRedisSerializer);
        }
        sessionRepository.setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);
        if (StringUtils.hasText(this.redisNamespace)) {
            sessionRepository.setRedisKeyNamespace(this.redisNamespace);
        }
        sessionRepository.setRedisFlushMode(this.redisFlushMode);
        int database = resolveDatabase();
        sessionRepository.setDatabase(database);
        return sessionRepository;
    }    
}

SessionRepositoryFilter 拦截到请求后,会先将 request 和 response 对象转换成 Spring 内部的包装类 SessionRepositoryRequestWrapper 和 SessionRepositoryResponseWrapper 对象。SessionRepositoryRequestWrapper 类重写了原生的getSession方法。代码如下:

@Override
public HttpSessionWrapper getSession(boolean create) {
  //通过request的getAttribue方法查找CURRENT_SESSION属性,有直接返回
  HttpSessionWrapper currentSession = getCurrentSession();
  if (currentSession != null) {
    return currentSession;
  }
  //查找客户端中一个叫SESSION的cookie,通过sessionRepository对象根据SESSIONID去Redis中查找Session
  S requestedSession = getRequestedSession();
  if (requestedSession != null) {
    if (getAttribute(INVALID_SESSION_ID_ATTR) == null) {
      requestedSession.setLastAccessedTime(Instant.now());
      this.requestedSessionIdValid = true;
      currentSession = new HttpSessionWrapper(requestedSession, getServletContext());
      currentSession.setNew(false);
      //将Session设置到request属性中
      setCurrentSession(currentSession);
      //返回Session
      return currentSession;
    }
  }
  else {
    // This is an invalid session id. No need to ask again if
    // request.getSession is invoked for the duration of this request
    if (SESSION_LOGGER.isDebugEnabled()) {
      SESSION_LOGGER.debug(
        "No session found by id: Caching result for getSession(false) for this HttpServletRequest.");
    }
    setAttribute(INVALID_SESSION_ID_ATTR, "true");
  }
  //不创建Session就直接返回null
  if (!create) {
    return null;
  }
  if (SESSION_LOGGER.isDebugEnabled()) {
    SESSION_LOGGER.debug(
      "A new session was created. To help you troubleshoot where the session was created we provided a StackTrace (this is not an error). You can prevent this from appearing by disabling DEBUG logging for "
      + SESSION_LOGGER_NAME,
      new RuntimeException(
        "For debugging purposes only (not an error)"));
  }
  //通过sessionRepository创建RedisSession这个对象,可以看下这个类的源代码,如果
  //@EnableRedisHttpSession这个注解中的redisFlushMode模式配置为IMMEDIATE模式,会立即
  //将创建的RedisSession同步到Redis中去。默认是不会立即同步的。
  S session = SessionRepositoryFilter.this.sessionRepository.createSession();
  session.setLastAccessedTime(Instant.now());
  currentSession = new HttpSessionWrapper(session, getServletContext());
  setCurrentSession(currentSession);
  return currentSession;
}

当调用 SessionRepositoryRequestWrapper 对象的getSession方法拿 Session 的时候,会先从当前请求的属性中查找CURRENT_SESSION属性,如果能拿到直接返回,这样操作能减少Redis操作,提升性能。

到现在为止我们发现如果redisFlushMode配置为 ON_SAVE 模式的话,Session 信息还没被保存到 Redis 中,那么这个同步操作到底是在哪里执行的呢?

仔细看代码,我们发现 SessionRepositoryFilter 的doFilterInternal方法最后有一个 finally 代码块,这个代码块的功能就是将 Session同步到 Redis。

@Override
protected void doFilterInternal(HttpServletRequest request,HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
  request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
  SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(
    request, response, this.servletContext);
  SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(
    wrappedRequest, response);
  try {
    filterChain.doFilter(wrappedRequest, wrappedResponse);
  }
  finally {
    //将Session同步到Redis,同时这个方法还会将当前的SESSIONID写到cookie中去,同时还会发布一
    //SESSION创建事件到队列里面去
    wrappedRequest.commitSession();
  }
}

主要的核心类有:

  • @EnableRedisHttpSession:开启 Session 共享功能;
  • RedisHttpSessionConfiguration:配置类,一般不需要我们自己配置,主要功能是配置 SessionRepositoryFilter 和 RedisOperationsSessionRepository 这两个Bean;
  • SessionRepositoryFilter:拦截器,Spring-Session 框架的核心;
  • RedisOperationsSessionRepository:可以认为是一个 Redis 操作的客户端,有在 Redis 中进行增删改查 Session 的功能;
  • SessionRepositoryRequestWrapper:Request 的包装类,主要是重写了getSession方法
  • SessionRepositoryResponseWrapper:Response的包装类。

原理简要总结:

当请求进来的时候,SessionRepositoryFilter 会先拦截到请求,将 request 和 response 对象转换成 SessionRepositoryRequestWrapper 和 SessionRepositoryResponseWrapper 。后续当第一次调用 request 的getSession方法时,会调用到 SessionRepositoryRequestWrapper 的getSession方法。这个方法是被从写过的,逻辑是先从 request 的属性中查找,如果找不到;再查找一个key值是"SESSION"的 Cookie,通过这个 Cookie 拿到 SessionId 去 Redis 中查找,如果查不到,就直接创建一个RedisSession 对象,同步到 Redis 中。

posted @ 2021-06-03 16:38  xujf  Views(748)  Comments(0Edit  收藏  举报