API Documentation

Using Session

Session 是名称值对的一个简化 Map

典型用法可能如下面的清单所示:

/*
 * Copyright 2014-2023 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package docs;

import java.time.Duration;
import java.util.concurrent.ConcurrentHashMap;

import com.hazelcast.config.Config;
import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;
import org.junit.jupiter.api.Test;

import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.mock.web.MockServletContext;
import org.springframework.session.MapSession;
import org.springframework.session.MapSessionRepository;
import org.springframework.session.ReactiveSessionRepository;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.session.data.redis.ReactiveRedisSessionRepository;
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
import org.springframework.session.data.redis.RedisSessionRepository;
import org.springframework.session.hazelcast.HazelcastIndexedSessionRepository;
import org.springframework.session.jdbc.JdbcIndexedSessionRepository;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;

import static org.assertj.core.api.Assertions.assertThat;

/**
 * @author Rob Winch
 * @author Vedran Pavic
 */
class IndexDocTests {

	private static final String ATTR_USER = "user";

	@Test
	void repositoryDemo() {
		RepositoryDemo<MapSession> demo = new RepositoryDemo<>();
		demo.repository = new MapSessionRepository(new ConcurrentHashMap<>());

		demo.demo();
	}

	// tag::repository-demo[]
	public class RepositoryDemo<S extends Session> {

		private SessionRepository<S> repository; (1)

		public void demo() {
			S toSave = this.repository.createSession(); (2)

			(3)
			User rwinch = new User("rwinch");
			toSave.setAttribute(ATTR_USER, rwinch);

			this.repository.save(toSave); (4)

			S session = this.repository.findById(toSave.getId()); (5)

			(6)
			User user = session.getAttribute(ATTR_USER);
			assertThat(user).isEqualTo(rwinch);
		}

		// ... setter methods ...

	}
	// end::repository-demo[]

	@Test
	void expireRepositoryDemo() {
		ExpiringRepositoryDemo<MapSession> demo = new ExpiringRepositoryDemo<>();
		demo.repository = new MapSessionRepository(new ConcurrentHashMap<>());

		demo.demo();
	}

	// tag::expire-repository-demo[]
	public class ExpiringRepositoryDemo<S extends Session> {

		private SessionRepository<S> repository; (1)

		public void demo() {
			S toSave = this.repository.createSession(); (2)
			// ...
			toSave.setMaxInactiveInterval(Duration.ofSeconds(30)); (3)

			this.repository.save(toSave); (4)

			S session = this.repository.findById(toSave.getId()); (5)
			// ...
		}

		// ... setter methods ...

	}
	// end::expire-repository-demo[]

	@Test
	void newRedisSessionRepository() {
		// tag::new-redissessionrepository[]
		RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();

		// ... configure redisTemplate ...

		SessionRepository<? extends Session> repository = new RedisSessionRepository(redisTemplate);
		// end::new-redissessionrepository[]
	}

	@Test
	void newRedisIndexedSessionRepository() {
		// tag::new-redisindexedsessionrepository[]
		RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();

		// ... configure redisTemplate ...

		SessionRepository<? extends Session> repository = new RedisIndexedSessionRepository(redisTemplate);
		// end::new-redisindexedsessionrepository[]
	}

	@Test
	void newReactiveRedisSessionRepository() {
		LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory();
		RedisSerializationContext<String, Object> serializationContext = RedisSerializationContext
			.<String, Object>newSerializationContext(new JdkSerializationRedisSerializer())
			.build();

		// tag::new-reactiveredissessionrepository[]
		// ... create and configure connectionFactory and serializationContext ...

		ReactiveRedisTemplate<String, Object> redisTemplate = new ReactiveRedisTemplate<>(connectionFactory,
				serializationContext);

		ReactiveSessionRepository<? extends Session> repository = new ReactiveRedisSessionRepository(redisTemplate);
		// end::new-reactiveredissessionrepository[]
	}

	@Test
	void mapRepository() {
		// tag::new-mapsessionrepository[]
		SessionRepository<? extends Session> repository = new MapSessionRepository(new ConcurrentHashMap<>());
		// end::new-mapsessionrepository[]
	}

	@Test
	void newJdbcIndexedSessionRepository() {
		// tag::new-jdbcindexedsessionrepository[]
		JdbcTemplate jdbcTemplate = new JdbcTemplate();

		// ... configure jdbcTemplate ...

		TransactionTemplate transactionTemplate = new TransactionTemplate();

		// ... configure transactionTemplate ...

		SessionRepository<? extends Session> repository = new JdbcIndexedSessionRepository(jdbcTemplate,
				transactionTemplate);
		// end::new-jdbcindexedsessionrepository[]
	}

	@Test
	void newHazelcastIndexedSessionRepository() {
		// tag::new-hazelcastindexedsessionrepository[]

		Config config = new Config();

		// ... configure Hazelcast ...

		HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance(config);

		HazelcastIndexedSessionRepository repository = new HazelcastIndexedSessionRepository(hazelcastInstance);
		// end::new-hazelcastindexedsessionrepository[]
	}

	@Test
	void runSpringHttpSessionConfig() {
		AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
		context.register(SpringHttpSessionConfig.class);
		context.setServletContext(new MockServletContext());
		context.refresh();

		try {
			context.getBean(SessionRepositoryFilter.class);
		}
		finally {
			context.close();
		}
	}

	private static final class User {

		private User(String username) {
		}

	}

}
1 我们创建一个 SessionRepository 实例与泛型类型 S,它扩展了 Session。泛型类型在我们的类中定义。
2 我们使用 SessionRepository 创建一个新的 Session,并将其分配给类型为 S 的变量。
3 我们与 Session 交互。在我们的示例中,我们演示将 User 保存到 Session
4 现在我们保存 Session。这就是为什么我们需要泛型类型 S 的原因。SessionRepository 只允许保存使用相同 SessionRepository 创建或检索的 Session 实例。这允许 SessionRepository 进行特定于实现的优化(即只写入已更改的属性)。
5 我们从 SessionRepository 中检索 Session
6 我们从 Session 中获取持久化的 User,而无需显式转换我们的属性。

Session API 还提供了与 Session 实例过期相关的属性。

典型用法可能如下面的清单所示:

/*
 * Copyright 2014-2023 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package docs;

import java.time.Duration;
import java.util.concurrent.ConcurrentHashMap;

import com.hazelcast.config.Config;
import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;
import org.junit.jupiter.api.Test;

import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.mock.web.MockServletContext;
import org.springframework.session.MapSession;
import org.springframework.session.MapSessionRepository;
import org.springframework.session.ReactiveSessionRepository;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.session.data.redis.ReactiveRedisSessionRepository;
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
import org.springframework.session.data.redis.RedisSessionRepository;
import org.springframework.session.hazelcast.HazelcastIndexedSessionRepository;
import org.springframework.session.jdbc.JdbcIndexedSessionRepository;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;

import static org.assertj.core.api.Assertions.assertThat;

/**
 * @author Rob Winch
 * @author Vedran Pavic
 */
class IndexDocTests {

	private static final String ATTR_USER = "user";

	@Test
	void repositoryDemo() {
		RepositoryDemo<MapSession> demo = new RepositoryDemo<>();
		demo.repository = new MapSessionRepository(new ConcurrentHashMap<>());

		demo.demo();
	}

	// tag::repository-demo[]
	public class RepositoryDemo<S extends Session> {

		private SessionRepository<S> repository; (1)

		public void demo() {
			S toSave = this.repository.createSession(); (2)

			(3)
			User rwinch = new User("rwinch");
			toSave.setAttribute(ATTR_USER, rwinch);

			this.repository.save(toSave); (4)

			S session = this.repository.findById(toSave.getId()); (5)

			(6)
			User user = session.getAttribute(ATTR_USER);
			assertThat(user).isEqualTo(rwinch);
		}

		// ... setter methods ...

	}
	// end::repository-demo[]

	@Test
	void expireRepositoryDemo() {
		ExpiringRepositoryDemo<MapSession> demo = new ExpiringRepositoryDemo<>();
		demo.repository = new MapSessionRepository(new ConcurrentHashMap<>());

		demo.demo();
	}

	// tag::expire-repository-demo[]
	public class ExpiringRepositoryDemo<S extends Session> {

		private SessionRepository<S> repository; (1)

		public void demo() {
			S toSave = this.repository.createSession(); (2)
			// ...
			toSave.setMaxInactiveInterval(Duration.ofSeconds(30)); (3)

			this.repository.save(toSave); (4)

			S session = this.repository.findById(toSave.getId()); (5)
			// ...
		}

		// ... setter methods ...

	}
	// end::expire-repository-demo[]

	@Test
	void newRedisSessionRepository() {
		// tag::new-redissessionrepository[]
		RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();

		// ... configure redisTemplate ...

		SessionRepository<? extends Session> repository = new RedisSessionRepository(redisTemplate);
		// end::new-redissessionrepository[]
	}

	@Test
	void newRedisIndexedSessionRepository() {
		// tag::new-redisindexedsessionrepository[]
		RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();

		// ... configure redisTemplate ...

		SessionRepository<? extends Session> repository = new RedisIndexedSessionRepository(redisTemplate);
		// end::new-redisindexedsessionrepository[]
	}

	@Test
	void newReactiveRedisSessionRepository() {
		LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory();
		RedisSerializationContext<String, Object> serializationContext = RedisSerializationContext
			.<String, Object>newSerializationContext(new JdkSerializationRedisSerializer())
			.build();

		// tag::new-reactiveredissessionrepository[]
		// ... create and configure connectionFactory and serializationContext ...

		ReactiveRedisTemplate<String, Object> redisTemplate = new ReactiveRedisTemplate<>(connectionFactory,
				serializationContext);

		ReactiveSessionRepository<? extends Session> repository = new ReactiveRedisSessionRepository(redisTemplate);
		// end::new-reactiveredissessionrepository[]
	}

