spring-session-data-redis核心原理

弘涛
2023-12-01

1、session原理

session是用来在服务器端保存用户登录信息的KV结构数据,用户在浏览器登录之后,服务器端生成sessionId,返回给浏览器端,浏览器下一次请求在Header的Cookie中带上sessionId,服务器根据sessionId就可以获取用户的信息,从而进行登录鉴权等操作。如果sessionId不存在,服务器端会返回一个新的sessionId。

2、分布式session

在多个微服务部署的场景中,用户登录之后,从服务A生成session, 拿到sessionId,去请求服务B,服务B没有这个session, 就会出现鉴权不通过的情况。这时需要引入分布式session, 常见的是基于redis 的分布式session

需要引入一下依赖:

<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

3、启动过程

核心注解是@EnableRedisHttpSession, 该注解定义了session有效时间、命名空间、刷新模式、定时清理cron表达式、保存模式等属性

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(RedisHttpSessionConfiguration.class)
@Configuration(proxyBeanMethods = false)
public @interface EnableRedisHttpSession {
    // 过期时间,单位秒,默认30min
	int maxInactiveIntervalInSeconds() default MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS;

    // session命名空间,一应用一个,多个应用使用同一个redis,需要保持唯一
	String redisNamespace() default RedisIndexedSessionRepository.DEFAULT_NAMESPACE;

    // redis session刷新模式
	@Deprecated
	RedisFlushMode redisFlushMode() default RedisFlushMode.ON_SAVE;

     // redis session刷新模式默认是ON_SAVE,即调用SessionRepository#save(Session)之后才会刷新redis,IMMEDIATE:任何对session的更新都会刷新到redis
	FlushMode flushMode() default FlushMode.ON_SAVE;

    // session过期定时任务的cron表达式,默认每分钟运行
	String cleanupCron() default RedisHttpSessionConfiguration.DEFAULT_CLEANUP_CRON;

    // session的保存模式,默认ON_SET_ATTRIBUTE:只保存对session的修改
	SaveMode saveMode() default SaveMode.ON_SET_ATTRIBUTE;

}

使用时可以新建配置文件,写入如下的内容(假设已经配置好redis,并使用lettuce)

@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 3600, redisNamespace = "xlt_namespace")
public class RedisSessionConfig {
    @Bean
    public LettuceConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory();
    }
}

核心配置类是RedisHttpSessionConfiguration,redis 的session配置需要的信息主要是通过这个类完成。首先,setImportMetadata方法会根据EnableRedisHttpSession注解的配置,读取到spring session的相关配置信息

@Override
@SuppressWarnings("deprecation")
public void setImportMetadata(AnnotationMetadata importMetadata) {
    // 获取EnableRedisHttpSession注解的配置信息
   Map<String, Object> attributeMap = importMetadata
         .getAnnotationAttributes(EnableRedisHttpSession.class.getName());
    // 拿到配置属性
   AnnotationAttributes attributes = AnnotationAttributes.fromMap(attributeMap);
    // session失效时间
   this.maxInactiveIntervalInSeconds = attributes.getNumber("maxInactiveIntervalInSeconds");
    // 命名空间
   String redisNamespaceValue = attributes.getString("redisNamespace");
   if (StringUtils.hasText(redisNamespaceValue)) {
      this.redisNamespace = this.embeddedValueResolver.resolveStringValue(redisNamespaceValue);
   }
    // 刷新模式
   FlushMode flushMode = attributes.getEnum("flushMode");
   RedisFlushMode redisFlushMode = attributes.getEnum("redisFlushMode");
   if (flushMode == FlushMode.ON_SAVE && redisFlushMode != RedisFlushMode.ON_SAVE) {
      flushMode = redisFlushMode.getFlushMode();
   }
   this.flushMode = flushMode;
    // 保存模式
   this.saveMode = attributes.getEnum("saveMode");
    // session清理定时任务频率配置,cron表达式,默认是每分钟一次 "0 * * * * *"
   String cleanupCron = attributes.getString("cleanupCron");
   if (StringUtils.hasText(cleanupCron)) {
      this.cleanupCron = cleanupCron;
   }
}

然后是创建RedisIndexedSessionRepository,他主要是管理RedisSession对Redis数据库的操作

