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 文件提供每個條目的版本。每個依賴項的最新版本可以在這裏找到:spring-boot-starter-security、spring-boot-starter-web、spring-boot-starter-test。
我們還將為我們的 Redis 服務器添加一些配置屬性,位於 application.properties 中:
spring.redis.host=localhost
spring.redis.port=63793. 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 來設置此處版本,因此這些版本可以保證與我們的其他依賴項兼容。每個依賴項的最新版本可以在這裏找到:spring-boot-starter-data-redis,spring-session.
4. 標準 Spring 配置 (不使用 Boot)
讓我們也看看如何集成和配置 spring-session,而不使用 Spring Boot – 僅使用純粹的 Spring。
4.1. 依賴關係
如果我們將 spring-session 添加到標準的 Spring 項目中,則需要明確定義:
<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 實例,並共享身份驗證信息。