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-securityspring-boot-starter-webspring-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-redisspring-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-sessionspring-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上可用。