Spring Session指南

2023/05/19

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>
    <version>2.7.2</version>
    <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](https://search.maven.org/classic/#search gav 1 g%3A”org.springframework.boot” AND a%3A”spring-boot-starter-security”)、[spring-boot-starter-web](https://search.maven.org/classic/#search gav 1 g%3A”org.springframework.boot” AND a%3A”spring-boot-starter-web”)、[spring-boot-starter-test](https://search.maven.org/classic/#search gav 1 g%3A”org.springframework.boot” AND a%3A”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>
我们使用启动父pom在这里设置版本,所以这些可以保证与我们的其他依赖项一起工作。可以在此处找到每个依赖项的最新版本:[spring-boot-starter-data-redis](https://search.maven.org/classic/#search gav 1 g%3A”org.springframework.boot” AND a%3A”spring-boot-starter-data-redis”)、[spring-session](https://search.maven.org/classic/#search gav 1 g%3A”org.springframework.session” AND a%3A”spring-session”)。

4. 标准 Spring 配置(无引导)

让我们也看看在没有Spring Boot的情况下集成和配置spring- session——只是使用普通的 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](https://search.maven.org/classic/#search gav 1 g%3A”org.springframework.session” AND a%3A”spring-session”)、[spring-data-redis](https://search.maven.org/classic/#search gav 1 g%3A”org.springframework.data” AND a%3A”spring-data-redis”)。

4.2. 春季会议配置

现在让我们为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()
            .and()
            .authorizeRequests()
            .antMatchers("/")
            .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正在管理我们的会话。

七. 总结

Spring Session是一个用于管理 HTTP 会话的强大工具。通过将我们的会话存储简化为一个配置类和一些 Maven 依赖项,我们现在可以将多个应用程序连接到同一个Redis实例并共享身份验证信息。

与往常一样,本教程的完整源代码可在GitHub上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章