1. 概述
Spring Session 的目标是将会话管理从服务器存储的HTTP会话的限制中解放出来。
这个解决方案使得在云中跨服务共享会话数据变得简单,不受单个容器(如Tomcat)的束缚。此外,它支持同一浏览器内的多个会话,并允许通过头部发送会话。
在这篇文章中,我们将使用Spring Session 在Web应用中管理身份验证信息。虽然Spring Session 可以使用JDBC、Gemfire或MongoDB持久化数据,但我们将使用Redis。
想了解Redis 的基础知识,请参阅这篇文章。
2. 简单项目
首先,我们创建一个简单的Spring Boot 项目,作为后续示例的基础:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
我们的应用基于Spring Boot 运行,父pom.xml
文件为每个依赖提供了版本。每个依赖的最新版本可以在这里找到:spring-boot-starter-security,spring-boot-starter-web,spring-boot-starter-test。
在application.properties
中添加一些Redis服务器配置:
spring.redis.host=localhost
spring.redis.port=6379
3. Spring Boot配置
对于Spring Boot,只需添加以下依赖,剩下的自动配置会处理其余部分:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
我们使用boot父pom.xml
来设置版本,确保与我们的其他依赖兼容。每个依赖的最新版本可以在这里找到:spring-boot-starter-data-redis,spring-session。
4. 标准Spring配置(无Boot)
让我们看看如何在不使用Spring Boot的情况下集成和配置spring-session - 只使用纯Spring。
4.1. 依赖
如果我们在标准Spring项目中添加spring-session,需要明确指定:
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session</artifactId>
<version>1.2.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>1.5.0.RELEASE</version>
</dependency>
这些模块的最新版本可以在这里找到:spring-session,spring-data-redis。
4.2. Spring Session配置
现在添加Spring Session 的配置类:
@Configuration
@EnableRedisHttpSession
public class SessionConfig extends AbstractHttpSessionApplicationInitializer {
@Bean
public JedisConnectionFactory connectionFactory() {
return new JedisConnectionFactory();
}
}
@EnableRedisHttpSession
和对AbstractHttpSessionApplicationInitializer
的扩展将在所有安全基础设施前面创建并连接一个过滤器,用于查找活动会话,并根据存储在Redis中的值填充安全上下文。
接下来,让我们的应用程序完成控制器和安全配置。
5. 应用配置
导航到主应用程序文件,添加一个控制器:
@RestController
public class SessionController {
@RequestMapping("/")
public String helloAdmin() {
return "hello admin";
}
}
这将为我们提供一个测试端点。
接下来,添加我们的安全配置类:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public InMemoryUserDetailsManager userDetailsService(PasswordEncoder passwordEncoder) {
UserDetails user = User.withUsername("admin")
.password(passwordEncoder.encode("password"))
.roles("ADMIN")
.build();
return new InMemoryUserDetailsManager(user);
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.httpBasic(withDefaults())
.sessionManagement(httpSecuritySessionManagementConfigurer -> httpSecuritySessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.ALWAYS))
.authorizeRequests((authorizeRequests) -> authorizeRequests.requestMatchers("/")
.hasRole("ADMIN")
.anyRequest()
.authenticated());
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
这个配置保护我们的端点进行基本认证,并设置一个用户进行测试。
6. 测试
最后,让我们测试一下 - 我们将定义一个简单的测试,它将帮助我们完成两件事:
- 消费活体Web应用
- 联系Redis
首先设置环境:
public class SessionControllerTest {
private Jedis jedis;
private TestRestTemplate testRestTemplate;
private TestRestTemplate testRestTemplateWithAuth;
private String testUrl = "http://localhost:8080/";
@Before
public void clearRedisData() {
testRestTemplate = new TestRestTemplate();
testRestTemplateWithAuth = new TestRestTemplate("admin", "password", null);
jedis = new Jedis("localhost", 6379);
jedis.flushAll();
}
}
注意我们设置了这两个客户端的客户端 - HTTP客户端和Redis客户端。当然,在此阶段,服务器(和Redis)应该已经运行,以便我们可以通过这些测试与它们通信。
首先,测试Redis是否为空:
@Test
public void testRedisIsEmpty() {
Set<String> result = jedis.keys("*");
assertEquals(0, result.size());
}
然后,测试未授权请求的安全返回401:
@Test
public void testUnauthenticatedCantAccess() {
ResponseEntity<String> result = testRestTemplate.getForEntity(testUrl, String.class);
assertEquals(HttpStatus.UNAUTHORIZED, result.getStatusCode());
}
接下来,我们测试Spring Session 正在管理我们的身份验证令牌:
@Test
public void testRedisControlsSession() {
ResponseEntity<String> result = testRestTemplateWithAuth.getForEntity(testUrl, String.class);
assertEquals("hello admin", result.getBody()); //login worked
Set<String> redisResult = jedis.keys("*");
assertTrue(redisResult.size() > 0); //redis is populated with session data
String sessionCookie = result.getHeaders().get("Set-Cookie").get(0).split(";")[0];
HttpHeaders headers = new HttpHeaders();
headers.add("Cookie", sessionCookie);
HttpEntity<String> httpEntity = new HttpEntity<>(headers);
result = testRestTemplate.exchange(testUrl, HttpMethod.GET, httpEntity, String.class);
assertEquals("hello admin", result.getBody()); //access with session works worked
jedis.flushAll(); //clear all keys in redis
result = testRestTemplate.exchange(testUrl, HttpMethod.GET, httpEntity, String.class);
assertEquals(HttpStatus.UNAUTHORIZED, result.getStatusCode());
//access denied after sessions are removed in redis
}
首先,我们的测试确认使用管理员凭证的请求成功。
然后,我们从响应头中提取会话值,并将其用作第二次请求的身份验证。我们验证了这一点,然后清除了Redis中的所有数据。
最后,我们使用会话cookie再次发送请求,确认已登出。这证实了Spring Session 正在管理我们的会话。
7. 总结
Spring Session 是管理HTTP会话的强大工具。通过简化到一个配置类和几个Maven依赖,我们现在可以将多个应用程序连接到同一个Redis实例上,并共享身份验证信息。
一如既往,所有示例代码都在GitHub上可用。