@Bean
public RedisIndexedSessionRepository sessionRepository() {
   // 创建redisTemplate,用来操作redis,默认使用hset结构存储session信息
   RedisTemplate<Object, Object> redisTemplate = createRedisTemplate();
   RedisIndexedSessionRepository sessionRepository = new RedisIndexedSessionRepository(redisTemplate);
    // 设置应用时间发布器
   sessionRepository.setApplicationEventPublisher(this.applicationEventPublisher);
   if (this.indexResolver != null) {
      sessionRepository.setIndexResolver(this.indexResolver);
   }
   if (this.defaultRedisSerializer != null) {
      sessionRepository.setDefaultSerializer(this.defaultRedisSerializer);
   }
    // 设置session最大有效时间
   sessionRepository.setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);
    // 设置命名空间
   if (StringUtils.hasText(this.redisNamespace)) {
      sessionRepository.setRedisKeyNamespace(this.redisNamespace);
   }
    // 设置刷新模式
   sessionRepository.setFlushMode(this.flushMode);
    // 设置保存模式
   sessionRepository.setSaveMode(this.saveMode);
    // 解析数据库类型,0为redis
   int database = resolveDatabase();
   sessionRepository.setDatabase(database);
   this.sessionRepositoryCustomizers
         .forEach((sessionRepositoryCustomizer) -> sessionRepositoryCustomizer.customize(sessionRepository));
   return sessionRepository;
}

由于RedisHttpSessionConfiguration这个类继承了SpringHttpSessionConfiguration,在setImportMetadata方法完成之后,就会调用springSessionRepositoryFilter方法,新建SessionRepositoryFilter,HTTP的每个请求都要经过这个Filter的处理,校验其中的session信息。

@Bean
public <S extends Session> SessionRepositoryFilter<? extends Session> springSessionRepositoryFilter(SessionRepository<S> sessionRepository) {
    SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter(sessionRepository);
    sessionRepositoryFilter.setHttpSessionIdResolver(this.httpSessionIdResolver);
    return sessionRepositoryFilter;
}

SessionRepositoryFilter的doFilter方法实现如下:

@Order(-2147483598)
public class SessionRepositoryFilter<S extends Session> extends OncePerRequestFilter {    
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    // 设置session属性
        request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
    // 打包请求
        SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryFilter.SessionRepositoryRequestWrapper(request, response);
    // 打包响应
        SessionRepositoryFilter.SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryFilter.SessionRepositoryResponseWrapper(wrappedRequest, response);

        try {
            // 执行下一个filter
            filterChain.doFilter(wrappedRequest, wrappedResponse);
        } finally {
            // 提交session
            wrappedRequest.commitSession();
        }

    }
}

然后初始化Redis消息监听器容器RedisMessageListenerContainer

@Configuration(proxyBeanMethods = false)
public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration
		implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware {
    @Bean
    public RedisMessageListenerContainer springSessionRedisMessageListenerContainer(
          RedisIndexedSessionRepository sessionRepository) {
       RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        //设置redis连接工厂
       container.setConnectionFactory(this.redisConnectionFactory);
        // 设置任务执行器
       if (this.redisTaskExecutor != null) {
          container.setTaskExecutor(this.redisTaskExecutor);
       }
        // 设置redis脚本执行器
       if (this.redisSubscriptionExecutor != null) {
          container.setSubscriptionExecutor(this.redisSubscriptionExecutor);
       }
        // 添加消息监听器,监听session的删除、到期、创建事件
       container.addMessageListener(sessionRepository,
             Arrays.asList(new ChannelTopic(sessionRepository.getSessionDeletedChannel()),
                   new ChannelTopic(sessionRepository.getSessionExpiredChannel())));
       container.addMessageListener(sessionRepository,
             Collections.singletonList(new PatternTopic(sessionRepository.getSessionCreatedChannelPrefix() + "*")));
       return container;
    }
}

接着开启Redis的键空间通知用来确保session的删除和到期会触发session销毁事件

@Configuration(proxyBeanMethods = false)
public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration
		implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware {
    @Bean
    public InitializingBean enableRedisKeyspaceNotificationsInitializer() {
        //开启Redis的键空间通知
       return new EnableRedisKeyspaceNotificationsInitializer(this.redisConnectionFactory, this.configureRedisAction);
    }
}

最后设置session事件监听适配器

@Order(-2147483598)
public class SessionRepositoryFilter<S extends Session> extends OncePerRequestFilter {  
    @Bean
    public SessionEventHttpSessionListenerAdapter sessionEventHttpSessionListenerAdapter() {
        return new SessionEventHttpSessionListenerAdapter(this.httpSessionListeners);
    }
}

4、运行原理

配置了spring session之后,HTTP的每个请求都要经过SessionRepositoryFilter的处理,校验其中的session信息, 他的doFilterInternal方法定义如下:

protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    // 设置session属性
        request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
    // 打包请求
        SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryFilter.SessionRepositoryRequestWrapper(request, response);
    // 打包响应
        SessionRepositoryFilter.SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryFilter.SessionRepositoryResponseWrapper(wrappedRequest, response);

        try {
            // 执行下一个filter
            filterChain.doFilter(wrappedRequest, wrappedResponse);
        } finally {
            // 提交session
            wrappedRequest.commitSession();
        }
}

