---
title: "如何使用会话"
version: 5.1
locale: zh-hans
source: https://docs.djangoproject.com/zh-hans/5.1/topics/http/sessions/
canonical: https://djangodocs.dev/zh-hans/5.1/topics/http/sessions/
---
# 如何使用会话

Django 提供了对匿名会话的全面支持。会话框架允许你在每个站点访问者的基础上存储和检索任意数据。它将数据存储在服务器端并抽象了 cookie 的发送和接收。Cookies 包含会话 ID - 而不是数据本身（除非你使用 [基于 cookie 的后端](#cookie-session-backend)）。

## 启用会话

会话是通过一段 [中间件](/zh-hans/5.1/ref/middleware/) 实现的。

要启用会话功能，请执行以下操作：

- 编辑 [`MIDDLEWARE`](/zh-hans/5.1/ref/settings/#std-setting-MIDDLEWARE) 设置，并确保其中包含 `'django.contrib.sessions.middleware.SessionMiddleware'`。由 `django-admin startproject` 创建的默认 `settings.py` 已经激活了 `SessionMiddleware`。

如果你不想使用会话，你也可以从 [`MIDDLEWARE`](/zh-hans/5.1/ref/settings/#std-setting-MIDDLEWARE) 中删除 `SessionMiddleware` 行，并从 [`INSTALLED_APPS`](/zh-hans/5.1/ref/settings/#std-setting-INSTALLED_APPS) 中删除 `'django.contrib.sessions'`。这将节省一些小额开销。

## 配置会话引擎

默认情况下，Django 将会话存储在数据库中（使用模型 `django.contrib.sessions.models.Session`）。尽管这很方便，但在某些设置中，将会话数据存储在其他地方可能更快，因此可以配置 Django 将会话数据存储在文件系统或缓存中。

### 使用基于数据库的会话

如果要使用基于数据库的会话，需要将 `'django.contrib.sessions'` 添加到你的 [`INSTALLED_APPS`](/zh-hans/5.1/ref/settings/#std-setting-INSTALLED_APPS) 设置中。

一旦配置完成，运行 `manage.py migrate` 来安装存储会话数据的单个数据库表。

### 使用缓存会话

为了获得更好的性能，你可能想要使用基于缓存的会话后端。

要使用 Django 的缓存系统存储会话数据，首先需要确保你已经配置了缓存；请查看 [缓存文档](/zh-hans/5.1/topics/cache/) 获取详细信息。

> **Warning**
>
> 只有当你使用 Memcached 或 Redis 缓存后端时，才应该使用基于缓存的会话。本地内存缓存后端不会保留数据足够长的时间，因此不是一个好选择，直接使用文件或数据库会话可能会更快，而不是通过文件或数据库缓存后端发送所有数据。此外，本地内存缓存后端不是多进程安全的，因此在生产环境中可能不是一个好选择。

如果在 [`CACHES`](/zh-hans/5.1/ref/settings/#std-setting-CACHES) 中定义了多个缓存，Django 将使用默认缓存。要使用另一个缓存，将 [`SESSION_CACHE_ALIAS`](/zh-hans/5.1/ref/settings/#std-setting-SESSION_CACHE_ALIAS) 设置为该缓存的名称。

一旦配置了缓存，你需要在数据库支持的缓存和非持久性缓存之间进行选择。

缓存数据库后端（`cached_db`）使用直写式缓存 —— 会话写入按顺序同时应用于数据库和缓存。如果写入缓存失败，异常将通过 [sessions logger](/zh-hans/5.1/ref/logging/#django-contrib-sessions-logger) 进行处理和记录，以避免导致其他成功的写入操作失败。

> **Changed in Django 5.1**
>
> 增加了写入缓存时的异常处理和记录功能。

会话读取使用缓存，如果数据已从缓存中逐出，则使用数据库。要使用此后端，请将 [`SESSION_ENGINE`](/zh-hans/5.1/ref/settings/#std-setting-SESSION_ENGINE) 设置为 `"django.contrib.sessions.backends.cached_db"`，并按照 [使用数据库支持的会话](#using-database-backed-sessions) 的配置说明进行操作。

缓存后端（`cache`）只在缓存中存储会话数据。这更快，因为它避免了数据库持久性，但你需要考虑当缓存数据被逐出时会发生什么情况。逐出可能发生在缓存填满或缓存服务器重新启动时，这将意味着会话数据丢失，包括用户的注销状态。要使用此后端，请将 [`SESSION_ENGINE`](/zh-hans/5.1/ref/settings/#std-setting-SESSION_ENGINE) 设置为 `"django.contrib.sessions.backends.cache"`。

缓存后端可以通过使用具有适当配置的持久缓存（例如 Redis）来变成持久性的。但除非你的缓存明确配置为具有足够的持久性，否则选择缓存数据库后端是更安全的选择。这可以避免由于生产中不可靠的数据存储引起的边缘情况。

### 使用基于文件的会话

要使用基于文件的会话，请将 [`SESSION_ENGINE`](/zh-hans/5.1/ref/settings/#std-setting-SESSION_ENGINE) 设置为 `"django.contrib.sessions.backends.file"`。

你可能还想设置 [`SESSION_FILE_PATH`](/zh-hans/5.1/ref/settings/#std-setting-SESSION_FILE_PATH) 配置（默认为 `tempfile.gettempdir()` 的输出，很可能是 `/tmp`）来控制 Django 存储会话文件的位置。请确保你的 Web 服务器具有读写此位置的权限。

### 使用基于 cookie 的会话

要使用基于 cookie 的会话，请将 [`SESSION_ENGINE`](/zh-hans/5.1/ref/settings/#std-setting-SESSION_ENGINE) 设置为 `"django.contrib.sessions.backends.signed_cookies"`。会话数据将使用 Django 的 [加密签名](/zh-hans/5.1/topics/signing/) 工具和 [`SECRET_KEY`](/zh-hans/5.1/ref/settings/#std-setting-SECRET_KEY) 设置存储。

> **Note**
>
> 建议将 [`SESSION_COOKIE_HTTPONLY`](/zh-hans/5.1/ref/settings/#std-setting-SESSION_COOKIE_HTTPONLY) 设置为 `True`，以防止 JavaScript 访问存储的数据。

> **Warning**
>
> **会话数据被签名但未加密**
>
> 在使用基于 cookie 的后端时，会话数据可以被客户端读取。
>
> 使用 MAC（消息认证码）用于保护数据免受客户端的更改，以便在被篡改时会话数据将被作废。如果存储 cookie 的客户端（例如你用户的浏览器）无法存储全部会话 cookie 并丢弃数据，会发生相同的作废。尽管 Django 对数据进行了压缩，但仍然完全有可能超过每个 cookie 的 [**常见限制 4096 字节**](https://datatracker.ietf.org/doc/html/rfc2965.html#section-5.3)。
>
> **不保证新鲜度**
>
> 还要注意，虽然 MAC 可以保证数据的真实性（即它是由你的站点生成的，而不是其他人生成的），以及数据的完整性（即它都在那里且正确），但它不能保证新鲜度，也就是不能保证你被发送回客户端的是最后发送给客户端的内容。这意味着对于一些使用会话数据的情况，基于 cookie 的后端可能会使你容易受到 [重放攻击](https://en.wikipedia.org/wiki/Replay_attack) 的威胁。与其他会话后端不同，这些后端会保留每个会话的服务器端记录，并在用户注销时使其失效，基于 cookie 的会话在用户注销时不会失效。因此，如果攻击者窃取了用户的 cookie，即使用户注销，他们仍然可以使用该 cookie 以该用户的身份登录。只有当 cookie 的寿命超过 [`SESSION_COOKIE_AGE`](/zh-hans/5.1/ref/settings/#std-setting-SESSION_COOKIE_AGE) 时，它们才会被检测为 '过时'。
>
> **性能**
>
> 最后，cookie 的大小可能会影响你的网站的速度。

## 在视图中使用会话

当激活了 `SessionMiddleware` 时，每个 [`HttpRequest`](/zh-hans/5.1/ref/request-response/#django.http.HttpRequest) 对象 -- 作为任何 Django 视图函数的第一个参数 -- 都将具有一个 `session` 属性，它是一个类似于字典的对象。

你可以在视图的任何时候读取并写入 `request.session`。你可以多次编辑它。

#### `class backends.base.SessionBase`

这是所有会话对象的基类。它具有以下标准的字典方法：

#### `__getitem__(key)`

比如：`fav_color = request.session['fav_color']`

#### `__setitem__(key, value)`

比如：`request.session['fav_color'] = 'blue'`

#### `__delitem__(key)`

比如：`del request.session['fav_color']` 。如果给定的 `key` 不在会话里，会引发 `KeyError` 。

#### `__contains__(key)`

比如：`'fav_color' in request.session`

#### `get(key, default=None)`

#### `aget(key, default=None)`

*异步版本*: `aget()`

比如：`fav_color = request.session.get('fav_color', 'red')`

> **Changed in Django 5.1**
>
> 增加了 `aget()` 函数。

#### `aset(key, value)`

> **New in Django 5.1**

示例：`await request.session.aset('fav_color', 'red')`

#### `update(dict)`

#### `aupdate(dict)`

*异步版本*: `aupdate()`

示例：`request.session.update({'fav_color': 'red'})`

> **Changed in Django 5.1**
>
> 增加了 `aupdate()` 函数。

#### `pop(key, default=__not_given)`

#### `apop(key, default=__not_given)`

*异步版本*：`apop()`

比如：`fav_color = request.session.pop('fav_color', 'blue')`

> **Changed in Django 5.1**
>
> 增加了 `apop()` 函数。

#### `keys()`

#### `akeys()`

*异步版本*：`akeys()`

> **Changed in Django 5.1**
>
> 增加了 `akeys()` 函数。

#### `values()`

#### `avalues()`

*异步版本*：`avalues()`

> **Changed in Django 5.1**
>
> 增加了 `avalues()` 函数。

#### `has_key(key)`

#### `ahas_key(key)`

*异步版本*：`ahas_key()`

> **Changed in Django 5.1**
>
> 增加了 `ahas_key()` 函数。

#### `items()`

#### `aitems()`

*异步版本*：`aitems()`

> **Changed in Django 5.1**
>
> 增加了 `aitems()` 函数。

#### `setdefault()`

#### `asetdefault()`

*异步版本*：`asetdefault()`

> **Changed in Django 5.1**
>
> 增加了 `asetdefault()` 函数。

#### `clear()`

它也有以下方法：

#### `flush()`

#### `aflush()`

*异步版本*：`aflush()`

从会话中删除当前会话数据并删除会话 cookie。如果你想确保用户的浏览器无法再次访问以前的会话数据（例如， [`django.contrib.auth.logout()`](/zh-hans/5.1/topics/auth/default/#django.contrib.auth.logout) 函数会调用它）。

> **Changed in Django 5.1**
>
> 增加了 `aflush()` 函数。

#### `set_test_cookie()`

#### `aset_test_cookie()`

*异步版本*：`aset_test_cookie()`

设置一个测试 cookie 来确定用户的浏览器是否支持 cookie。由于 cookie 的工作方式，你将无法在用户的下一个页面请求之前进行测试。有关更多信息，请参阅下面的 [设置测试 cookie](#setting-test-cookies)。

> **Changed in Django 5.1**
>
> 增加了 `aset_test_cookie()` 函数。

#### `test_cookie_worked()`

#### `atest_cookie_worked()`

*异步版本*：`atest_cookie_worked()`

根据用户的浏览器是否接受测试 cookie，返回 `True` 或 `False`。由于 cookie 的工作方式，你必须在之前的单独页面请求中调用 `set_test_cookie()` 或 `aset_test_cookie()`。有关更多信息，请参阅下面的 [设置测试 cookie](#setting-test-cookies)。

> **Changed in Django 5.1**
>
> 增加了 `atest_cookie_worked()` 函数。

#### `delete_test_cookie()`

#### `adelete_test_cookie()`

*异步版本*：`adelete_test_cookie()`

删除测试 cookie。使用它来清理。

> **Changed in Django 5.1**
>
> 增加了 `adelete_test_cookie()` 函数。

#### `get_session_cookie_age()`

返回 [`SESSION_COOKIE_AGE`](/zh-hans/5.1/ref/settings/#std-setting-SESSION_COOKIE_AGE) 设置的值。这可以在自定义会话后端中进行覆盖。

#### `set_expiry(value)`

#### `aset_expiry(value)`

*异步版本*：`aset_expiry()`

设置会话的过期时间。你可以传递多种不同的值：

- 如果 `value` 是一个整数，会话将在多少秒的不活动后过期。例如，调用 `request.session.set_expiry(300)` 会使会话在5分钟后过期。
- 如果 `value` 是一个 `datetime` 或 `timedelta` 对象，会话将在特定的日期/时间过期。
- 如果 `value` 是 `0`，用户的会话 cookie 将在用户关闭 Web 浏览器时过期。
- 如果 `value` 是 `None`，会话将恢复使用全局会话过期策略。

读取会话不被视为用于过期目的的活动。会话过期是根据会话上次被 *修改* 的时间计算的。

> **Changed in Django 5.1**
>
> 增加了 `aset_expiry()` 函数。

#### `get_expiry_age()`

#### `aget_expiry_age()`

*异步版本*：`aget_expiry_age()`

返回会话过期前剩余的秒数。对于没有自定义过期时间的会话（或设置为在关闭浏览器时过期的会话），这将等于 [`SESSION_COOKIE_AGE`](/zh-hans/5.1/ref/settings/#std-setting-SESSION_COOKIE_AGE)。

此函数接受两个可选的关键字参数：

- `modification`：会话的最后修改时间，作为一个 [`datetime`](https://docs.python.org/3/library/datetime.html#datetime.datetime) 对象。默认为当前时间。
- `expiry`：会话的过期信息，可以是 [`datetime`](https://docs.python.org/3/library/datetime.html#datetime.datetime) 对象、``` int`（以秒为单位）或 ``None` ```。默认为 [`set_expiry()`](#django.contrib.sessions.backends.base.SessionBase.set_expiry)/[`aset_expiry()`](#django.contrib.sessions.backends.base.SessionBase.aset_expiry) 存储在会话中的值（如果有），否则为 `None`。

> **Note**
>
> 这个方法用于会话后端在保存会话时确定会话过期时间的秒数。它实际上并不打算在那种上下文之外使用。
>
> 特别是，尽管在你拥有正确的 `modification` 值 **且** `expiry` 设置为 `datetime` 对象的情况下可以 **可能** 确定会话的剩余寿命，但在你有 `modification` 值的情况下，手动计算到期时间更加直接：
>
> ```
> expires_at = modification + timedelta(seconds=settings.SESSION_COOKIE_AGE)
> ```

> **Changed in Django 5.1**
>
> 增加了 `aget_expiry_age()` 函数。

#### `get_expiry_date()`

#### `aget_expiry_date()`

*异步版本*：`aget_expiry_date()`

返回会话将过期的日期。对于没有自定义过期时间的会话（或设置为在关闭浏览器时过期的会话），这将等于从现在开始 [`SESSION_COOKIE_AGE`](/zh-hans/5.1/ref/settings/#std-setting-SESSION_COOKIE_AGE) 秒后的日期。

此函数接受与 [`get_expiry_age()`](#django.contrib.sessions.backends.base.SessionBase.get_expiry_age) 相同的关键字参数，并适用类似的用法注意事项。

> **Changed in Django 5.1**
>
> 增加了 `aget_expiry_date()` 函数。

#### `get_expire_at_browser_close()`

#### `aget_expire_at_browser_close()`

*异步版本*：`aget_expire_at_browser_close()`

根据用户的会话 cookie 是否在用户关闭 Web 浏览器时过期，返回 `True` 或 `False`。

> **Changed in Django 5.1**
>
> 增加了 `aget_expire_at_browser_close()` 函数。

#### `clear_expired()`

#### `aclear_expired()`

*异步版本*：`aclear_expired()`

从会话存储中删除已过期的会话。这个类方法由 [`clearsessions`](/zh-hans/5.1/ref/django-admin/#django-admin-clearsessions) 调用。

> **Changed in Django 5.1**
>
> 增加了 `aclear_expired()` 函数。

#### `cycle_key()`

#### `acycle_key()`

*异步版本*：`acycle_key()`

在保留当前会话数据的同时创建一个新的会话密钥。 [`django.contrib.auth.login()`](/zh-hans/5.1/topics/auth/default/#django.contrib.auth.login) 调用这个方法以减轻会话固定攻击。

> **Changed in Django 5.1**
>
> 增加了 `acycle_key()` 函数。

### 会话序列化

默认情况下，Django使用 JSON 对会话数据进行序列化。你可以使用 [`SESSION_SERIALIZER`](/zh-hans/5.1/ref/settings/#std-setting-SESSION_SERIALIZER) 设置来自定义会话序列化格式。尽管在 [编写你自己的序列化器](#custom-serializers) 中描述了一些注意事项，但我们强烈建议 *特别是如果你使用的是基于 cookie 的后端*，坚持使用 JSON 序列化。

例如，如果你使用 [`pickle`](https://docs.python.org/3/library/pickle.html#module-pickle) 来序列化会话数据，以下是一个攻击场景。如果你使用的是 [签名 cookie 会话后端](#cookie-session-backend) 并且 [`SECRET_KEY`](/zh-hans/5.1/ref/settings/#std-setting-SECRET_KEY) （或 [`SECRET_KEY_FALLBACKS`](/zh-hans/5.1/ref/settings/#std-setting-SECRET_KEY_FALLBACKS) 的任何密钥）被攻击者知道（Django 本身没有内在的漏洞会导致其泄露），攻击者可以在他们的会话中插入一个字符串，当反序列化时，它在服务器上执行任意代码。这种技巧很简单，而且在互联网上很容易找到。尽管 cookie 会话存储对 cookie 中存储的数据进行了签名以防止篡改，但 [`SECRET_KEY`](/zh-hans/5.1/ref/settings/#std-setting-SECRET_KEY) 泄漏会立即升级为远程代码执行漏洞。

#### 捆绑的序列化器

#### `class serializers.JSONSerializer`

一个包装了 [`django.core.signing`](/zh-hans/5.1/topics/signing/#module-django.core.signing) 中的 JSON 序列化器的包装器。只能序列化基本数据类型。

此外，由于 JSON 只支持字符串键，因此请注意在 `request.session` 中使用非字符串键不会按预期工作：

```pycon
>>> # initial assignment
>>> request.session[0] = "bar"
>>> # subsequent requests following serialization & deserialization
>>> # of session data
>>> request.session[0]  # KeyError
>>> request.session["0"]
'bar'
```

类似地，不能编码为 JSON 的数据，例如不能编码为 UTF-8 的字节，如 `'\xd9'` （会引发 [`UnicodeDecodeError`](https://docs.python.org/3/library/exceptions.html#UnicodeDecodeError)），也无法存储。

请参阅 [编写你自己的序列化器](#custom-serializers) 部分以获取关于 JSON 序列化的限制的更多详细信息。

#### 编写你自己的序列化器

请注意， [`JSONSerializer`](#django.contrib.sessions.serializers.JSONSerializer) 不能处理任意的 Python 数据类型。通常情况下，方便和安全之间存在权衡。如果你希望在 JSON 支持的会话中存储更高级的数据类型，包括 `datetime` 和 `Decimal`，你需要编写一个自定义的序列化器（或在存储到 `request.session` 之前将这些值转换为可 JSON 序列化的对象）。尽管序列化这些值通常很简单（[`DjangoJSONEncoder`](/zh-hans/5.1/topics/serialization/#django.core.serializers.json.DjangoJSONEncoder) 可能会有所帮助），但编写一个可以可靠地获取与放入相同的东西的解码器更加脆弱。例如，你可能会冒险返回一个实际上是一个字符串的 `datetime`，只是碰巧与为 `datetime` 选择的相同格式。

你的序列化器类必须实现两个方法，`dumps(self, obj)` 和 `loads(self, data)`，分别用于序列化和反序列化会话数据的字典。

### 会话对象指南

- 在 `request.session` 上使用普通的 Python 字符串作为字典键。这更像是一种约定而不是硬性规则。
- 以下划线开头的会话字典键被保留供 Django 内部使用。
- 不要用新对象覆盖 `request.session`，也不要访问或设置它的属性。像使用 Python 字典一样使用它。

### 示例

这个简单的视图在用户发布评论后将一个变量 `has_commented` 设置为 `True`。它不允许用户发布多次评论：

```
def post_comment(request, new_comment):
    if request.session.get("has_commented", False):
        return HttpResponse("You've already commented.")
    c = comments.Comment(comment=new_comment)
    c.save()
    request.session["has_commented"] = True
    return HttpResponse("Thanks for your comment!")
```

这个简单的视图登录了站点的一个“成员”：

```
def login(request):
    m = Member.objects.get(username=request.POST["username"])
    if m.check_password(request.POST["password"]):
        request.session["member_id"] = m.id
        return HttpResponse("You're logged in.")
    else:
        return HttpResponse("Your username and password didn't match.")
```

...而这一个则根据上面的 `login()` 方法将成员注销：

```
def logout(request):
    try:
        del request.session["member_id"]
    except KeyError:
        pass
    return HttpResponse("You're logged out.")
```

标准的 [`django.contrib.auth.logout()`](/zh-hans/5.1/topics/auth/default/#django.contrib.auth.logout) 函数实际上做了更多的工作，以防止意外的数据泄露。它调用 `request.session` 的 [`flush()`](#django.contrib.sessions.backends.base.SessionBase.flush) 方法。我们在这个示例中只是演示如何使用会话对象，而不是完整的 `logout()` 实现。

## 设置测试 cookie

作为一种便利，Django 提供了一种测试用户浏览器是否接受 cookie 的方法。在视图中调用 `request.session` 的 [`set_test_cookie()`](#django.contrib.sessions.backends.base.SessionBase.set_test_cookie) 方法，并在后续的视图中调用 [`test_cookie_worked()`](#django.contrib.sessions.backends.base.SessionBase.test_cookie_worked) -- 不要在同一个视图调用中执行。

这种 `set_test_cookie()` 和 `test_cookie_worked()` 之间的分离方式是由于 cookie 的工作方式所必需的。当你设置一个 cookie 时，你实际上无法知道浏览器是否接受了它，直到浏览器的下一个请求。

使用 [`delete_test_cookie()`](#django.contrib.sessions.backends.base.SessionBase.delete_test_cookie) 来清理。在验证了测试 cookie 有效后执行这个操作。

这里是一个典型的用法示例：

```
from django.http import HttpResponse
from django.shortcuts import render

def login(request):
    if request.method == "POST":
        if request.session.test_cookie_worked():
            request.session.delete_test_cookie()
            return HttpResponse("You're logged in.")
        else:
            return HttpResponse("Please enable cookies and try again.")
    request.session.set_test_cookie()
    return render(request, "foo/login_form.html")
```

> **Changed in Django 5.1**
>
> 增加了在异步视图函数中设置测试 cookie 的支持。

## 在视图外使用会话

> **Note**
>
> 本节中的示例直接从 `django.contrib.sessions.backends.db` 后端导入 `SessionStore` 对象。在你自己的代码中，你应该考虑从由 [`SESSION_ENGINE`](/zh-hans/5.1/ref/settings/#std-setting-SESSION_ENGINE) 指定的会话引擎中导入 `SessionStore`，如下所示：
>
> ```pycon
> >>> from importlib import import_module
> >>> from django.conf import settings
> >>> SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
> ```

在视图之外，有一个 API 可用于操作会话数据：

```pycon
>>> from django.contrib.sessions.backends.db import SessionStore
>>> s = SessionStore()
>>> # stored as seconds since epoch since datetimes are not serializable in JSON.
>>> s["last_login"] = 1376587691
>>> s.create()
>>> s.session_key
'2b1189a188b44ad18c35e113ac6ceead'
>>> s = SessionStore(session_key="2b1189a188b44ad18c35e113ac6ceead")
>>> s["last_login"]
1376587691
```

`SessionStore.create()` 用于创建一个新的会话（即，一个未从会话存储加载并且 `session_key=None` 的会话）。`save()` 用于保存一个已存在的会话（即，从会话存储加载的会话）。在新会话上调用 `save()` 也可能有效，但有小概率生成与现有会话冲突的 `session_key`。`create()` 调用 `save()` 并循环直到生成一个未使用的 `session_key`。

如果你使用的是 `django.contrib.sessions.backends.db` 后端，每个会话都是一个普通的 Django 模型。`Session` 模型在 [django/contrib/sessions/models.py](https://github.com/django/django/blob/stable/5.1.x/django/contrib/sessions/models.py) 中定义。因为它是一个普通的模型，所以你可以使用普通的 Django 数据库 API 访问会话：

```pycon
>>> from django.contrib.sessions.models import Session
>>> s = Session.objects.get(pk="2b1189a188b44ad18c35e113ac6ceead")
>>> s.expire_date
datetime.datetime(2005, 8, 20, 13, 35, 12)
```

请注意，你需要调用 [`get_decoded()`](#django.contrib.sessions.base_session.AbstractBaseSession.get_decoded) 来获取会话字典。这是必要的，因为字典是以编码格式存储的：

```pycon
>>> s.session_data
'KGRwMQpTJ19hdXRoX3VzZXJfaWQnCnAyCkkxCnMuMTExY2ZjODI2Yj...'
>>> s.get_decoded()
{'user_id': 42}
```

## 当会话被保存时

默认情况下，只有当会话被修改时，Django 才会保存到会话数据库，也就是说，如果它的字典值中有任何一个被赋值或删除：

```
# Session is modified.
request.session["foo"] = "bar"

# Session is modified.
del request.session["foo"]

# Session is modified.
request.session["foo"] = {}

# Gotcha: Session is NOT modified, because this alters
# request.session['foo'] instead of request.session.
request.session["foo"]["bar"] = "baz"
```

在上面的示例的最后一种情况中，我们可以通过在会话对象上设置 `modified` 属性来明确告诉会话对象它已被修改：

```
request.session.modified = True
```

要更改这个默认行为，将 [`SESSION_SAVE_EVERY_REQUEST`](/zh-hans/5.1/ref/settings/#std-setting-SESSION_SAVE_EVERY_REQUEST) 设置为 `True`。当设置为 `True` 时，Django 将在每个请求上将会话保存到数据库中。

请注意，只有在创建或修改会话时才会发送会话 cookie。如果 [`SESSION_SAVE_EVERY_REQUEST`](/zh-hans/5.1/ref/settings/#std-setting-SESSION_SAVE_EVERY_REQUEST) 是 `True`，则会在每个请求上发送会话 cookie。

类似地，会话 cookie 的 `expires` 部分在每次发送会话 cookie 时都会更新。

如果响应状态代码为 500，会话不会被保存。

## 浏览器长度会话与持久会话

你可以使用 [`SESSION_EXPIRE_AT_BROWSER_CLOSE`](/zh-hans/5.1/ref/settings/#std-setting-SESSION_EXPIRE_AT_BROWSER_CLOSE) 设置来控制会话框架是否使用浏览器长度会话还是持久会话。

默认情况下， [`SESSION_EXPIRE_AT_BROWSER_CLOSE`](/zh-hans/5.1/ref/settings/#std-setting-SESSION_EXPIRE_AT_BROWSER_CLOSE) 设置为 `False`，这意味着会话 cookie 将在用户的浏览器中存储，时间为 [`SESSION_COOKIE_AGE`](/zh-hans/5.1/ref/settings/#std-setting-SESSION_COOKIE_AGE) 的值。如果你不希望用户每次打开浏览器时都要登录，可以使用这个设置。

如果 [`SESSION_EXPIRE_AT_BROWSER_CLOSE`](/zh-hans/5.1/ref/settings/#std-setting-SESSION_EXPIRE_AT_BROWSER_CLOSE) 设置为 `True`，Django 将使用浏览器长度的 cookie -- 即当用户关闭浏览器时立即过期的 cookie。如果你希望用户每次打开浏览器时都需要登录，可以使用这个设置。

这个设置是全局默认设置，可以通过在每个会话级别上显式调用 `request.session` 的 [`set_expiry()`](#django.contrib.sessions.backends.base.SessionBase.set_expiry) 方法来覆盖，如上面在 [在视图中使用会话](#using-sessions-in-views) 部分所述。

> **Note**
>
> 一些浏览器（例如Chrome）提供了允许用户在关闭并重新打开浏览器后继续浏览会话的设置。在某些情况下，这可能会影响到 [`SESSION_EXPIRE_AT_BROWSER_CLOSE`](/zh-hans/5.1/ref/settings/#std-setting-SESSION_EXPIRE_AT_BROWSER_CLOSE) 设置，并阻止会话在关闭浏览器时过期。在测试启用了 [`SESSION_EXPIRE_AT_BROWSER_CLOSE`](/zh-hans/5.1/ref/settings/#std-setting-SESSION_EXPIRE_AT_BROWSER_CLOSE) 设置的 Django 应用程序时，请注意这一点。

## 清除会话存储

随着用户在你的网站上创建新的会话，会话数据可能会在你的会话存储中累积。如果你使用的是数据库后端，`django_session` 数据库表会增长。如果你使用的是文件后端，你的临时目录会包含越来越多的文件。

要理解这个问题，考虑一下数据库后端的情况。当用户登录时，Django 会向 `django_session` 数据库表添加一行记录。每当会话数据发生更改时，Django 会更新此行记录。如果用户手动注销，Django 会删除这行记录。但如果用户没有注销，这行记录永远不会被删除。文件后端也有类似的过程。

Django *不会* 自动清除过期的会话。因此，你需要定期清除过期的会话。Django 为此提供了一个清理管理命令：[`clearsessions`](/zh-hans/5.1/ref/django-admin/#django-admin-clearsessions)。建议定期调用这个命令，例如作为每日的 cron 作业。

请注意，缓存后端不会受到这个问题的影响，因为缓存会自动删除过时的数据。同样，cookie 后端也不会受到影响，因为会话数据是由用户的浏览器存储的。

## 配置

一些 [Django 设置](/zh-hans/5.1/ref/settings/#settings-sessions) 允许你控制会话的行为：

- [`SESSION_CACHE_ALIAS`](/zh-hans/5.1/ref/settings/#std-setting-SESSION_CACHE_ALIAS)
- [`SESSION_COOKIE_AGE`](/zh-hans/5.1/ref/settings/#std-setting-SESSION_COOKIE_AGE)
- [`SESSION_COOKIE_DOMAIN`](/zh-hans/5.1/ref/settings/#std-setting-SESSION_COOKIE_DOMAIN)
- [`SESSION_COOKIE_HTTPONLY`](/zh-hans/5.1/ref/settings/#std-setting-SESSION_COOKIE_HTTPONLY)
- [`SESSION_COOKIE_NAME`](/zh-hans/5.1/ref/settings/#std-setting-SESSION_COOKIE_NAME)
- [`SESSION_COOKIE_PATH`](/zh-hans/5.1/ref/settings/#std-setting-SESSION_COOKIE_PATH)
- [`SESSION_COOKIE_SAMESITE`](/zh-hans/5.1/ref/settings/#std-setting-SESSION_COOKIE_SAMESITE)
- [`SESSION_COOKIE_SECURE`](/zh-hans/5.1/ref/settings/#std-setting-SESSION_COOKIE_SECURE)
- [`SESSION_ENGINE`](/zh-hans/5.1/ref/settings/#std-setting-SESSION_ENGINE)
- [`SESSION_EXPIRE_AT_BROWSER_CLOSE`](/zh-hans/5.1/ref/settings/#std-setting-SESSION_EXPIRE_AT_BROWSER_CLOSE)
- [`SESSION_FILE_PATH`](/zh-hans/5.1/ref/settings/#std-setting-SESSION_FILE_PATH)
- [`SESSION_SAVE_EVERY_REQUEST`](/zh-hans/5.1/ref/settings/#std-setting-SESSION_SAVE_EVERY_REQUEST)
- [`SESSION_SERIALIZER`](/zh-hans/5.1/ref/settings/#std-setting-SESSION_SERIALIZER)

## 会话安全

站点内的子域名可以在整个域上为客户端设置 cookie。如果允许来自不受信任用户控制的子域的 cookie，这将可能导致会话固定。

例如，攻击者可以登录到 `good.example.com` 并获得他们账户的有效会话。如果攻击者控制 `bad.example.com`，他们可以使用它来将他们的会话密钥发送给你，因为子域名被允许在 `*.example.com` 上设置 cookie。当你访问 `good.example.com` 时，你将以攻击者的身份登录，并可能不小心将你的敏感个人数据（如信用卡信息）输入到攻击者的帐户中。

另一个可能的攻击是，如果 `good.example.com` 将其 [`SESSION_COOKIE_DOMAIN`](/zh-hans/5.1/ref/settings/#std-setting-SESSION_COOKIE_DOMAIN) 设置为 `"example.com"`，这将导致该站点的会话 cookie 被发送到 `bad.example.com`。

## 技术细节

- 当使用 [`JSONSerializer`](#django.contrib.sessions.serializers.JSONSerializer) 时，会话字典接受任何可以进行 [`json`](https://docs.python.org/3/library/json.html#module-json) 序列化的值。
- 会话数据保存在名为 `django_session` 的数据库表中。
- Django 只有它需要的时候才会发送 cookie 。如果你不想设置任何会话数据，它将不会发送会话 cookie 。

### `SessionStore` 对象

在内部处理会话时，Django 使用相应会话引擎的会话存储对象。按照约定，会话存储对象类的名称为 `SessionStore`，并位于由 [`SESSION_ENGINE`](/zh-hans/5.1/ref/settings/#std-setting-SESSION_ENGINE) 指定的模块中。

Django 中所有可用的 `SessionStore` 子类都实现了以下数据操作方法：

- `exists()`
- `create()`
- `save()`
- `delete()`
- `load()`
- [`clear_expired()`](#django.contrib.sessions.backends.base.SessionBase.clear_expired)

通过使用 `sync_to_async()` 包装这些方法，提供了它们的异步接口。如果有原生的异步实现，可以直接实现它们：

- `aexists()`
- `acreate()`
- `asave()`
- `adelete()`
- `aload()`
- [`aclear_expired()`](#django.contrib.sessions.backends.base.SessionBase.aclear_expired)

为了构建自定义的会话引擎或自定义现有的引擎，你可以创建一个继承自 [`SessionBase`](#django.contrib.sessions.backends.base.SessionBase) 或任何其他现有的 `SessionStore` 类的新类。

你可以扩展会话引擎，但通常在使用数据库后端的会话引擎时需要额外的努力（详见下一节的详细信息）。

> **Changed in Django 5.1**
>
> 增加了 `aexists()`、`acreate()`、`asave()`、`adelete()`、`aload()` 和 `aclear_expired()` 方法。

## 扩展数据库后端的会话引擎

可以通过继承 [`AbstractBaseSession`](#django.contrib.sessions.base_session.AbstractBaseSession) 和任何 `SessionStore` 类来创建一个基于 Django 中包含的数据库后端会话引擎（即 `db` 和 `cached_db` ）的自定义会话引擎。

`AbstractBaseSession` 和 `BaseSessionManager` 可以从 `django.contrib.sessions.base_session` 中导入，这样可以在 [`INSTALLED_APPS`](/zh-hans/5.1/ref/settings/#std-setting-INSTALLED_APPS) 中不包括 `django.contrib.sessions` 的情况下导入它们。

#### `class base_session.AbstractBaseSession`

抽象的基础会话模型。

#### `session_key`

主键。该字段本身最多可包含 40 个字符。当前的实现生成一个 32 个字符的字符串（由数字和小写 ASCII 字母的随机序列组成）。

#### `session_data`

一个包含编码和序列化的会话字典的字符串。

#### `expire_date`

一个指示会话过期时间的日期时间。

已过期的会话对用户不可用，但它们仍然可能存储在数据库中，直到运行 [`clearsessions`](/zh-hans/5.1/ref/django-admin/#django-admin-clearsessions) 管理命令。

#### `classmethod get_session_store_class()`

返回一个与此会话模型一起使用的会话存储类。

#### `get_decoded()`

返回解码后的会话数据。

解码由会话存储类执行。

你还可以通过继承 [`BaseSessionManager`](#django.contrib.sessions.base_session.BaseSessionManager) 来自定义模型管理器：

#### `class base_session.BaseSessionManager`

#### `encode(session_dict)`

将给定的会话字典序列化并编码为字符串后返回。

编码由与模型类相关联的会话存储类执行。

#### `save(session_key, session_dict, expire_date)`

保存提供的会话键的会话数据，如果数据为空，则删除该会话。

通过覆盖下面描述的方法和属性来实现对 `SessionStore` 类的定制：

#### `class backends.db.SessionStore`

实现基于数据库的会话存储。

#### `classmethod get_model_class()`

如果需要，覆盖此方法以返回自定义的会话模型。

#### `create_model_instance(data)`

返回一个新的会话模型对象实例，该对象表示当前会话状态。

覆盖此方法可以在数据保存到数据库之前修改会话模型数据。

#### `class backends.cached_db.SessionStore`

实现了基于缓存的数据库后端的会话存储。

#### `cache_key_prefix`

用于构建缓存键字符串的会话键前缀。

### 例如

下面的示例展示了一个自定义的基于数据库的会话引擎，其中包括一个额外的数据库列来存储账户 ID（从而提供了查询账户的所有活动会话的选项）：

```
from django.contrib.sessions.backends.db import SessionStore as DBStore
from django.contrib.sessions.base_session import AbstractBaseSession
from django.db import models

class CustomSession(AbstractBaseSession):
    account_id = models.IntegerField(null=True, db_index=True)

    @classmethod
    def get_session_store_class(cls):
        return SessionStore

class SessionStore(DBStore):
    @classmethod
    def get_model_class(cls):
        return CustomSession

    def create_model_instance(self, data):
        obj = super().create_model_instance(data)
        try:
            account_id = int(data.get("_auth_user_id"))
        except (ValueError, TypeError):
            account_id = None
        obj.account_id = account_id
        return obj
```

如果你正在从 Django 内置的 `cached_db` 会话存储迁移到一个基于 `cached_db` 的自定义存储，你应该覆盖缓存键前缀以防止命名空间冲突：

```
class SessionStore(CachedDBStore):
    cache_key_prefix = "mysessions.custom_cached_db_backend"

    # ...
```

## URL 中的会话 ID

Django 会话框架完全且仅基于 cookie。与 PHP 不同，它不会作为最后的手段将会话 ID 放在 URL 中。这是一个有意的设计决策。这种行为不仅会使 URL 变得难看，还会使你的网站容易受到通过"Referer"头部进行会话 ID 窃取的攻击。