	@Test
	void mapRepository() {
		// tag::new-mapsessionrepository[]
		SessionRepository<? extends Session> repository = new MapSessionRepository(new ConcurrentHashMap<>());
		// end::new-mapsessionrepository[]
	}

	@Test
	void newJdbcIndexedSessionRepository() {
		// tag::new-jdbcindexedsessionrepository[]
		JdbcTemplate jdbcTemplate = new JdbcTemplate();

		// ... configure jdbcTemplate ...

		TransactionTemplate transactionTemplate = new TransactionTemplate();

		// ... configure transactionTemplate ...

		SessionRepository<? extends Session> repository = new JdbcIndexedSessionRepository(jdbcTemplate,
				transactionTemplate);
		// end::new-jdbcindexedsessionrepository[]
	}

	@Test
	void newHazelcastIndexedSessionRepository() {
		// tag::new-hazelcastindexedsessionrepository[]

		Config config = new Config();

		// ... configure Hazelcast ...

		HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance(config);

		HazelcastIndexedSessionRepository repository = new HazelcastIndexedSessionRepository(hazelcastInstance);
		// end::new-hazelcastindexedsessionrepository[]
	}

	@Test
	void runSpringHttpSessionConfig() {
		AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
		context.register(SpringHttpSessionConfig.class);
		context.setServletContext(new MockServletContext());
		context.refresh();

		try {
			context.getBean(SessionRepositoryFilter.class);
		}
		finally {
			context.close();
		}
	}

	private static final class User {

		private User(String username) {
		}

	}

}
1 我们创建一个 SessionRepository 实例与泛型类型 S,它扩展了 Session。泛型类型在我们的类中定义。
2 我们使用 SessionRepository 创建一个新的 Session,并将其分配给类型为 S 的变量。
3 我们与`Session`交互。在示例中,我们演示了可在`Session`过期之前更新其处于非活动状态的时间量。
4 我们现在保存`Session`。这就是我们为何需要泛型类型`S`的原因。`SessionRepository`仅允许保存使用相同的`SessionRepository`创建或检索的`Session`实例。这允许`SessionRepository`进行特定于实现的优化(即仅写入已更改的属性)。当保存`Session`时,最后一次访问时间会自动更新。
5 从`SessionRepository`检索`Session`。如果`Session`已过期,结果将为 null。

Using SessionRepository

SessionRepository 负责创建、检索和持久化 Session 实例。

如果可能,您不应直接与 SessionRepositorySession 进行交互。相反,开发人员应更倾向于通过 HttpSessionWebSocket 集成间接与 SessionRepositorySession 进行交互。

Using FindByIndexNameSessionRepository

Spring Session 用于使用 Session 的最基本 API 是 SessionRepository。此 API 故意非常简单,以便你可以轻松地提供具有基本功能的其他实现。

某些 SessionRepository 实现还可能选择实现 FindByIndexNameSessionRepository。例如,Spring 的 Redis、JDBC 和 Hazelcast 支持库都实现了 FindByIndexNameSessionRepository

FindByIndexNameSessionRepository 提供了一个方法来查找具有给定索引名称和索引值的所有会话。作为所有提供的 FindByIndexNameSessionRepository 实现都支持的常见用例,你可以使用一个方便的方法来查找特定用户的会话。这通过确保名称为 FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME 的会话属性填充了用户名来完成。确保属性已填充是你的责任,因为 Spring Session 不知道正在使用的认证机制。以下清单中演示了如何使用此功能的示例:

/*
 * Copyright 2014-2022 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package docs;

import java.util.Map;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.Session;

/**
 * @author Rob Winch
 *
 */
@ExtendWith(MockitoExtension.class)
class FindByIndexNameSessionRepositoryTests {

	@Mock
	FindByIndexNameSessionRepository<Session> sessionRepository;

	@Mock
	Session session;

	@Test
	void setUsername() {
		// tag::set-username[]
		String username = "username";
		this.session.setAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, username);
		// end::set-username[]
	}

	@Test
	void findByUsername() {
		// tag::findby-username[]
		String username = "username";
		Map<String, Session> sessionIdToSession = this.sessionRepository.findByPrincipalName(username);
		// end::findby-username[]
	}

}

FindByIndexNameSessionRepository 的一些实现提供钩子来自动索引其他会话属性。例如,许多实现自动确保当前 Spring Security 用户名使用 FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME 的索引名称进行索引。

对会话编制索引后,你可以使用类似于以下代码找到会话:

/*
 * Copyright 2014-2022 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package docs;

import java.util.Map;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.Session;

/**
 * @author Rob Winch
 *
 */
@ExtendWith(MockitoExtension.class)
class FindByIndexNameSessionRepositoryTests {

	@Mock
	FindByIndexNameSessionRepository<Session> sessionRepository;

	@Mock
	Session session;

	@Test
	void setUsername() {
		// tag::set-username[]
		String username = "username";
		this.session.setAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, username);
		// end::set-username[]
	}

	@Test
	void findByUsername() {
		// tag::findby-username[]
		String username = "username";
		Map<String, Session> sessionIdToSession = this.sessionRepository.findByPrincipalName(username);
		// end::findby-username[]
	}

}

Using ReactiveSessionRepository

ReactiveSessionRepository 负责以非阻塞和反应式的方式创建、检索和持久化 Session 实例。

如果可能,您不应直接与 ReactiveSessionRepositorySession 进行交互。相反,您应更倾向于通过 WebSession 集成间接与 ReactiveSessionRepositorySession 进行交互。

Using @EnableSpringHttpSession

你可以将 @EnableSpringHttpSession 注解添加到 @Configuration 类中,以将 SessionRepositoryFilter 公开为名为 springSessionRepositoryFilter 的 bean。要使用此注解,你必须提供一个 SessionRepository bean。以下示例演示了如何操作:

/*
 * Copyright 2014-2019 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package docs;

import java.util.concurrent.ConcurrentHashMap;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.MapSessionRepository;
import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession;

// tag::class[]
@EnableSpringHttpSession
@Configuration
public class SpringHttpSessionConfig {

	@Bean
	public MapSessionRepository sessionRepository() {
		return new MapSessionRepository(new ConcurrentHashMap<>());
	}

}
// end::class[]

请注意,为你配置的会话过期没有基础设施。这是因为诸如会话过期等事项高度依赖于实现方式。这意味着,如果你需要清理过期的会话,则负责清理过期的会话。

Using @EnableSpringWebSession

你可以将 @EnableSpringWebSession 注解添加到 @Configuration 类中,以将 WebSessionManager 公开为名为 webSessionManager 的 bean。要使用此注解,你必须提供一个 ReactiveSessionRepository bean。以下示例演示了如何操作:

/*
 * Copyright 2014-2022 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package docs;

import java.util.concurrent.ConcurrentHashMap;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.ReactiveMapSessionRepository;
import org.springframework.session.ReactiveSessionRepository;
import org.springframework.session.config.annotation.web.server.EnableSpringWebSession;

// tag::class[]
@Configuration(proxyBeanMethods = false)
@EnableSpringWebSession
public class SpringWebSessionConfig {

	@Bean
	public ReactiveSessionRepository reactiveSessionRepository() {
		return new ReactiveMapSessionRepository(new ConcurrentHashMap<>());
	}

}
// end::class[]

请注意,为你配置的会话过期没有基础设施。这是因为诸如会话过期等事项高度依赖于实现方式。这意味着,如果你需要清理过期的会话,则负责清理过期的会话。

Using RedisSessionRepository

RedisSessionRepository 是一个 SessionRepository,通过使用 Spring Data 的 RedisOperations 实现。在 Web 环境中,此实现通常与 SessionRepositoryFilter 结合使用。请注意,此实现不支持会话事件的发布。

Instantiating a RedisSessionRepository

以下列表中展示了如何创建新实例的典型示例:

/*
 * Copyright 2014-2023 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package docs;

import java.time.Duration;
import java.util.concurrent.ConcurrentHashMap;

import com.hazelcast.config.Config;
import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;
import org.junit.jupiter.api.Test;

import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.mock.web.MockServletContext;
import org.springframework.session.MapSession;
import org.springframework.session.MapSessionRepository;
import org.springframework.session.ReactiveSessionRepository;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.session.data.redis.ReactiveRedisSessionRepository;
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
import org.springframework.session.data.redis.RedisSessionRepository;
import org.springframework.session.hazelcast.HazelcastIndexedSessionRepository;
import org.springframework.session.jdbc.JdbcIndexedSessionRepository;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;

import static org.assertj.core.api.Assertions.assertThat;

/**
 * @author Rob Winch
 * @author Vedran Pavic
 */
class IndexDocTests {

	private static final String ATTR_USER = "user";

	@Test
	void repositoryDemo() {
		RepositoryDemo<MapSession> demo = new RepositoryDemo<>();
		demo.repository = new MapSessionRepository(new ConcurrentHashMap<>());

		demo.demo();
	}

	// tag::repository-demo[]
	public class RepositoryDemo<S extends Session> {

		private SessionRepository<S> repository; (1)

		public void demo() {
			S toSave = this.repository.createSession(); (2)

			(3)
			User rwinch = new User("rwinch");
			toSave.setAttribute(ATTR_USER, rwinch);

			this.repository.save(toSave); (4)

			S session = this.repository.findById(toSave.getId()); (5)

			(6)
			User user = session.getAttribute(ATTR_USER);
			assertThat(user).isEqualTo(rwinch);
		}

		// ... setter methods ...

	}
	// end::repository-demo[]

	@Test
	void expireRepositoryDemo() {
		ExpiringRepositoryDemo<MapSession> demo = new ExpiringRepositoryDemo<>();
		demo.repository = new MapSessionRepository(new ConcurrentHashMap<>());

		demo.demo();
	}

	// tag::expire-repository-demo[]
	public class ExpiringRepositoryDemo<S extends Session> {

		private SessionRepository<S> repository; (1)

		public void demo() {
			S toSave = this.repository.createSession(); (2)
			// ...
			toSave.setMaxInactiveInterval(Duration.ofSeconds(30)); (3)

			this.repository.save(toSave); (4)

			S session = this.repository.findById(toSave.getId()); (5)
			// ...
		}

		// ... setter methods ...

	}
	// end::expire-repository-demo[]

	@Test
	void newRedisSessionRepository() {
		// tag::new-redissessionrepository[]
		RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();

		// ... configure redisTemplate ...

		SessionRepository<? extends Session> repository = new RedisSessionRepository(redisTemplate);
		// end::new-redissessionrepository[]
	}

	@Test
	void newRedisIndexedSessionRepository() {
		// tag::new-redisindexedsessionrepository[]
		RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();

		// ... configure redisTemplate ...

		SessionRepository<? extends Session> repository = new RedisIndexedSessionRepository(redisTemplate);
		// end::new-redisindexedsessionrepository[]
	}

	@Test
	void newReactiveRedisSessionRepository() {
		LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory();
		RedisSerializationContext<String, Object> serializationContext = RedisSerializationContext
			.<String, Object>newSerializationContext(new JdkSerializationRedisSerializer())
			.build();

		// tag::new-reactiveredissessionrepository[]
		// ... create and configure connectionFactory and serializationContext ...

		ReactiveRedisTemplate<String, Object> redisTemplate = new ReactiveRedisTemplate<>(connectionFactory,
				serializationContext);

		ReactiveSessionRepository<? extends Session> repository = new ReactiveRedisSessionRepository(redisTemplate);
		// end::new-reactiveredissessionrepository[]
	}

	@Test
	void mapRepository() {
		// tag::new-mapsessionrepository[]
		SessionRepository<? extends Session> repository = new MapSessionRepository(new ConcurrentHashMap<>());
		// end::new-mapsessionrepository[]
	}

	@Test
	void newJdbcIndexedSessionRepository() {
		// tag::new-jdbcindexedsessionrepository[]
		JdbcTemplate jdbcTemplate = new JdbcTemplate();

		// ... configure jdbcTemplate ...

		TransactionTemplate transactionTemplate = new TransactionTemplate();

		// ... configure transactionTemplate ...

		SessionRepository<? extends Session> repository = new JdbcIndexedSessionRepository(jdbcTemplate,
				transactionTemplate);
		// end::new-jdbcindexedsessionrepository[]
	}

	@Test
	void newHazelcastIndexedSessionRepository() {
		// tag::new-hazelcastindexedsessionrepository[]

		Config config = new Config();

		// ... configure Hazelcast ...

		HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance(config);

		HazelcastIndexedSessionRepository repository = new HazelcastIndexedSessionRepository(hazelcastInstance);
		// end::new-hazelcastindexedsessionrepository[]
	}

	@Test
	void runSpringHttpSessionConfig() {
		AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
		context.register(SpringHttpSessionConfig.class);
		context.setServletContext(new MockServletContext());
		context.refresh();

		try {
			context.getBean(SessionRepositoryFilter.class);
		}
		finally {
			context.close();
		}
	}

	private static final class User {

		private User(String username) {
		}

	}

}

有关如何创建 RedisConnectionFactory 的更多信息,请参阅 Spring Data Redis 参考。

Using @EnableRedisHttpSession

在 Web 环境中,创建新的 RedisSessionRepository 的最简单方法是使用 @EnableRedisHttpSession。您可以在 Samples and Guides (Start Here) 中找到完整的示例用法。您可以使用以下属性来自定义配置:

enableIndexingAndEvents* enableIndexingAndEvents:是否使用 RedisIndexedSessionRepository 而不是 RedisSessionRepository。默认值是 false。* maxInactiveIntervalInSeconds:会话过期之前的时间,以秒为单位。* redisNamespace:允许为会话配置特定于应用程序的命名空间。Redis 密钥和信道 ID 以 <redisNamespace>: 的前缀开头。* flushMode:允许指定何时将数据写入 Redis。默认值仅在 SessionRepository 中调用 save 时写入。FlushMode.IMMEDIATE 值会尽快写入 Redis。

Custom RedisSerializer

您可以通过创建一个实现 RedisSerializer<Object> 的名为 springSessionDefaultRedisSerializer 的 bean 来定制序列化。

Viewing the Session in Redis

installing redis-cli 之后,你可以检查 Redis using the redis-cli 中的值。例如,可以再一个终端窗口中输入以下命令:redis-cli HGETALL spring:session:sessions:1

$ redis-cli
redis 127.0.0.1:6379> keys *
1) "spring:session:sessions:4fc39ce3-63b3-4e17-b1c4-5e1ed96fb021" 1
1 此密钥的后缀是 Spring 会话的会话标识符。

您还可以通过使用 hkeys 命令来查看每个会话的属性。以下示例显示了如何执行此操作:

redis 127.0.0.1:6379> hkeys spring:session:sessions:4fc39ce3-63b3-4e17-b1c4-5e1ed96fb021
1) "lastAccessedTime"
2) "creationTime"
3) "maxInactiveInterval"
4) "sessionAttr:username"
redis 127.0.0.1:6379> hget spring:session:sessions:4fc39ce3-63b3-4e17-b1c4-5e1ed96fb021 sessionAttr:username
"\xac\xed\x00\x05t\x00\x03rob"

Using RedisIndexedSessionRepository

RedisIndexedSessionRepository 是通过使用 Spring Data 的 RedisOperations 实现的 SessionRepository。在 Web 环境中,此通常与 SessionRepositoryFilter 结合使用。该实现通过 SessionMessageListener 支持 SessionDestroyedEventSessionCreatedEvent

Instantiating a RedisIndexedSessionRepository

以下列表中展示了如何创建新实例的典型示例:

/*
 * Copyright 2014-2023 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package docs;

import java.time.Duration;
import java.util.concurrent.ConcurrentHashMap;

import com.hazelcast.config.Config;
import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;
import org.junit.jupiter.api.Test;

import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.mock.web.MockServletContext;
import org.springframework.session.MapSession;
import org.springframework.session.MapSessionRepository;
import org.springframework.session.ReactiveSessionRepository;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.session.data.redis.ReactiveRedisSessionRepository;
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
import org.springframework.session.data.redis.RedisSessionRepository;
import org.springframework.session.hazelcast.HazelcastIndexedSessionRepository;
import org.springframework.session.jdbc.JdbcIndexedSessionRepository;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;

import static org.assertj.core.api.Assertions.assertThat;

/**
 * @author Rob Winch
 * @author Vedran Pavic
 */
class IndexDocTests {

	private static final String ATTR_USER = "user";

	@Test
	void repositoryDemo() {
		RepositoryDemo<MapSession> demo = new RepositoryDemo<>();
		demo.repository = new MapSessionRepository(new ConcurrentHashMap<>());

		demo.demo();
	}

	// tag::repository-demo[]
	public class RepositoryDemo<S extends Session> {

		private SessionRepository<S> repository; (1)

		public void demo() {
			S toSave = this.repository.createSession(); (2)

			(3)
			User rwinch = new User("rwinch");
			toSave.setAttribute(ATTR_USER, rwinch);

			this.repository.save(toSave); (4)

			S session = this.repository.findById(toSave.getId()); (5)

			(6)
			User user = session.getAttribute(ATTR_USER);
			assertThat(user).isEqualTo(rwinch);
		}

		// ... setter methods ...

	}
	// end::repository-demo[]

	@Test
	void expireRepositoryDemo() {
		ExpiringRepositoryDemo<MapSession> demo = new ExpiringRepositoryDemo<>();
		demo.repository = new MapSessionRepository(new ConcurrentHashMap<>());

		demo.demo();
	}

	// tag::expire-repository-demo[]
	public class ExpiringRepositoryDemo<S extends Session> {

		private SessionRepository<S> repository; (1)

		public void demo() {
			S toSave = this.repository.createSession(); (2)
			// ...
			toSave.setMaxInactiveInterval(Duration.ofSeconds(30)); (3)

			this.repository.save(toSave); (4)

			S session = this.repository.findById(toSave.getId()); (5)
			// ...
		}

		// ... setter methods ...

	}
	// end::expire-repository-demo[]

	@Test
	void newRedisSessionRepository() {
		// tag::new-redissessionrepository[]
		RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();

		// ... configure redisTemplate ...

		SessionRepository<? extends Session> repository = new RedisSessionRepository(redisTemplate);
		// end::new-redissessionrepository[]
	}

	@Test
	void newRedisIndexedSessionRepository() {
		// tag::new-redisindexedsessionrepository[]
		RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();

		// ... configure redisTemplate ...

		SessionRepository<? extends Session> repository = new RedisIndexedSessionRepository(redisTemplate);
		// end::new-redisindexedsessionrepository[]
	}

	@Test
	void newReactiveRedisSessionRepository() {
		LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory();
		RedisSerializationContext<String, Object> serializationContext = RedisSerializationContext
			.<String, Object>newSerializationContext(new JdkSerializationRedisSerializer())
			.build();

		// tag::new-reactiveredissessionrepository[]
		// ... create and configure connectionFactory and serializationContext ...

		ReactiveRedisTemplate<String, Object> redisTemplate = new ReactiveRedisTemplate<>(connectionFactory,
				serializationContext);

		ReactiveSessionRepository<? extends Session> repository = new ReactiveRedisSessionRepository(redisTemplate);
		// end::new-reactiveredissessionrepository[]
	}

	@Test
	void mapRepository() {
		// tag::new-mapsessionrepository[]
		SessionRepository<? extends Session> repository = new MapSessionRepository(new ConcurrentHashMap<>());
		// end::new-mapsessionrepository[]
	}

	@Test
	void newJdbcIndexedSessionRepository() {
		// tag::new-jdbcindexedsessionrepository[]
		JdbcTemplate jdbcTemplate = new JdbcTemplate();

		// ... configure jdbcTemplate ...

		TransactionTemplate transactionTemplate = new TransactionTemplate();

		// ... configure transactionTemplate ...

		SessionRepository<? extends Session> repository = new JdbcIndexedSessionRepository(jdbcTemplate,
				transactionTemplate);
		// end::new-jdbcindexedsessionrepository[]
	}

	@Test
	void newHazelcastIndexedSessionRepository() {
		// tag::new-hazelcastindexedsessionrepository[]

		Config config = new Config();

		// ... configure Hazelcast ...

		HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance(config);

		HazelcastIndexedSessionRepository repository = new HazelcastIndexedSessionRepository(hazelcastInstance);
		// end::new-hazelcastindexedsessionrepository[]
	}

	@Test
	void runSpringHttpSessionConfig() {
		AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
		context.register(SpringHttpSessionConfig.class);
		context.setServletContext(new MockServletContext());
		context.refresh();

		try {
			context.getBean(SessionRepositoryFilter.class);
		}
		finally {
			context.close();
		}
	}

	private static final class User {

		private User(String username) {
		}

	}

}

有关如何创建 RedisConnectionFactory 的更多信息,请参阅 Spring Data Redis 参考。

Using @EnableRedisHttpSession(enableIndexingAndEvents = true)

在 Web 环境中,创建新的 RedisIndexedSessionRepository 的最简单方法是使用 @EnableRedisHttpSession(enableIndexingAndEvents = true)。您可以在 Samples and Guides (Start Here) 中找到完整的示例用法。您可以使用以下属性来自定义配置:

  • enableIndexingAndEvents: 是否使用`RedisIndexedSessionRepository`而不是`RedisSessionRepository`。默认值为`false`。

  • maxInactiveIntervalInSeconds: 会话过期的时间量(以秒为单位)。

  • redisNamespace: 允许为会话配置特定于应用程序的命名空间。Redis 密钥和通道 ID 以`&lt;redisNamespace&gt;:`的前缀开头。

  • flushMode: 允许指定何时将数据写入 Redis。默认值为仅当在`SessionRepository`上调用`save`时。值为`FlushMode.IMMEDIATE`尽可能快地写入 Redis。

Custom RedisSerializer

您可以通过创建一个实现 RedisSerializer<Object> 的名为 springSessionDefaultRedisSerializer 的 bean 来定制序列化。

Redis TaskExecutor

RedisIndexedSessionRepository 订阅以使用 RedisMessageListenerContainer 从 Redis 接收事件。你可以创建名为 springSessionRedisTaskExecutor 的 bean、bean springSessionRedisSubscriptionExecutor 或两者来自定义那些事件被调度的方式。更多有关配置 Redis 任务执行器的详细信息,请点按此处查看 {docs-url}/spring-data-redis/docs/{spring-data-redis-version}/reference/html/#redis:pubsub:subscribe:containers[here]。

Storage Details

以下部分概述了 Redis 如何针对每个操作进行更新。以下示例显示了创建新会话的示例:

HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe creationTime 1404360000000 \
	maxInactiveInterval 1800 \
	lastAccessedTime 1404360000000 \
	sessionAttr:attrName someAttrValue \
	sessionAttr:attrName2 someAttrValue2
EXPIRE spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe 2100
APPEND spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe ""
EXPIRE spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe 1800
SADD spring:session:expirations:1439245080000 expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe
EXPIRE spring:session:expirations1439245080000 2100

后续部分描述了详细信息。

Saving a Session

每个会话都作为一个 Hash 存储在 Redis 中。每个会话都使用 HMSET 命令进行设置和更新。以下示例展示了每个会话是如何存储的:

HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe creationTime 1404360000000 \
	maxInactiveInterval 1800 \
	lastAccessedTime 1404360000000 \
	sessionAttr:attrName someAttrValue \
	sessionAttr:attrName2 someAttrValue2

在前面的示例中,关于会话存在以下语句:

  • 会话 ID 为 33fdd1b6-b496-4b33-9f7d-df96679d32fe。

  • 会话创建于 1404360000000(从 1/1/1970 GMT 的午夜开始的毫秒数)。

  • 会话在 1800 秒(30 分钟)后过期。

  • 会话最后一次访问时间为 1404360000000(从 1/1/1970 GMT 的午夜开始的毫秒数)。

  • 会话有两个属性。第一个是`attrName`,值是`someAttrValue`。第二个会话属性名为`attrName2`,值是`someAttrValue2`。

Optimized Writes

RedisIndexedSessionRepository 管理的 Session 实例跟踪改变的属性,只更新那些属性。这意味着如果一个属性写一次并读多次,我们只需要写一次该属性。例如,假设前一部分的 lsiting 中的 attrName2 会话属性被更新。在保存时将运行以下命令:

HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe sessionAttr:attrName2 newValue

Session Expiration

一个过期时间使用 EXPIRE 命令与每个会话相关联,根据 Session.getMaxInactiveInterval() 设置。以下示例显示了一个典型的 EXPIRE 命令:

EXPIRE spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe 2100

请注意,在会话实际过期后的五分钟设置为过期时间。这是必要的,以便在会话过期时可以访问会话的值。一个过期时间在会话本身在实际过期后的五分钟设置,以确保它被清除,但仅在我们执行任何必要处理之后。

SessionRepository.findById(String) 方法确保不返回任何过期的会话。这意味着您在使用会话之前不需要检查是否已过期。

Spring Session 依赖 Redis 中的删除和已过期的 keyspace notifications 来触发 <<`SessionDeletedEvent`,api-redisindexedsessionrepository-sessiondestroyedevent>> 和 <<`SessionExpiredEvent`,api-redisindexedsessionrepository-sessiondestroyedevent>>。SessionDeletedEventSessionExpiredEvent 确保与 Session 相关联的资源已被清除。例如,当你使用 Spring Session 的 WebSocket 支持时,Redis 过期或删除事件会触发与会话相关联的任何 WebSocket 连接关闭。

过期时间没有直接跟踪在会话键本身上,因为这意味着会话数据将不可用。相反,使用了一个特殊会话过期时间键。在前一个示例中,过期时间键如下所示:

APPEND spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe ""
EXPIRE spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe 1800

当会话过期时间键被删除或过期时,键空间通知触发对实际会话的查找,并触发 SessionDestroyedEvent

仅仅依赖 Redis 过期有一个问题,如果未访问过该键,Redis 将不保证在何时触发过期事件。具体来说,Redis 用于清除过期键的后台任务是一个低优先级任务,并且可能不会触发键过期。更多详细信息,请参阅 Redis 文档中的 Timing of Expired Events 部分。

为了规避无法保证发生到期事件的情况,我们可以确保在预期到期时访问每个键。这意味着,如果该键上的 TTL 已过期,则当我们尝试访问该键时,Redis 会删除该键并触发到期事件。

出于此原因,还会将每个会话到期时间跟踪到最近的分钟。这允许一个后台任务访问可能已过期的会话,以确保 Redis 到期事件以更确定方式触发。以下示例展示了这些事件:

SADD spring:session:expirations:1439245080000 expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe
EXPIRE spring:session:expirations1439245080000 2100

然后,后台任务将这些映射用于显式请求每个键。通过访问键而不是删除它,我们可以确保 Redis 仅在 TTL 过期时为我们删除该键。

我们没有明确删除这些键,因为在某些情况下,当键没有过期时,可能会存在错误地将键标识为过期的竞争条件。除了使用分布式锁(这会降低我们的性能)外,没有办法确保过期映射的一致性。通过简单地访问密钥,我们确保仅在该密钥上的 TTL 过期时才移除该密钥。

SessionDeletedEvent and SessionExpiredEvent

SessionDeletedEventSessionExpiredEvent 都是 SessionDestroyedEvent 的类型。

RedisIndexedSessionRepository 支持在删除 Session 时触发 SessionDeletedEvent,或在 Session 过期时触发 SessionExpiredEvent。这对于确保与 Session 关联的资源得到妥善清理是必要的。

例如,在与 WebSocket 集成时,SessionDestroyedEvent 负责关闭任何活动的 WebSocket 连接。

通过监听 Redis Keyspace eventsSessionMessageListener 提供触发 SessionDeletedEventSessionExpiredEvent 的功能。为了使其工作,需要启用 Generic 命令和 Expired 事件的 Redis Keyspace 事件。以下示例演示如何执行此操作:

redis-cli config set notify-keyspace-events Egx

如果你使用 @EnableRedisHttpSession(enableIndexingAndEvents = true),则将自动管理 SessionMessageListener 并启用必要的 Redis 密钥空间事件。但是,在受保护的 Redis 环境中,将禁用 config 命令。这意味着 Spring Session 无法为你配置 Redis 密钥空间事件。要禁用自动配置,可添加 ConfigureRedisAction.NO_OP 作为 bean。

例如,使用 Java 配置时,你可以使用以下内容:

/*
 * Copyright 2014-2022 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package docs;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.SubscriptionListener;
import org.springframework.session.data.redis.config.ConfigureRedisAction;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willAnswer;
import static org.mockito.Mockito.mock;

/**
 * @author Rob Winch
 */
@ExtendWith(SpringExtension.class)
@ContextConfiguration
@WebAppConfiguration
class RedisHttpSessionConfigurationNoOpConfigureRedisActionTests {

	@Test
	void redisConnectionFactoryNotUsedSinceNoValidation() {
	}

	@EnableRedisHttpSession
	@Configuration
	static class Config {

		// tag::configure-redis-action[]
		@Bean
		ConfigureRedisAction configureRedisAction() {
			return ConfigureRedisAction.NO_OP;
		}
		// end::configure-redis-action[]

		@Bean
		RedisConnectionFactory redisConnectionFactory() {
			RedisConnectionFactory connectionFactoryMock = mock(RedisConnectionFactory.class);
			RedisConnection connectionMock = mock(RedisConnection.class);
			given(connectionFactoryMock.getConnection()).willReturn(connectionMock);

			willAnswer((it) -> {
				SubscriptionListener listener = it.getArgument(0);
				listener.onPatternSubscribed(it.getArgument(1), 0);
				listener.onChannelSubscribed("__keyevent@0__:del".getBytes(), 0);
				listener.onChannelSubscribed("__keyevent@0__:expired".getBytes(), 0);

				return null;
			}).given(connectionMock).pSubscribe(any(), any());

			return connectionFactoryMock;
		}

	}

}

在 XML 配置中,你可以使用以下内容:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:p="http://www.springframework.org/schema/p"
	xmlns:util="http://www.springframework.org/schema/util"
	xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd
		http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util-4.1.xsd">

	<context:annotation-config/>

	<bean class="org.springframework.session.data.redis.config.annotation.web.http.RedisIndexedHttpSessionConfiguration"/>

	<!-- tag::configure-redis-action[] -->
	<util:constant
		static-field="org.springframework.session.data.redis.config.ConfigureRedisAction.NO_OP"/>
	<!-- end::configure-redis-action[] -->

	<bean class="docs.HttpSessionConfigurationNoOpConfigureRedisActionXmlTests"
		factory-method="connectionFactory"/>
</beans>

Using SessionCreatedEvent

创建会话后,将向 Redis 发送一个事件,其通道 ID 为 spring:session:channel:created:33fdd1b6-b496-4b33-9f7d-df96679d32fe,其中 33fdd1b6-b496-4b33-9f7d-df96679d32fe 是会话 ID。事件正文是已创建的会话。

如果注册为 MessageListener(默认),则 RedisIndexedSessionRepository 随后将 Redis 消息转换为 SessionCreatedEvent

Viewing the Session in Redis

installing redis-cli 之后,你可以检查 Redis using the redis-cli 中的值。例如,你可以在一个终端中输入以下内容:redis-cli HGETALL spring:session:events:1

$ redis-cli
redis 127.0.0.1:6379> keys *
1) "spring:session:sessions:4fc39ce3-63b3-4e17-b1c4-5e1ed96fb021" 1
2) "spring:session:expirations:1418772300000" 2
1 此密钥的后缀是 Spring 会话的会话标识符。
2 此密钥包含所有应在时间`1418772300000`删除的会话 ID。

你还可以查看每个会话的属性。以下示例展示了如何执行此操作:

redis 127.0.0.1:6379> hkeys spring:session:sessions:4fc39ce3-63b3-4e17-b1c4-5e1ed96fb021
1) "lastAccessedTime"
2) "creationTime"
3) "maxInactiveInterval"
4) "sessionAttr:username"
redis 127.0.0.1:6379> hget spring:session:sessions:4fc39ce3-63b3-4e17-b1c4-5e1ed96fb021 sessionAttr:username
"\xac\xed\x00\x05t\x00\x03rob"

Using ReactiveRedisSessionRepository

ReactiveRedisSessionRepository 是使用 Spring Data 的 ReactiveRedisOperations 实现的 ReactiveSessionRepository。在 Web 环境中,这通常与 WebSessionStore 一起使用。

Instantiating a ReactiveRedisSessionRepository

以下示例展示了如何创建新实例:

/*
 * Copyright 2014-2023 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package docs;

import java.time.Duration;
import java.util.concurrent.ConcurrentHashMap;

import com.hazelcast.config.Config;
import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;
import org.junit.jupiter.api.Test;

import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.mock.web.MockServletContext;
import org.springframework.session.MapSession;
import org.springframework.session.MapSessionRepository;
import org.springframework.session.ReactiveSessionRepository;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.session.data.redis.ReactiveRedisSessionRepository;
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
import org.springframework.session.data.redis.RedisSessionRepository;
import org.springframework.session.hazelcast.HazelcastIndexedSessionRepository;
import org.springframework.session.jdbc.JdbcIndexedSessionRepository;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;

import static org.assertj.core.api.Assertions.assertThat;

/**
 * @author Rob Winch
 * @author Vedran Pavic
 */
class IndexDocTests {

	private static final String ATTR_USER = "user";

	@Test
	void repositoryDemo() {
		RepositoryDemo<MapSession> demo = new RepositoryDemo<>();
		demo.repository = new MapSessionRepository(new ConcurrentHashMap<>());

		demo.demo();
	}

	// tag::repository-demo[]
	public class RepositoryDemo<S extends Session> {

		private SessionRepository<S> repository; (1)

		public void demo() {
			S toSave = this.repository.createSession(); (2)

			(3)
			User rwinch = new User("rwinch");
			toSave.setAttribute(ATTR_USER, rwinch);

			this.repository.save(toSave); (4)

			S session = this.repository.findById(toSave.getId()); (5)

			(6)
			User user = session.getAttribute(ATTR_USER);
			assertThat(user).isEqualTo(rwinch);
		}

		// ... setter methods ...

	}
	// end::repository-demo[]

	@Test
	void expireRepositoryDemo() {
		ExpiringRepositoryDemo<MapSession> demo = new ExpiringRepositoryDemo<>();
		demo.repository = new MapSessionRepository(new ConcurrentHashMap<>());

		demo.demo();
	}

	// tag::expire-repository-demo[]
	public class ExpiringRepositoryDemo<S extends Session> {

		private SessionRepository<S> repository; (1)

		public void demo() {
			S toSave = this.repository.createSession(); (2)
			// ...
			toSave.setMaxInactiveInterval(Duration.ofSeconds(30)); (3)

			this.repository.save(toSave); (4)

			S session = this.repository.findById(toSave.getId()); (5)
			// ...
		}

		// ... setter methods ...

	}
	// end::expire-repository-demo[]

	@Test
	void newRedisSessionRepository() {
		// tag::new-redissessionrepository[]
		RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();

		// ... configure redisTemplate ...

		SessionRepository<? extends Session> repository = new RedisSessionRepository(redisTemplate);
		// end::new-redissessionrepository[]
	}

	@Test
	void newRedisIndexedSessionRepository() {
		// tag::new-redisindexedsessionrepository[]
		RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();

		// ... configure redisTemplate ...

		SessionRepository<? extends Session> repository = new RedisIndexedSessionRepository(redisTemplate);
		// end::new-redisindexedsessionrepository[]
	}

	@Test
	void newReactiveRedisSessionRepository() {
		LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory();
		RedisSerializationContext<String, Object> serializationContext = RedisSerializationContext
			.<String, Object>newSerializationContext(new JdkSerializationRedisSerializer())
			.build();

		// tag::new-reactiveredissessionrepository[]
		// ... create and configure connectionFactory and serializationContext ...

		ReactiveRedisTemplate<String, Object> redisTemplate = new ReactiveRedisTemplate<>(connectionFactory,
				serializationContext);

		ReactiveSessionRepository<? extends Session> repository = new ReactiveRedisSessionRepository(redisTemplate);
		// end::new-reactiveredissessionrepository[]
	}

	@Test
	void mapRepository() {
		// tag::new-mapsessionrepository[]
		SessionRepository<? extends Session> repository = new MapSessionRepository(new ConcurrentHashMap<>());
		// end::new-mapsessionrepository[]
	}

	@Test
	void newJdbcIndexedSessionRepository() {
		// tag::new-jdbcindexedsessionrepository[]
		JdbcTemplate jdbcTemplate = new JdbcTemplate();

		// ... configure jdbcTemplate ...

		TransactionTemplate transactionTemplate = new TransactionTemplate();

		// ... configure transactionTemplate ...

		SessionRepository<? extends Session> repository = new JdbcIndexedSessionRepository(jdbcTemplate,
				transactionTemplate);
		// end::new-jdbcindexedsessionrepository[]
	}

	@Test
	void newHazelcastIndexedSessionRepository() {
		// tag::new-hazelcastindexedsessionrepository[]

		Config config = new Config();

		// ... configure Hazelcast ...

		HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance(config);

		HazelcastIndexedSessionRepository repository = new HazelcastIndexedSessionRepository(hazelcastInstance);
		// end::new-hazelcastindexedsessionrepository[]
	}

	@Test
	void runSpringHttpSessionConfig() {
		AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
		context.register(SpringHttpSessionConfig.class);
		context.setServletContext(new MockServletContext());
		context.refresh();

		try {
			context.getBean(SessionRepositoryFilter.class);
		}
		finally {
			context.close();
		}
	}

	private static final class User {

		private User(String username) {
		}

	}

}

有关如何创建 ReactiveRedisConnectionFactory 的详细信息,请参见 Spring Data Redis 参考。

Using @EnableRedisWebSession

在 Web 环境中,创建新 ReactiveRedisSessionRepository 的最简单方法是使用 @EnableRedisWebSession。你可以使用以下属性自定义配置:

  • maxInactiveIntervalInSeconds: 会话过期的时间量(以秒为单位)

  • redisNamespace: 允许为会话配置特定于应用程序的命名空间。Redis 密钥和通道 ID 以`&lt;redisNamespace&gt;:`的前缀开头。

  • flushMode: 允许指定何时将数据写入 Redis。默认值为仅当在`ReactiveSessionRepository`上调用`save`时。值为`FlushMode.IMMEDIATE`尽可能快地写入 Redis。

Optimized Writes

ReactiveRedisSessionRepository 管理的 Session 实例会跟踪已更改的属性,并仅更新那些属性。这意味着,如果某个属性已写入一次并读取多次,则我们只需写入该属性一次。

Viewing the Session in Redis

installing redis-cli 之后,你可以检查 Redis using the redis-cli 中的值。例如,可以再一个终端窗口中输入以下命令:redis-cli HGETALL spring:session:sessions:1

$ redis-cli
redis 127.0.0.1:6379> keys *
1) "spring:session:sessions:4fc39ce3-63b3-4e17-b1c4-5e1ed96fb021" 1
1 此密钥的后缀是 Spring 会话的会话标识符。

您还可以通过使用 hkeys 命令来查看每个会话的属性。以下示例显示了如何执行此操作:

redis 127.0.0.1:6379> hkeys spring:session:sessions:4fc39ce3-63b3-4e17-b1c4-5e1ed96fb021
1) "lastAccessedTime"
2) "creationTime"
3) "maxInactiveInterval"
4) "sessionAttr:username"
redis 127.0.0.1:6379> hget spring:session:sessions:4fc39ce3-63b3-4e17-b1c4-5e1ed96fb021 sessionAttr:username
"\xac\xed\x00\x05t\x00\x03rob"

Using MapSessionRepository

MapSessionRepository 允许在 Map 中持久化 Session,密钥为 Session ID,值为 Session。您可以将该实现与 ConcurrentHashMap 一起用作一种测试或便利机制。或者,您也可以将它与分布式 Map 实现结合使用。例如,可以将其与 Hazelcast 一起使用。

Instantiating MapSessionRepository

以下示例展示了如何创建新实例:

/*
 * Copyright 2014-2023 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package docs;

import java.time.Duration;
import java.util.concurrent.ConcurrentHashMap;

import com.hazelcast.config.Config;
import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;
import org.junit.jupiter.api.Test;

import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.mock.web.MockServletContext;
import org.springframework.session.MapSession;
import org.springframework.session.MapSessionRepository;
import org.springframework.session.ReactiveSessionRepository;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.session.data.redis.ReactiveRedisSessionRepository;
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
import org.springframework.session.data.redis.RedisSessionRepository;
import org.springframework.session.hazelcast.HazelcastIndexedSessionRepository;
import org.springframework.session.jdbc.JdbcIndexedSessionRepository;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;

import static org.assertj.core.api.Assertions.assertThat;

/**
 * @author Rob Winch
 * @author Vedran Pavic
 */
class IndexDocTests {

	private static final String ATTR_USER = "user";

	@Test
	void repositoryDemo() {
		RepositoryDemo<MapSession> demo = new RepositoryDemo<>();
		demo.repository = new MapSessionRepository(new ConcurrentHashMap<>());

		demo.demo();
	}

	// tag::repository-demo[]
	public class RepositoryDemo<S extends Session> {

		private SessionRepository<S> repository; (1)

		public void demo() {
			S toSave = this.repository.createSession(); (2)

			(3)
			User rwinch = new User("rwinch");
			toSave.setAttribute(ATTR_USER, rwinch);

			this.repository.save(toSave); (4)

			S session = this.repository.findById(toSave.getId()); (5)

			(6)
			User user = session.getAttribute(ATTR_USER);
			assertThat(user).isEqualTo(rwinch);
		}

		// ... setter methods ...

	}
	// end::repository-demo[]

	@Test
	void expireRepositoryDemo() {
		ExpiringRepositoryDemo<MapSession> demo = new ExpiringRepositoryDemo<>();
		demo.repository = new MapSessionRepository(new ConcurrentHashMap<>());

		demo.demo();
	}

	// tag::expire-repository-demo[]
	public class ExpiringRepositoryDemo<S extends Session> {

		private SessionRepository<S> repository; (1)

		public void demo() {
			S toSave = this.repository.createSession(); (2)
			// ...
			toSave.setMaxInactiveInterval(Duration.ofSeconds(30)); (3)

			this.repository.save(toSave); (4)

			S session = this.repository.findById(toSave.getId()); (5)
			// ...
		}

		// ... setter methods ...

	}
	// end::expire-repository-demo[]

	@Test
	void newRedisSessionRepository() {
		// tag::new-redissessionrepository[]
		RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();

		// ... configure redisTemplate ...

		SessionRepository<? extends Session> repository = new RedisSessionRepository(redisTemplate);
		// end::new-redissessionrepository[]
	}

	@Test
	void newRedisIndexedSessionRepository() {
		// tag::new-redisindexedsessionrepository[]
		RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();

		// ... configure redisTemplate ...

		SessionRepository<? extends Session> repository = new RedisIndexedSessionRepository(redisTemplate);
		// end::new-redisindexedsessionrepository[]
	}

	@Test
	void newReactiveRedisSessionRepository() {
		LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory();
		RedisSerializationContext<String, Object> serializationContext = RedisSerializationContext
			.<String, Object>newSerializationContext(new JdkSerializationRedisSerializer())
			.build();

		// tag::new-reactiveredissessionrepository[]
		// ... create and configure connectionFactory and serializationContext ...

		ReactiveRedisTemplate<String, Object> redisTemplate = new ReactiveRedisTemplate<>(connectionFactory,
				serializationContext);

		ReactiveSessionRepository<? extends Session> repository = new ReactiveRedisSessionRepository(redisTemplate);
		// end::new-reactiveredissessionrepository[]
	}

	@Test
	void mapRepository() {
		// tag::new-mapsessionrepository[]
		SessionRepository<? extends Session> repository = new MapSessionRepository(new ConcurrentHashMap<>());
		// end::new-mapsessionrepository[]
	}

	@Test
	void newJdbcIndexedSessionRepository() {
		// tag::new-jdbcindexedsessionrepository[]
		JdbcTemplate jdbcTemplate = new JdbcTemplate();

		// ... configure jdbcTemplate ...

		TransactionTemplate transactionTemplate = new TransactionTemplate();

		// ... configure transactionTemplate ...

		SessionRepository<? extends Session> repository = new JdbcIndexedSessionRepository(jdbcTemplate,
				transactionTemplate);
		// end::new-jdbcindexedsessionrepository[]
	}

	@Test
	void newHazelcastIndexedSessionRepository() {
		// tag::new-hazelcastindexedsessionrepository[]

		Config config = new Config();

		// ... configure Hazelcast ...

		HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance(config);

		HazelcastIndexedSessionRepository repository = new HazelcastIndexedSessionRepository(hazelcastInstance);
		// end::new-hazelcastindexedsessionrepository[]
	}

	@Test
	void runSpringHttpSessionConfig() {
		AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
		context.register(SpringHttpSessionConfig.class);
		context.setServletContext(new MockServletContext());
		context.refresh();

		try {
			context.getBean(SessionRepositoryFilter.class);
		}
		finally {
			context.close();
		}
	}

	private static final class User {

		private User(String username) {
		}

	}

}

Using Spring Session and Hazlecast

Hazelcast Sample 是一个完整的应用程序,展示如何将 Spring Session 与 Hazelcast 配合使用。

要运行它,请使用以下命令:

	./gradlew :samples:hazelcast:tomcatRun

Hazelcast Spring Sample 是一个完整的应用程序,展示如何将 Spring Session 与 Hazelcast 和 Spring Security 配合使用。

它包括支持触发 SessionCreatedEventSessionDeletedEventSessionExpiredEvent 的示例 Hazelcast MapListener 实现。

要运行它,请使用以下命令:

	./gradlew :samples:hazelcast-spring:tomcatRun

Using ReactiveMapSessionRepository

ReactiveMapSessionRepository 允许在 Map 中持久化 Session,密钥为 Session ID,值为 Session。您可以将该实现与 ConcurrentHashMap 一起用作一种测试或便利机制。或者,您也可以将它与分布式 Map 实现结合使用,但要求所提供的 Map 必须是非阻塞的。

Using JdbcIndexedSessionRepository

JdbcIndexedSessionRepository 是一个 SessionRepository 实现,它使用 Spring 的 JdbcOperations 将会话存储在关系数据库中。在 Web 环境中,这通常与 SessionRepositoryFilter 结合使用。请注意,此实现不支持发布会话事件。

Instantiating a JdbcIndexedSessionRepository

以下示例展示了如何创建新实例:

/*
 * Copyright 2014-2023 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package docs;

import java.time.Duration;
import java.util.concurrent.ConcurrentHashMap;

import com.hazelcast.config.Config;
import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;
import org.junit.jupiter.api.Test;

import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.mock.web.MockServletContext;
import org.springframework.session.MapSession;
import org.springframework.session.MapSessionRepository;
import org.springframework.session.ReactiveSessionRepository;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.session.data.redis.ReactiveRedisSessionRepository;
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
import org.springframework.session.data.redis.RedisSessionRepository;
import org.springframework.session.hazelcast.HazelcastIndexedSessionRepository;
import org.springframework.session.jdbc.JdbcIndexedSessionRepository;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;

import static org.assertj.core.api.Assertions.assertThat;

/**
 * @author Rob Winch
 * @author Vedran Pavic
 */
class IndexDocTests {

	private static final String ATTR_USER = "user";

	@Test
	void repositoryDemo() {
		RepositoryDemo<MapSession> demo = new RepositoryDemo<>();
		demo.repository = new MapSessionRepository(new ConcurrentHashMap<>());

		demo.demo();
	}

	// tag::repository-demo[]
	public class RepositoryDemo<S extends Session> {

		private SessionRepository<S> repository; (1)

		public void demo() {
			S toSave = this.repository.createSession(); (2)

			(3)
			User rwinch = new User("rwinch");
			toSave.setAttribute(ATTR_USER, rwinch);

			this.repository.save(toSave); (4)

			S session = this.repository.findById(toSave.getId()); (5)

			(6)
			User user = session.getAttribute(ATTR_USER);
			assertThat(user).isEqualTo(rwinch);
		}

		// ... setter methods ...

	}
	// end::repository-demo[]

	@Test
	void expireRepositoryDemo() {
		ExpiringRepositoryDemo<MapSession> demo = new ExpiringRepositoryDemo<>();
		demo.repository = new MapSessionRepository(new ConcurrentHashMap<>());

		demo.demo();
	}

	// tag::expire-repository-demo[]
	public class ExpiringRepositoryDemo<S extends Session> {

		private SessionRepository<S> repository; (1)

		public void demo() {
			S toSave = this.repository.createSession(); (2)
			// ...
			toSave.setMaxInactiveInterval(Duration.ofSeconds(30)); (3)

			this.repository.save(toSave); (4)

			S session = this.repository.findById(toSave.getId()); (5)
			// ...
		}

		// ... setter methods ...

	}
	// end::expire-repository-demo[]

	@Test
	void newRedisSessionRepository() {
		// tag::new-redissessionrepository[]
		RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();

		// ... configure redisTemplate ...

		SessionRepository<? extends Session> repository = new RedisSessionRepository(redisTemplate);
		// end::new-redissessionrepository[]
	}

	@Test
	void newRedisIndexedSessionRepository() {
		// tag::new-redisindexedsessionrepository[]
		RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();

		// ... configure redisTemplate ...

		SessionRepository<? extends Session> repository = new RedisIndexedSessionRepository(redisTemplate);
		// end::new-redisindexedsessionrepository[]
	}

	@Test
	void newReactiveRedisSessionRepository() {
		LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory();
		RedisSerializationContext<String, Object> serializationContext = RedisSerializationContext
			.<String, Object>newSerializationContext(new JdkSerializationRedisSerializer())
			.build();

		// tag::new-reactiveredissessionrepository[]
		// ... create and configure connectionFactory and serializationContext ...

		ReactiveRedisTemplate<String, Object> redisTemplate = new ReactiveRedisTemplate<>(connectionFactory,
				serializationContext);

		ReactiveSessionRepository<? extends Session> repository = new ReactiveRedisSessionRepository(redisTemplate);
		// end::new-reactiveredissessionrepository[]
	}

	@Test
	void mapRepository() {
		// tag::new-mapsessionrepository[]
		SessionRepository<? extends Session> repository = new MapSessionRepository(new ConcurrentHashMap<>());
		// end::new-mapsessionrepository[]
	}

	@Test
	void newJdbcIndexedSessionRepository() {
		// tag::new-jdbcindexedsessionrepository[]
		JdbcTemplate jdbcTemplate = new JdbcTemplate();

		// ... configure jdbcTemplate ...

		TransactionTemplate transactionTemplate = new TransactionTemplate();

		// ... configure transactionTemplate ...

		SessionRepository<? extends Session> repository = new JdbcIndexedSessionRepository(jdbcTemplate,
				transactionTemplate);
		// end::new-jdbcindexedsessionrepository[]
	}

	@Test
	void newHazelcastIndexedSessionRepository() {
		// tag::new-hazelcastindexedsessionrepository[]

		Config config = new Config();

		// ... configure Hazelcast ...

		HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance(config);

		HazelcastIndexedSessionRepository repository = new HazelcastIndexedSessionRepository(hazelcastInstance);
		// end::new-hazelcastindexedsessionrepository[]
	}

	@Test
	void runSpringHttpSessionConfig() {
		AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
		context.register(SpringHttpSessionConfig.class);
		context.setServletContext(new MockServletContext());
		context.refresh();

		try {
			context.getBean(SessionRepositoryFilter.class);
		}
		finally {
			context.close();
		}
	}

	private static final class User {

		private User(String username) {
		}

	}

}

有关如何创建和配置 JdbcTemplatePlatformTransactionManager 的其他信息,请参阅 {docs-url}/spring/docs/{spring-core-version}/spring-framework-reference/data-access.html[Spring Framework Reference Documentation]。

Using @EnableJdbcHttpSession

在 Web 环境中,创建新的 JdbcIndexedSessionRepository 的最简单方法是使用 @EnableJdbcHttpSession。您可以在 Samples and Guides (Start Here) 中找到完整的示例用法。您可以使用以下属性来自定义配置:

  • tableName: Spring 会话使用的数据库表名称,用于存储会话

  • maxInactiveIntervalInSeconds: 会话在多少秒后过期的时间量

Customizing LobHandler

您可以通过创建一个名为 springSessionLobHandler 的 bean(该 bean 实现 LobHandler)来自定义 BLOB 处理。

Customizing ConversionService

可以通过提供 ConversionService 实例来自定义会话的默认序列化和反序列化。在典型的 Spring 环境中,会自动选择默认 ConversionService bean(名为 conversionService)并将其用于序列化和反序列化。但是,您可以通过提供名为 springSessionConversionService 的 bean 来覆盖默认 ConversionService

Storage Details

默认情况下,该实现使用 SPRING_SESSIONSPRING_SESSION_ATTRIBUTES 表来存储会话。请注意,如前所述,您可以自定义表名。在这种情况下,用于存储属性的表会使用提供的表名加上 _ATTRIBUTES 作为后缀来命名。如果需要进一步自定义,您可以使用 set*Query setter 方法来自定义存储库所使用的 SQL 查询。在这种情况下,您需要手动配置 sessionRepository bean。

由于各种数据库供应商之间存在差异,尤其是在存储二进制数据时,请确保使用特定于您的数据库的 SQL 脚本。大多数主要数据库供应商的脚本打包为 org/springframework/session/jdbc/schema-.sql, where 是目标数据库类型。

例如,对于 PostgreSQL,您可以使用以下架构脚本:

Unresolved include directive in modules/ROOT/pages/api.adoc - include::example$session-jdbc-main-resources-dir/org/springframework/session/jdbc/schema-postgresql.sql[]

对于 MySQL 数据库,您可以使用以下脚本:

Unresolved include directive in modules/ROOT/pages/api.adoc - include::example$session-jdbc-main-resources-dir/org/springframework/session/jdbc/schema-mysql.sql[]

Transaction Management

JdbcIndexedSessionRepository 中的所有 JDBC 操作都是以事务方式执行的。事务的传播设置成 REQUIRES_NEW 来避免因与现有事务发生冲突而导致意外的行为(例如,在一个已参与只读事务的线程中运行 save 操作)。

Using HazelcastIndexedSessionRepository

HazelcastIndexedSessionRepository 是一个 SessionRepository 实现,它将会话存储在 Hazelcast 的分布式 IMap 中。在 Web 环境中,这通常与 SessionRepositoryFilter 结合使用。

Instantiating a HazelcastIndexedSessionRepository

以下示例展示了如何创建新实例:

/*
 * Copyright 2014-2023 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package docs;

import java.time.Duration;
import java.util.concurrent.ConcurrentHashMap;

import com.hazelcast.config.Config;
import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;
import org.junit.jupiter.api.Test;

import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.mock.web.MockServletContext;
import org.springframework.session.MapSession;
import org.springframework.session.MapSessionRepository;
import org.springframework.session.ReactiveSessionRepository;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.session.data.redis.ReactiveRedisSessionRepository;
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
import org.springframework.session.data.redis.RedisSessionRepository;
import org.springframework.session.hazelcast.HazelcastIndexedSessionRepository;
import org.springframework.session.jdbc.JdbcIndexedSessionRepository;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;

import static org.assertj.core.api.Assertions.assertThat;

/**
 * @author Rob Winch
 * @author Vedran Pavic
 */
class IndexDocTests {

	private static final String ATTR_USER = "user";

	@Test
	void repositoryDemo() {
		RepositoryDemo<MapSession> demo = new RepositoryDemo<>();
		demo.repository = new MapSessionRepository(new ConcurrentHashMap<>());

		demo.demo();
	}

	// tag::repository-demo[]
	public class RepositoryDemo<S extends Session> {

		private SessionRepository<S> repository; (1)

		public void demo() {
			S toSave = this.repository.createSession(); (2)

			(3)
			User rwinch = new User("rwinch");
			toSave.setAttribute(ATTR_USER, rwinch);

			this.repository.save(toSave); (4)

			S session = this.repository.findById(toSave.getId()); (5)

			(6)
			User user = session.getAttribute(ATTR_USER);
			assertThat(user).isEqualTo(rwinch);
		}

		// ... setter methods ...

	}
	// end::repository-demo[]

	@Test
	void expireRepositoryDemo() {
		ExpiringRepositoryDemo<MapSession> demo = new ExpiringRepositoryDemo<>();
		demo.repository = new MapSessionRepository(new ConcurrentHashMap<>());

		demo.demo();
	}

	// tag::expire-repository-demo[]
	public class ExpiringRepositoryDemo<S extends Session> {

		private SessionRepository<S> repository; (1)