其中,commitSession是进行session校验和持久化的关键

// 使用HttpSessionIdResolver把sessionId写入响应并且持久化session
private void commitSession() {
    // 获取当前的包装session
   HttpSessionWrapper wrappedSession = getCurrentSession();
   if (wrappedSession == null) {
      if (isInvalidateClientSession()) {
         SessionRepositoryFilter.this.httpSessionIdResolver.expireSession(this, this.response);
      }
   } else {
       // 获取session
      S session = wrappedSession.getSession();
       // 清理请求session缓存,主要是把请求的sessionId等清空
      clearRequestedSessionCache();
       // 保存session
      SessionRepositoryFilter.this.sessionRepository.save(session);
       // 获取sessionId
      String sessionId = session.getId();
      // 已请求的sessionId是否有效,并且是否和当前的sessionId
      if (!isRequestedSessionIdValid() || !sessionId.equals(getRequestedSessionId())) { // 不相等
          // 生成新的sessionId返回
         SessionRepositoryFilter.this.httpSessionIdResolver.setSessionId(this, this.response, sessionId);
      }
   }
}

上面的保存session,会调用RedisIndexedSessionRepository的保存方法,将session信息保存到redis

@Override
public void save(RedisSession session) {
    // 保存session
   session.save();
   if (session.isNew) { // 新的session
       // 创建session缓存的Key
      String sessionCreatedKey = getSessionCreatedChannel(session.getId());
       // 保存到redis
      this.sessionRedisOperations.convertAndSend(sessionCreatedKey, session.delta);
      session.isNew = false;
   }
}

private void save() {
    // 保存变更的sesssionId 
    saveChangeSessionId();
    // 保存属性
    saveDelta();
}

private void saveChangeSessionId() {
    // 获取当前的sessionId
    String sessionId = getId();
    if (sessionId.equals(this.originalSessionId)) { // sessionId没有变
        return;
    }
    if (!this.isNew) { // sessionId变了
        // 原始key
        String originalSessionIdKey = getSessionKey(this.originalSessionId);
        // 新的key
        String sessionIdKey = getSessionKey(sessionId);
        try {
            // 更新redis中的值
            RedisIndexedSessionRepository.this.sessionRedisOperations.rename(originalSessionIdKey,
                                                                             sessionIdKey);
        }
        catch (NonTransientDataAccessException ex) {
            handleErrNoSuchKeyError(ex);
        }
        // 原始过期key
        String originalExpiredKey = getExpiredKey(this.originalSessionId);
        // 新过期key
        String expiredKey = getExpiredKey(sessionId);
        try {
            // 更新过期key
            RedisIndexedSessionRepository.this.sessionRedisOperations.rename(originalExpiredKey, expiredKey);
        }
        catch (NonTransientDataAccessException ex) {
            handleErrNoSuchKeyError(ex);
        }
    }
    // 更新key
    this.originalSessionId = sessionId;
}

// 保存所有属性的变更并修改过期时间
private void saveDelta() {
    if (this.delta.isEmpty()) {
        return;
    }
    String sessionId = getId();
    getSessionBoundHashOperations(sessionId).putAll(this.delta);
    String principalSessionKey = getSessionAttrNameKey(
        FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME);
    String securityPrincipalSessionKey = getSessionAttrNameKey(SPRING_SECURITY_CONTEXT);
    // 判断基本命名指数是否发生变化
    if (this.delta.containsKey(principalSessionKey) || this.delta.containsKey(securityPrincipalSessionKey)) {
        if (this.originalPrincipalName != null) {
            String originalPrincipalRedisKey = getPrincipalKey(this.originalPrincipalName);
            RedisIndexedSessionRepository.this.sessionRedisOperations.boundSetOps(originalPrincipalRedisKey)
                .remove(sessionId);
        }
        Map<String, String> indexes = RedisIndexedSessionRepository.this.indexResolver.resolveIndexesFor(this);
        String principal = indexes.get(PRINCIPAL_NAME_INDEX_NAME);
        this.originalPrincipalName = principal;
        if (principal != null) {
            String principalRedisKey = getPrincipalKey(principal);
            RedisIndexedSessionRepository.this.sessionRedisOperations.boundSetOps(principalRedisKey)
                .add(sessionId);
        }
    }
    
    this.delta = new HashMap<>(this.delta.size());
    
    // 过期时间=上一次访问时间+最大过期时间间隔
    Long originalExpiration = (this.originalLastAccessTime != null)
        ? this.originalLastAccessTime.plus(getMaxInactiveInterval()).toEpochMilli() : null;
    // 持久化到redis
    RedisIndexedSessionRepository.this.expirationPolicy.onExpirationUpdated(originalExpiration, this);
}
 类似资料: