Ohhnews

分类导航

$ cd ..
foojay原文

使用 Spring Session MongoDB 构建分布式 HTTP 会话

#spring session#mongodb#分布式系统#会话管理#微服务

目录

Spring Session MongoDB 是一个库,它使 Spring 应用程序能够将 HTTP 会话数据存储和管理在 MongoDB 中,而不是依赖于容器特定的会话存储。在传统的部署中,会话状态通常绑定到单个应用程序实例,这使得跨多个服务器的扩展变得困难。通过将 Spring Session 与 MongoDB 集成,会话数据可以在应用程序重启后持久保存,并在集群中的多个实例之间共享,从而以最小的配置实现可扩展的分布式应用程序。

在本教程中,我们将构建一个小型 API,用于管理用户的主题偏好(浅色或深色)。这个示例特意设计得很简单,因为我们的目标不是演示业务逻辑,而是清楚地观察 HTTP 会话在实践中是如何工作的。

会话在服务器上创建,链接到客户端的 Cookie,然后在请求之间重用,以便应用程序能够记住状态。使用 Spring Session MongoDB,该会话状态被持久化在 MongoDB 中,而不是存储在应用程序容器的内存中。

MongoDB 作为会话存储表现良好,因为文档模型可以自然地映射到会话对象,TTL 索引可以自动处理过期,并且数据库可以随着应用程序流量的增长进行水平扩展。

在本教程结束时,您将了解:

  • 会话是如何创建的
  • Cookie 如何将请求链接到会话
  • 会话状态如何存储在 MongoDB 中
  • 同一个会话如何在请求之间重用

如果您想要本教程的完整代码,请查看 GitHub 仓库

前提条件

在开始之前,请确保您已安装以下内容:

应用程序需要通过环境变量提供 MongoDB 连接字符串:

MONGODB_URI

例如:

export MONGODB_URI="mongodb+srv://<username>:<password>@cluster.mongodb.net/"

应用程序配置将自动追加数据库名称。

项目依赖

我们将从一个新的 Spring 应用程序开始。您可以使用 Spring Initializr,并确保使用 Spring Boot 4.0+,以确保与 Spring Session 4.0 或更高版本兼容。该项目的 Maven 配置如下所示。

$ xml
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-mongodb</artifactId>
    </dependency>
    <dependency>
        <groupId>org.mongodb</groupId>
        <artifactId>mongodb-spring-session</artifactId>
        <version>4.0.0-rc0</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

Spring Web

spring-boot-starter-web 提供了一个嵌入式 Web 服务器,Spring MVC 框架用于构建 REST API。它包含以下注解:

  • @RestController
  • @RequestMapping
  • @GetMapping
  • @PostMapping

如果没有此依赖项,将没有可供会话附加的 HTTP 应用程序。

Spring Data MongoDB

spring-boot-starter-data-mongodb 提供了 Spring Boot 使用的 MongoDB 驱动集成。它管理数据库连接、配置和映射基础设施。

尽管我们的控制器代码从未直接与 MongoDB 交互,但 Spring Session 依赖此集成来持久化会话文档。

MongoDB Spring Session

最重要的依赖项是:

mongodb-spring-session

该库用基于 MongoDB 的版本替换了默认的 HTTP 会话实现。

会话不是存储在应用程序容器内的内存中,而是作为文档持久化在 MongoDB 中。这允许多个应用程序实例访问相同的会话数据。

在分布式系统中,这消除了用户会话与单个服务器实例之间的依赖关系。

应用程序配置

接下来,我们配置 MongoDB 连接。

$ properties
spring.application.name=devrel-tutorial-java-spring-session-mongodb

spring.mongodb.database=springSessions

spring.mongodb.uri=${MONGODB_URI}&appName=${spring.application.name}

这里定义了三个属性。

  1. spring.application.name 仅用于标识应用程序,并作为 appName 追加到 MongoDB 连接中。
  2. spring.mongodb.database 指定存储会话文档的数据库。
  3. spring.mongodb.uriMONGODB_URI 环境变量中获取基础连接字符串,并追加应用程序名称。

注意:

上面的示例使用 & 追加 appName。这假设您的 MONGODB_URI 已经包含查询参数(这在 MongoDB Atlas 连接字符串中很常见),例如:

mongodb+srv://<username>:<password>@cluster.mongodb.net/?retryWrites=true&w=majority

如果您的 URI 已经包含类似 ?retryWrites=true 的选项,您可以完全按照上述方式编写配置:

$ properties
spring.mongodb.uri=${MONGODB_URI}&appName=${spring.application.name}

但是,如果您的 URI 包含查询部分,追加 &appName 将产生无效的连接字符串。在这种情况下,您应该使用 ? 追加参数:

$ properties
spring.mongodb.uri=${MONGODB_URI}?appName=${spring.application.name}

简而言之:

  • 如果 URI 已经有查询参数,请使用 &appName=
  • 如果 URI 没有查询参数,请使用 ?appName=

引导应用程序

应用程序的入口点是一个标准的 Spring Boot 类。

$ java
@SpringBootApplication
public class SpringSessionsMongodbAppApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringSessionsMongodbAppApplication.class, args);
    }
}

这里没有特殊操作。重要的细节是我们没有手动配置会话存储。一旦启用了会话配置,Spring Boot 将自动完成所有连线工作。

启用 MongoDB HTTP 会话

为了激活基于 MongoDB 的会话,我们添加一个配置类。

$ java
@Configuration
@EnableMongoHttpSession
public class SessionConfig {
}

@EnableMongoHttpSession 注解指示 Spring 使用 Spring Session 提供的基于 MongoDB 的实现来替换默认的会话管理机制。此注解会更改整个应用程序的底层会话存储模型。

控制器将继续使用熟悉的 HttpSession API,但会话状态现在将持久化在 MongoDB 中。

构建主题 API

该 API 公开了两个端点:

POST /theme

GET /theme

控制器实现如下所示。

$ java
@RestController
@RequestMapping("/theme")
public class ThemeController {
    @PostMapping
    public Map<String, Object> setTheme(
            @RequestParam String theme,
            HttpSession session) {
        session.setAttribute("theme", theme);
        return Map.of(
                "message", "Theme set",
                "theme", theme,
                "sessionId", session.getId()
        );
    }

    @GetMapping
    public Map<String, Object> getTheme(HttpSession session) {
        String theme = (String) session.getAttribute("theme");
        return Map.of(
                "theme", theme,
                "sessionId", session.getId()
        );
    }
}

这里发生了两件重要的事情。首先,控制器接受一个 HttpSession 对象作为方法参数。Spring 会自动为每个请求提供此对象。

其次,控制器使用标准 API 与会话进行交互。

$ java
session.setAttribute("theme", theme);

从控制器的角度来看,其行为与普通会话完全相同。但是,由于启用了 Spring Session MongoDB,会话数据不会存储在内存中。相反,它作为文档持久化在 MongoDB 中。控制器不需要了解该实现细节。

运行应用程序

使用以下命令启动应用程序:

$ bash
mvn spring-boot:run

API 将在以下地址提供:

http://localhost:8080

现在我们可以测试会话行为。

使用 curl 测试会话行为

使用 curl 可以让我们直接检查 HTTP 标头和 Cookie。首先,我们创建一个会话并存储主题偏好。

$ bash
curl -i -c cookies.txt -X POST "http://localhost:8080/theme?theme=light"

响应应如下所示:

$ http
HTTP/1.1 200
Set-Cookie: SESSION=YjI0MGU5NjctYjJlYS00ZGY1LWFlNjgtOTBhNmE1MWQzMTBj
Content-Type: application/json

{
 "sessionId":"b240e967-b2ea-4df5-ae68-90a6a51d310c",
 "theme":"light",
 "message":"Theme set"
}

这里发生了几件事。因为请求没有包含会话 Cookie,Spring 创建了一个新会话。主题值被存储为会话属性,Spring Session 将该会话持久化在 MongoDB 中。服务器随后返回了一个名为 SESSION 的 Cookie。-c cookies.txt 选项指示 curl 保存该 Cookie,以便稍后重用。

重用会话

接下来,我们使用保存的 Cookie 发送另一个请求。

$ bash
curl -i -b cookies.txt http://localhost:8080/theme

响应示例:

$ cat
{
 "theme":"light",
 "sessionId":"b240e967-b2ea-4df5-ae68-90a6a51d310c"
}

会话 ID 与之前的请求相同。这确认了会话已使用 Cookie 成功解析。

Spring 在内部执行了以下步骤:

  1. 读取 SESSION Cookie
  2. 提取会话标识符
  3. 从 MongoDB 中检索会话文档
  4. 填充 HttpSession 对象
  5. 将存储的属性返回给控制器

从应用程序的角度来看,这看起来仍然像是正常的会话使用。