		public void demo() {
			S toSave = this.repository.createSession(); (2)
			// ...
			toSave.setMaxInactiveInterval(Duration.ofSeconds(30)); (3)

			this.repository.save(toSave); (4)

			S session = this.repository.findById(toSave.getId()); (5)
			// ...
		}

		// ... setter methods ...

	}
	// end::expire-repository-demo[]

	@Test
	void newRedisSessionRepository() {
		// tag::new-redissessionrepository[]
		RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();

		// ... configure redisTemplate ...

		SessionRepository<? extends Session> repository = new RedisSessionRepository(redisTemplate);
		// end::new-redissessionrepository[]
	}

	@Test
	void newRedisIndexedSessionRepository() {
		// tag::new-redisindexedsessionrepository[]
		RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();

		// ... configure redisTemplate ...

		SessionRepository<? extends Session> repository = new RedisIndexedSessionRepository(redisTemplate);
		// end::new-redisindexedsessionrepository[]
	}

	@Test
	void newReactiveRedisSessionRepository() {
		LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory();
		RedisSerializationContext<String, Object> serializationContext = RedisSerializationContext
			.<String, Object>newSerializationContext(new JdkSerializationRedisSerializer())
			.build();

		// tag::new-reactiveredissessionrepository[]
		// ... create and configure connectionFactory and serializationContext ...

		ReactiveRedisTemplate<String, Object> redisTemplate = new ReactiveRedisTemplate<>(connectionFactory,
				serializationContext);

		ReactiveSessionRepository<? extends Session> repository = new ReactiveRedisSessionRepository(redisTemplate);
		// end::new-reactiveredissessionrepository[]
	}

	@Test
	void mapRepository() {
		// tag::new-mapsessionrepository[]
		SessionRepository<? extends Session> repository = new MapSessionRepository(new ConcurrentHashMap<>());
		// end::new-mapsessionrepository[]
	}

	@Test
	void newJdbcIndexedSessionRepository() {
		// tag::new-jdbcindexedsessionrepository[]
		JdbcTemplate jdbcTemplate = new JdbcTemplate();

		// ... configure jdbcTemplate ...

		TransactionTemplate transactionTemplate = new TransactionTemplate();

		// ... configure transactionTemplate ...

		SessionRepository<? extends Session> repository = new JdbcIndexedSessionRepository(jdbcTemplate,
				transactionTemplate);
		// end::new-jdbcindexedsessionrepository[]
	}

	@Test
	void newHazelcastIndexedSessionRepository() {
		// tag::new-hazelcastindexedsessionrepository[]

		Config config = new Config();

		// ... configure Hazelcast ...

		HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance(config);

		HazelcastIndexedSessionRepository repository = new HazelcastIndexedSessionRepository(hazelcastInstance);
		// end::new-hazelcastindexedsessionrepository[]
	}

	@Test
	void runSpringHttpSessionConfig() {
		AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
		context.register(SpringHttpSessionConfig.class);
		context.setServletContext(new MockServletContext());
		context.refresh();

		try {
			context.getBean(SessionRepositoryFilter.class);
		}
		finally {
			context.close();
		}
	}

	private static final class User {

		private User(String username) {
		}

	}

}

有关如何创建和配置 Hazelcast 实例的更多信息,请参阅 Hazelcast documentation

Using @EnableHazelcastHttpSession

若要将 Hazelcast 用作 SessionRepository 的后端源,您可以将 @EnableHazelcastHttpSession 注释添加到一个 @Configuration 类中。这样做将扩展由 @EnableSpringHttpSession 注释提供的功能,但会在 Hazelcast 中为您创建 SessionRepository。您必须提供一个单一的 HazelcastInstance bean,让配置可以正常运作。您可以在 Samples and Guides (Start Here) 中找到一个完整的配置示例。

Basic Customization

您可以在 @EnableHazelcastHttpSession 中使用以下属性来自定义配置:

  • maxInactiveIntervalInSeconds: 会话过期的时间量(以秒为单位)。默认值为 1800 秒(30 分钟)

  • sessionMapName: Hazelcast 中用于存储会话数据的分布式`Map`的名称。

Session Events

使用 MapListener 来响应添加到分布式 Map 中的条目、从分布式 Map 中驱逐条目,以及从分布式 Map 中移除条目,会使这些事件触发通过 ApplicationEventPublisher 发布 SessionCreatedEventSessionExpiredEventSessionDeletedEvent 事件(分别)。

Storage Details

会话被存储在 Hazelcast 的一个分布式 IMap 中。IMap 接口方法用于 get()put() 会话。此外,values() 方法支持 FindByIndexNameSessionRepository#findByIndexNameAndIndexValue 操作,以及适当的 ValueExtractor(需要向 Hazelcast 注册)。有关此配置的详细信息,请参阅 Hazelcast Spring Sample。会话在 IMap 中的过期处理方式是由 Hazelcast 支持在条目被 put()IMap 中时设置生存时间来实现的。空闲时间超过生存时间的条目(会话)会自动从 IMap 中移除。

无需配置榛子配置中的 max-idle-secondstime-to-live-seconds 等任何设置。

请注意,如果您使用 Hazelcast 的 MapStore 来持久化会话 IMap,则在从 MapStore 重新加载会话时将适用以下限制:

  • 重新加载触发器`EntryAddedListener`会导致重新发布`SessionCreatedEvent`

  • 重新加载使用给定`IMap`的默认 TTL 会导致会话失去其原始 TTL

Using CookieSerializer

CookieSerializer 负责定义如何写入会话 cookie。Spring Session 提供使用 DefaultCookieSerializer 的默认实现。

Exposing CookieSerializer as a bean

CookieSerializer 公开为 Spring Bean 可在您使用 @EnableRedisHttpSession 等配置时增加现有配置。

以下示例演示了如何执行此操作:

Unresolved include directive in modules/ROOT/pages/api.adoc - include::example$spring-session-samples/spring-session-sample-javaconfig-custom-cookie/src/main/java/sample/Config.java[]
1 我们自定义 cookie 名称,使其为 JSESSIONID
2 我们自定义 cookie 路径,使其为 /(而不是默认的上下文根)。
3 我们自定义域名模式(正则表达式),使其为 ^.?\\.(\\w\\.[a-z]+)$。这可以在多个域和应用程序之间共享会话。如果正则表达式不匹配,则不会设置域,并且会使用现有的域。如果正则表达式匹配,则第一个 grouping 将用作域。这意味着对 [role="bare"][role="bare"]https://child.example.com 的请求将域设置为 example.com。然而,对 [role="bare"][role="bare"]http://localhost:8080/ 或 [role="bare"][role="bare"]https://192.168.1.100:8080/ 的请求会使 cookie 未设置,因此在开发中无需任何更改即可继续使用。

你应当只匹配有效的域名字符,因为域名会反映在响应中。这样做可以防止恶意用户实施攻击,比如 HTTP Response Splitting

Customizing CookieSerializer

您可以使用 DefaultCookieSerializer 上的任何以下配置选项自定义如何写入会话 cookie。

  • cookieName:要使用的 cookie 名称。默认: SESSION

  • useSecureCookie:指定是否应使用安全 cookie。默认:创建时使用 HttpServletRequest.isSecure() 的值。

  • cookiePath:cookie 的路径。默认:上下文根。

  • cookieMaxAge:指定创建会话时要设置的 cookie 的最大生存期。默认:-1,表示在关闭浏览器时应删除 cookie。

  • jvmRoute:指定要追加到会话 ID 中并在 cookie 中包含的后缀。用于标识应路由到哪个 JVM 来获得会话关联性。对于某些实现(即 Redis),此选项不提供性能优势。然而,它可以帮助跟踪特定用户的日志。

  • domainName:允许指定要用于 cookie 的特定域名。这个选项易于理解,但通常需要开发环境和生产环境之间的不同配置。将 domainNamePattern 视为一种替代方案。

  • domainNamePattern:不区分大小写的模式,用于从 HttpServletRequest#getServerName() 中提取域名。模式应提供一个单独的分组,该分组用于提取 cookie 域的值。如果正则表达式不匹配,则不会设置域,并且会使用现有的域。如果正则表达式匹配,则第一个 grouping 将用作域。

  • sameSite:用于 SameSite cookie 指令的值。若要禁用 SameSite cookie 指令的序列化,可以将此值设为 null。默认:Lax

你应当只匹配有效的域名字符,因为域名会反映在响应中。这样做可以防止恶意用户实施攻击,比如 HTTP Response Splitting

Customizing SessionRepository

实现一个自定义 <<`SessionRepository`,api-sessionrepository>> API 应该是一个相当简单的任务。将自定义实现与 <<`@EnableSpringHttpSession`,api-enablespringhttpsession>> 支持结合起来,可以让您重用现有的 Spring Session 配置设施和基础结构。然而,有几个方面值得仔细考虑。

在 HTTP 请求的生命周期中,HttpSession 通常会持久化到 SessionRepository 中两次。第一个持久化操作是为了确保会话在客户端可以访问会话 ID 时可供客户端使用,并且在提交会话后也需要写入,因为可能会对会话进行进一步修改。考虑到这一点,我们通常建议 SessionRepository 实现跟踪更改以确保只保存增量。这在高并发环境中尤为重要,在该环境中,多个请求对同一 HttpSession 进行操作,因此会引起竞争条件,导致请求相互覆盖对会话属性的更改。Spring Session 提供的所有 SessionRepository 实现都使用所描述的方法来持久化会话更改,可在您实现自定义 SessionRepository 时作为指导。

请注意,同样的建议同样适用于实现自定义 <<`ReactiveSessionRepository`,api-reactivesessionrepository>>。在这种情况下,您应该使用 <<`@EnableSpringWebSession`,api-enablespringwebsession>>。