Redis 設定

現在您已經設定好應用程式,您可能想要開始自訂一些項目

使用 JSON 序列化 Session

預設情況下,Spring Session 使用 Java 序列化來序列化 session 屬性。 有時這可能會產生問題,尤其是在您有多個應用程式使用相同的 Redis 實例,但具有相同類別的不同版本時。 您可以提供 RedisSerializer bean 以自訂 session 如何序列化到 Redis 中。 Spring Data Redis 提供了 GenericJackson2JsonRedisSerializer,它使用 Jackson 的 ObjectMapper 序列化和反序列化物件。

設定 RedisSerializer
@Configuration
public class SessionConfig implements BeanClassLoaderAware {

	private ClassLoader loader;

	/**
	 * Note that the bean name for this bean is intentionally
	 * {@code springSessionDefaultRedisSerializer}. It must be named this way to override
	 * the default {@link RedisSerializer} used by Spring Session.
	 */
	@Bean
	public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
		return new GenericJackson2JsonRedisSerializer(objectMapper());
	}

	/**
	 * Customized {@link ObjectMapper} to add mix-in for class that doesn't have default
	 * constructors
	 * @return the {@link ObjectMapper} to use
	 */
	private ObjectMapper objectMapper() {
		ObjectMapper mapper = new ObjectMapper();
		mapper.registerModules(SecurityJackson2Modules.getModules(this.loader));
		return mapper;
	}

	/*
	 * @see
	 * org.springframework.beans.factory.BeanClassLoaderAware#setBeanClassLoader(java.lang
	 * .ClassLoader)
	 */
	@Override
	public void setBeanClassLoader(ClassLoader classLoader) {
		this.loader = classLoader;
	}

}

上面的程式碼片段使用了 Spring Security,因此我們正在建立一個自訂的 ObjectMapper,它使用 Spring Security 的 Jackson 模組。 如果您不需要 Spring Security Jackson 模組,您可以注入應用程式的 ObjectMapper bean 並像這樣使用它

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

指定不同的命名空間

有多個應用程式使用相同的 Redis 實例是很常見的。 因此,Spring Session 使用 namespace (預設為 spring:session) 來在需要時保持 session 資料分離。

使用 Spring Boot 屬性

您可以透過設定 spring.session.redis.namespace 屬性來指定它。

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

使用註解的屬性

您可以透過在 @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 {
    // ...
}

RedisSessionRepositoryRedisIndexedSessionRepository 之間做選擇

當使用 Spring Session Redis 時,您可能需要選擇 RedisSessionRepositoryRedisIndexedSessionRepository。 兩者都是 SessionRepository 介面的實作,用於將 session 資料儲存在 Redis 中。 然而,它們在處理 session 索引和查詢方面有所不同。

  • RedisSessionRepository: RedisSessionRepository 是一個基本實作,它將 session 資料儲存在 Redis 中,而沒有任何額外的索引。 它使用簡單的鍵值結構來儲存 session 屬性。 每個 session 都會被分配一個唯一的 session ID,並且 session 資料儲存在與該 ID 關聯的 Redis 鍵下。 當需要檢索 session 時,儲存庫會使用 session ID 查詢 Redis 以獲取相關的 session 資料。 由於沒有索引,因此基於 session ID 以外的屬性或條件查詢 session 可能效率低下。

  • RedisIndexedSessionRepository: RedisIndexedSessionRepository 是一個擴充的實作,它為儲存在 Redis 中的 session 提供索引功能。 它在 Redis 中引入了額外的資料結構,以根據屬性或條件有效地查詢 session。 除了 RedisSessionRepository 使用的鍵值結構之外,它還維護額外的索引以實現快速查找。 例如,它可以基於 session 屬性 (如使用者 ID 或上次訪問時間) 建立索引。 這些索引允許基於特定條件有效地查詢 session,從而提高效能並啟用進階的 session 管理功能。 除此之外,RedisIndexedSessionRepository 也支援 session 過期和刪除。

當將 RedisIndexedSessionRepository 與 Redis Cluster 一起使用時,您必須注意它只訂閱叢集中一個隨機 redis 節點的事件,如果事件發生在不同的節點中,這可能會導致某些 session 索引未被清除。

設定 RedisSessionRepository

使用 Spring Boot 屬性

如果您使用 Spring Boot,則 RedisSessionRepository 是預設的實作。 但是,如果您想明確地指定它,您可以在應用程式中設定以下屬性

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

使用註解

您可以使用 @EnableRedisHttpSession 註解來設定 RedisSessionRepository

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

設定 RedisIndexedSessionRepository

使用 Spring Boot 屬性

您可以透過在應用程式中設定以下屬性來設定 RedisIndexedSessionRepository

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

使用註解

您可以使用 @EnableRedisIndexedHttpSession 註解來設定 RedisIndexedSessionRepository

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

監聽 Session 事件

通常,對 session 事件做出反應很有價值,例如,您可能想要根據 session 生命週期執行某種類型的處理。 為了能夠做到這一點,您必須使用索引儲存庫。 如果您不知道索引儲存庫和預設儲存庫之間的區別,您可以前往此章節

設定索引儲存庫後,您現在可以開始監聽 SessionCreatedEventSessionDeletedEventSessionDestroyedEventSessionExpiredEvent 事件。 在 Spring 中,有幾種監聽應用程式事件的方法,我們將使用 @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
    }

}

尋找特定使用者的所有 Session

透過檢索特定使用者的所有 session,您可以追蹤使用者在不同裝置或瀏覽器上的活動 session。 例如,您可以使用此資訊進行 session 管理,例如允許使用者使特定 session 無效或登出,或根據使用者的 session 活動執行動作。

要做到這一點,首先您必須使用索引儲存庫,然後您可以注入 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 方法來尋找特定使用者的所有 session,並使用 removeSession 方法來移除使用者的特定 session。

設定 Redis Session Mapper

Spring Session Redis 從 Redis 檢索 session 資訊並將其儲存在 Map<String, Object> 中。 此 map 需要經過對應程序才能轉換為 MapSession 物件,然後在 RedisSession 中使用。

用於此目的的預設 mapper 稱為 RedisSessionMapper。 如果 session map 不包含建構 session 所需的最小必要鍵 (例如 creationTime),則此 mapper 將會拋出例外。 缺少必要鍵的一個可能情境是當 session 鍵同時被刪除時,通常是由於過期,同時儲存程序正在進行中。 發生這種情況的原因是使用 HSET 命令 來設定鍵內的欄位,如果該鍵不存在,此命令將會建立它。

如果您想自訂對應程序,您可以建立自己的 BiFunction<String, Map<String, Object>, MapSession> 實作並將其設定到 session 儲存庫中。 以下範例示範如何將對應程序委派給預設 mapper,但如果拋出例外,則會從 Redis 中刪除 session

  • 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()));
        }

    }

}