在 MongoDB 中检查会话

如果您连接到 MongoDB 并检查 springSessions 数据库,您将看到由 Spring Session 创建的代表每个活跃 HTTP 会话的文档。

会话文档可能如下所示:

$ cat
{
  "_id": "4321d619-8526-4ca2-8163-32d09b12ee98",
  "created": { "$date": "2026-03-12T14:24:11.341Z" },
  "accessed": { "$date": "2026-03-12T14:24:15.733Z" },
  "interval": "PT30M",
  "principal": null,
  "expireAt": { "$date": "2026-03-12T14:54:15.733Z" },
  "attr": { "$binary": "..."}
}

每个字段都捕获了会话生命周期的不同方面。

_id 字段是会话标识符。这对应于 Spring 在解析 HttpSession 时内部使用的会话 ID。当请求带着 SESSION Cookie 到达时,Spring 从该 Cookie 中提取标识符,并使用它来检索匹配的会话文档。

created 时间戳记录了会话首次创建的时间。这对于了解会话在应用程序中通常保持活跃的时长或审计会话活动非常有用。

accessed 字段跟踪会话最后一次使用的时间。每次请求成功解析会话时,Spring 都会更新此值。这允许系统确定会话是仍然活跃还是已变为空闲。

interval 字段定义了会话不活动超时时间。在此示例中,值 PT30M 表示 30 分钟的超时。如果在此窗口内未访问会话,它将有资格过期。

expireAt 字段存储了会话应过期的确切时刻。MongoDB 通常在此字段上维护一个 TTL 索引,以便过期的会话自动从数据库中删除,而无需任何额外的清理逻辑。这意味着会话生命周期管理是在数据库层面自动发生的。

attr 字段存储了会话属性本身。在我们的示例中,控制器使用以下代码在会话中存储了主题偏好:

$ java
session.setAttribute("theme", theme);

Spring 序列化会话属性并将其存储在此字段中。当再次加载会话时,Spring 会反序列化数据并将属性恢复到控制器交互的 HttpSession 对象中。

尽管该示例仅存储了一个主题值,但实际应用程序通常会存储更有意义的会话数据。常见的例子包括:

  • 已认证的用户信息
  • 临时用户偏好
  • 多步工作流状态
  • 购物车内容
  • CSRF 令牌
  • 功能标志或 UI 状态

关键的架构优势是这些会话数据现在被外部化了。它不再存在于单个应用程序实例的内存中,而是存储在 MongoDB 中,集群中的任何实例都可以检索它。

这在负载均衡环境中尤为重要。当请求到达时,它可能会被路由到应用程序集群中的任何服务器。由于会话数据是集中存储的,处理请求的服务器可以使用 Cookie 标识符解析会话,并重新构建相同的 HttpSession 状态,而不管哪个实例处理了之前的请求。

实际上,这意味着您的应用程序可以在不丢失跨请求维护一致会话状态能力的情况下进行水平扩展。

为什么这很重要

在单节点应用程序中,将会话存储在内存中似乎就足够了。然而,一旦引入多个应用程序实例,这种方法就会失效。想象一个负载均衡系统,用户将第一个请求发送到服务器 A。该服务器将会话存储在内存中。在下一个请求时,负载均衡器将用户路由到服务器 B。如果会话存储在本地,服务器 B 就不知道该用户的会话。这会导致应用程序行为不一致。许多系统尝试使用负载均衡器上的粘性会话(Sticky Sessions)来解决这个问题,但这种方法降低了弹性并使扩展复杂化。

Spring Session MongoDB 通过将会话状态移至共享数据存储来解决此问题。现在,每个应用程序实例都可以使用存储在 Cookie 中的会话标识符来解析相同的会话。

结论

Spring Session MongoDB 允许 Spring 应用程序外部化 HTTP 会话存储,而无需更改控制器使用的编程模型。开发人员可以继续使用熟悉的 HttpSession API,而底层的会话状态则持久化在 MongoDB 中。

在本教程中,我们构建了一个简单的 API,将主题偏好存储在会话中,使用 @EnableMongoHttpSession 启用了基于 MongoDB 的会话,并使用 curl 验证了其行为。

尽管该示例特意设计得很小,但相同的架构支持更大的用例,例如认证会话、用户偏好、购物车和多步工作流。

通过将会话状态存储在 MongoDB 中,应用程序获得了水平扩展的能力,同时保持了跨集群的一致会话行为。