1. 概述
Spring Session 的簡單目標是從服務器端存儲的 HTTP 會話中解放會話管理。
該解決方案使在雲端之間共享會話數據變得容易,而無需與單個容器(例如 Tomcat)綁定。此外,它還支持在同一瀏覽器中的多會話以及通過標頭髮送會話。
在本文中,我們將使用 Spring Session 來管理 Web 應用程序中的身份驗證信息。雖然 Spring Session 可以使用 JDBC、Gemfire 或 MongoDB 存儲數據,但我們將使用 Redis。
有關 Redis 的介紹,請查看本文。
2. 一個簡單的項目
首先,讓我們創建一個簡單的 Spring Boot 項目,作為我們後續會例子的基礎:
org.springframework.boot
spring-boot-starter-parent
org.springframework.boot
spring-boot-starter-security
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-test
test
我們的應用程序使用 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=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 來設置版本,這裏保證與我們的其他依賴項兼容。每個依賴項的最新版本可以在這裏找到:spring-boot-starter-data-redis,spring-session。
4. 標準 Spring 配置 (無 Boot )
我們還將查看如何集成和配置 spring-session,而無需 Spring Boot – 僅使用純 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. 測試
最後,我們來測試一切 – 我們將定義一個簡單的測試,以便我們能夠做 2 件事:
- 消費實時 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 實例,並共享身份驗證信息。