Redis Configurations

现在你的应用程序配置完毕,你可能希望开始自定义某些内容:

Serializing the Session using JSON

默认情况下,Spring Session 使用 Java 序列化序列化会话属性。有时这可能会出现问题,特别是当使用同一 Redis 实例但具有相同类别的不同版本时。你可以提供 RedisSerializer bean 来自定义会话序列化到 Redis 的方式。Spring Data Redis 提供 GenericJackson2JsonRedisSerializer,它使用 Jackson’s ObjectMapper 序列化和反序列化对象。

Configuring the RedisSerializer
Unresolved include directive in modules/ROOT/pages/configuration/redis.adoc - include::example$spring-session-samples/spring-session-sample-boot-redis-json/src/main/java/sample/config/SessionConfig.java[]

上面的代码片段使用的是 Spring Security,因此我们正在创建一个自定义 ObjectMapper,它使用 Spring Security 的 Jackson 模块。如果您不需要 Spring Security Jackson 模块,则可以注入应用程序的 ObjectMapper bean 并像这样使用它:

@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer(ObjectMapper objectMapper) {
    return new GenericJackson2JsonRedisSerializer(objectMapper);
}

Specifying a Different Namespace

使用同一 Redis 实例有多个应用程序的情况并不少见。出于该原因,Spring Session 使用 namespace(默认为 spring:session)在需要时保持会话数据分离。

Using Spring Boot Properties

你可以通过设置 spring.session.redis.namespace 属性来指定它。

application.properties
spring.session.redis.namespace=spring:session:myapplication
application.yml
spring:
  session:
    redis:
      namespace: "spring:session:myapplication"

Using the Annotation’s Attributes

你可以通过在 @EnableRedisHttpSession@EnableRedisIndexedHttpSession@EnableRedisWebSession 注释中设置 redisNamespace 属性来指定 namespace

@EnableRedisHttpSession
@Configuration
@EnableRedisHttpSession(redisNamespace = "spring:session:myapplication")
public class SessionConfig {
    // ...
}
@EnableRedisIndexedHttpSession
@Configuration
@EnableRedisIndexedHttpSession(redisNamespace = "spring:session:myapplication")
public class SessionConfig {
    // ...
}
@EnableRedisWebSession
@Configuration
@EnableRedisWebSession(redisNamespace = "spring:session:myapplication")
public class SessionConfig {
    // ...
}

Choosing Between RedisSessionRepository and RedisIndexedSessionRepository

在使用 Spring Session Redis 时,你可能不得不从 RedisSessionRepositoryRedisIndexedSessionRepository 中进行选择。二者都是将会话数据存储在 Redis 中的 SessionRepository 接口实现。但它们处理会话索引和查询的方式有所不同。

  • RedisSessionRepositoryRedisSessionRepository 是一个基本实现,它在 Redis 中存储会话数据,没有任何其他索引。它使用简单的键值结构存储会话属性。为每个会话分配一个唯一的会话 ID,会话数据存储在与该 ID 关联的 Redis 密钥下。当需要检索会话时,存储库使用会话 ID 查询 Redis 以获取关联的会话数据。由于没有索引,基于属性或非会话 ID 标准查询会话可能会很低效。

  • RedisIndexedSessionRepositoryRedisIndexedSessionRepository 是一个扩展的实现,它为存储在 Redis 中的会话提供索引功能。它在 Redis 中引入其他数据结构,以便基于属性或条件有效地查询会话。除了 RedisSessionRepository 使用的键值结构之外,它还维护其他索引以启用快速查找。例如,它可以根据会话属性(如用户 ID 或上次访问时间)创建索引。这些索引允许基于特定条件有效地查询会话,从而提高性能并启用高级会话管理功能。除此之外,RedisIndexedSessionRepository 还支持会话过期和删除。

Configuring the RedisSessionRepository

Using Spring Boot Properties

如果你使用 Spring Boot,则 RedisSessionRepository 是默认实现。但是,如果你希望明确表达它,则可以在应用程序中设置以下属性:

application.properties
spring.session.redis.repository-type=default
application.yml
spring:
  session:
    redis:
      repository-type: default

Using Annotations

你可以使用 @EnableRedisHttpSession 注释来配置 RedisSessionRepository

@Configuration
@EnableRedisHttpSession
public class SessionConfig {
    // ...
}

Configuring the RedisIndexedSessionRepository

Using Spring Boot Properties

你可以通过在应用程序中设置以下属性来配置 RedisIndexedSessionRepository

application.properties
spring.session.redis.repository-type=indexed
application.yml
spring:
  session:
    redis:
      repository-type: indexed

Using Annotations

你可以使用 @EnableRedisIndexedHttpSession 注释来配置 RedisIndexedSessionRepository

@Configuration
@EnableRedisIndexedHttpSession
public class SessionConfig {
    // ...
}

Listening to Session Events

通常,对会话事件做出反应很有价值,例如,你可能希望根据会话生命周期执行某种处理。为了做到这一点,你必须使用 indexed repository。如果你不知道索引存储库和默认存储库之间的区别,请转至 this section

在配置了已编制索引的存储库后,现在可以开始监听 SessionCreatedEventSessionDeletedEventSessionDestroyedEventSessionExpiredEvent 事件。Spring 中有多种 {docs-url}/spring-framework/reference/core/beans/context-introduction.html#context-functionality-events[监听应用程序事件的方法],我们将使用 @EventListener 注释。

@Component
public class SessionEventListener {

    @EventListener
    public void processSessionCreatedEvent(SessionCreatedEvent event) {
        // do the necessary work
    }

    @EventListener
    public void processSessionDeletedEvent(SessionDeletedEvent event) {
        // do the necessary work
    }

    @EventListener
    public void processSessionDestroyedEvent(SessionDestroyedEvent event) {
        // do the necessary work
    }

    @EventListener
    public void processSessionExpiredEvent(SessionExpiredEvent event) {
        // do the necessary work
    }

}

Finding All Sessions of a Specific User

通过检索特定用户的全部会话,你可以在所有设备或浏览器中跟踪用户的活动会话。例如,你可以使用此信息用于会话管理目的,例如允许用户注销或从特定会话中退出,或根据用户的会话活动执行操作。

要做到这一点,你首先必须使用 indexed repository,然后你可以注入 FindByIndexNameSessionRepository 接口,如下所示:

@Autowired
public FindByIndexNameSessionRepository<? extends Session> sessions;

public Collection<? extends Session> getSessions(Principal principal) {
    Collection<? extends Session> usersSessions = this.sessions.findByPrincipalName(principal.getName()).values();
    return usersSessions;
}

public void removeSession(Principal principal, String sessionIdToDelete) {
    Set<String> usersSessionIds = this.sessions.findByPrincipalName(principal.getName()).keySet();
    if (usersSessionIds.contains(sessionIdToDelete)) {
        this.sessions.deleteById(sessionIdToDelete);
    }
}

在以上示例中,你可以使用 getSessions 方法查找特定用户的全部会话,并使用 removeSession 方法删除用户的特定会话。

Configuring Redis Session Mapper

Spring Session Redis 从 Redis 中检索会话信息,并将其存储在 Map<String, Object> 中。该映射需要经过一个映射过程才能转换为 MapSession 对象,然后在 RedisSession 中使用。

用于此目的的默认映射器被称为 RedisSessionMapper.如果会话映射不包含构造会话所需的最少量键,如 creationTime,此映射器将会抛出一个异常。必需键缺失的一个可能场景是当在保存进程进行中时,会话键被同时删除,通常由于过期。这发生是因为 HSET command 被用来设置键中的字段,如果键不存在,此命令将会创建它。

如果你想自定义映射过程,可以创建 BiFunction<String, Map<String, Object>, MapSession> 的实现并将其设置到会话存储库中。以下示例演示如何将映射过程委派给默认映射器,但是如果抛出异常,则会话将从 Redis 中删除:

  • RedisSessionRepository

  • RedisIndexedSessionRepository

  • ReactiveRedisSessionRepository

@Configuration
@EnableRedisHttpSession
public class SessionConfig {

    @Bean
    SessionRepositoryCustomizer<RedisSessionRepository> redisSessionRepositoryCustomizer() {
        return (redisSessionRepository) -> redisSessionRepository
                .setRedisSessionMapper(new SafeRedisSessionMapper(redisSessionRepository));
    }

    static class SafeRedisSessionMapper implements BiFunction<String, Map<String, Object>, MapSession> {

        private final RedisSessionMapper delegate = new RedisSessionMapper();

        private final RedisSessionRepository sessionRepository;

        SafeRedisSessionMapper(RedisSessionRepository sessionRepository) {
            this.sessionRepository = sessionRepository;
        }

        @Override
        public MapSession apply(String sessionId, Map<String, Object> map) {
            try {
                return this.delegate.apply(sessionId, map);
            }
            catch (IllegalStateException ex) {
                this.sessionRepository.deleteById(sessionId);
                return null;
            }
        }

    }

}
@Configuration
@EnableRedisIndexedHttpSession
public class SessionConfig {

    @Bean
    SessionRepositoryCustomizer<RedisIndexedSessionRepository> redisSessionRepositoryCustomizer() {
        return (redisSessionRepository) -> redisSessionRepository.setRedisSessionMapper(
                new SafeRedisSessionMapper(redisSessionRepository.getSessionRedisOperations()));
    }

    static class SafeRedisSessionMapper implements BiFunction<String, Map<String, Object>, MapSession> {

        private final RedisSessionMapper delegate = new RedisSessionMapper();

        private final RedisOperations<String, Object> redisOperations;

        SafeRedisSessionMapper(RedisOperations<String, Object> redisOperations) {
            this.redisOperations = redisOperations;
        }

        @Override
        public MapSession apply(String sessionId, Map<String, Object> map) {
            try {
                return this.delegate.apply(sessionId, map);
            }
            catch (IllegalStateException ex) {
                // if you use a different redis namespace, change the key accordingly
                this.redisOperations.delete("spring:session:sessions:" + sessionId); // we do not invoke RedisIndexedSessionRepository#deleteById to avoid an infinite loop because the method also invokes this mapper
                return null;
            }
        }

    }

}
@Configuration
@EnableRedisWebSession
public class SessionConfig {

    @Bean
    ReactiveSessionRepositoryCustomizer<ReactiveRedisSessionRepository> redisSessionRepositoryCustomizer() {
        return (redisSessionRepository) -> redisSessionRepository
                .setRedisSessionMapper(new SafeRedisSessionMapper(redisSessionRepository));
    }

    static class SafeRedisSessionMapper implements BiFunction<String, Map<String, Object>, Mono<MapSession>> {

        private final RedisSessionMapper delegate = new RedisSessionMapper();

        private final ReactiveRedisSessionRepository sessionRepository;

        SafeRedisSessionMapper(ReactiveRedisSessionRepository sessionRepository) {
            this.sessionRepository = sessionRepository;
        }

        @Override
        public Mono<MapSession> apply(String sessionId, Map<String, Object> map) {
            return Mono.fromSupplier(() -> this.delegate.apply(sessionId, map))
                .onErrorResume(IllegalStateException.class,
                    (ex) -> this.sessionRepository.deleteById(sessionId).then(Mono.empty()));
        }

    }

}