如何使用会话

Django 完全支持匿名会话。会话框架允许您基于每个站点访问者存储和检索任意数据。它在服务器端存储数据,并对发送和接收 Cookie 进行抽象。Cookie 包含会话 ID,而不是数据本身(除非您使用的是基于 Cookie 的后端)。

启用会话

会话是通过一段中间件实现的。

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

  • 编辑MIDDLEWARE 设置,并确保它包含 'django.contrib.sessions.middleware.SessionMiddleware'。由 django-admin startproject 创建的默认 settings.py 已激活 SessionMiddleware

如果您不想使用会话,您可以从MIDDLEWARE中删除SessionMiddleware行,并从您的INSTALLED_APPS中删除'django.contrib.sessions'。这将节省您少量开销。

配置会话引擎

默认情况下,Django 将会话存储在您的数据库中(使用模型django.contrib.sessions.models.Session)。虽然这很方便,但在某些设置中,将会话数据存储在其他位置更快,因此可以配置 Django 将会话数据存储在您的文件系统或缓存中。

使用基于数据库的会话

如果您想使用基于数据库的会话,您需要将'django.contrib.sessions'添加到您的INSTALLED_APPS设置中。

配置好安装后,运行manage.py migrate以安装存储会话数据的单个数据库表。

使用缓存会话

为了获得更好的性能,您可能希望使用基于缓存的会话后端。

要使用 Django 的缓存系统存储会话数据,您首先需要确保已配置您的缓存;有关详细信息,请参阅缓存文档

警告

只有在使用 Memcached 或 Redis 缓存后端时,才应使用基于缓存的会话。本地内存缓存后端不会长时间保留数据,因此不是一个好的选择,直接使用文件或数据库会话比通过文件或数据库缓存后端发送所有内容更快。此外,本地内存缓存后端不是多进程安全的,因此可能不适合生产环境。

如果您在CACHES中定义了多个缓存,Django 将使用默认缓存。要使用其他缓存,请将SESSION_CACHE_ALIAS设置为该缓存的名称。

配置缓存后,您必须在基于数据库的缓存和非持久性缓存之间进行选择。

缓存数据库后端(cached_db)使用直写缓存——会话写入将按顺序应用于数据库和缓存。如果写入缓存失败,则异常将通过会话记录器进行处理和记录,以避免使其他成功的写入操作失败。

Django 5.1 中的更改

添加了写入缓存时异常的处理和记录。

会话读取使用缓存,或者如果数据已从缓存中逐出则使用数据库。要使用此后端,请将SESSION_ENGINE设置为"django.contrib.sessions.backends.cached_db",并按照使用基于数据库的会话的配置说明进行操作。

缓存后端(cache)仅将会话数据存储在您的缓存中。这更快,因为它避免了数据库持久性,但是您必须考虑缓存数据被逐出时会发生什么。如果缓存已满或缓存服务器已重新启动,可能会发生逐出,这意味着会话数据将丢失,包括注销用户。要使用此后端,请将SESSION_ENGINE设置为"django.contrib.sessions.backends.cache"

可以通过使用持久性缓存(例如具有适当配置的 Redis)来使缓存后端持久化。但是,除非您的缓存肯定配置了足够的持久性,否则请选择缓存数据库后端。这避免了生产环境中不可靠的数据存储造成的边缘情况。

使用基于文件的会话

要使用基于文件的会话,请将SESSION_ENGINE设置设置为"django.contrib.sessions.backends.file"

您可能还想设置SESSION_FILE_PATH设置(默认为tempfile.gettempdir()的输出,很可能是/tmp)以控制 Django 存储会话文件的位置。请务必检查您的 Web 服务器是否具有对此位置进行读写权限。

在视图中使用会话

当激活SessionMiddleware时,每个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')

Django 5.1 中的更改

添加了aget()函数。

aset(key, value)
Django 5.1 新特性。

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

update(dict)
aupdate(dict)

异步版本aupdate()

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

Django 5.1 中的更改

添加了aupdate()函数。

pop(key, default=__not_given)
apop(key, default=__not_given)

异步版本apop()

示例:fav_color = request.session.pop('fav_color', 'blue')

Django 5.1 中的更改

添加了apop()函数。

keys()
akeys()

异步版本akeys()

Django 5.1 中的更改

添加了akeys()函数。

values()
avalues()

异步版本avalues()

Django 5.1 中的更改

添加了avalues()函数。

has_key(key)
ahas_key(key)

异步版本ahas_key()

Django 5.1 中的更改

添加了ahas_key()函数。

items()
aitems()

异步版本aitems()

Django 5.1 中的更改

添加了aitems()函数。

setdefault()
asetdefault()

异步版本asetdefault()

Django 5.1 中的更改

添加了asetdefault()函数。

clear()

它还具有以下方法:

flush()
aflush()

异步版本aflush()

删除会话中的当前会话数据并删除会话cookie。如果您想确保用户浏览器无法再次访问以前的会话数据(例如,django.contrib.auth.logout()函数会调用它),则可以使用此方法。

Django 5.1 中的更改

添加了aflush()函数。

异步版本aset_test_cookie()

设置测试cookie以确定用户浏览器是否支持cookie。由于cookie的工作方式,您必须等到用户的下一个页面请求才能测试它。有关更多信息,请参见下面的设置测试cookie

Django 5.1 中的更改

添加了aset_test_cookie()函数。

异步版本atest_cookie_worked()

根据用户浏览器是否接受测试cookie,返回TrueFalse。由于cookie的工作方式,您必须在之前的单独页面请求中调用set_test_cookie()aset_test_cookie()。有关更多信息,请参见下面的设置测试cookie

Django 5.1 中的更改

添加了atest_cookie_worked()函数。

异步版本adelete_test_cookie()

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

Django 5.1 中的更改

添加了adelete_test_cookie()函数。

返回设置SESSION_COOKIE_AGE的值。这可以在自定义会话后端中被覆盖。

set_expiry(value)
aset_expiry(value)

异步版本aset_expiry()

设置会话的过期时间。您可以传递多个不同的值。

  • 如果value是整数,则会话将在指定秒数的空闲时间后过期。例如,调用request.session.set_expiry(300)将使会话在5分钟后过期。

  • 如果valuedatetimetimedelta对象,会话将在该特定日期/时间过期。

  • 如果value0,则用户会话cookie将在用户关闭网页浏览器时过期。

  • 如果valueNone,则会话将恢复为使用全局会话过期策略。

读取会话不视为过期目的的活动。会话过期时间是从上次修改会话的时间计算的。

Django 5.1 中的更改

添加了aset_expiry()函数。

get_expiry_age()
aget_expiry_age()

异步版本aget_expiry_age()

返回此会话过期前剩余的秒数。对于没有自定义过期时间(或设置为在浏览器关闭时过期)的会话,这将等于SESSION_COOKIE_AGE

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

  • modification:会话的最后修改时间,作为datetime对象。默认为当前时间。

  • expiry:会话的过期信息,作为datetime对象,一个int(以秒为单位),或None。默认为会话中由set_expiry()/aset_expiry()存储的值(如果存在),或None

注意

此方法由会话后端用于在保存会话时确定会话过期时间(以秒为单位)。它并非真正用于此上下文之外。

特别是,虽然**可以**确定会话的剩余生命周期,**只有当**您拥有正确的modification值**并且**expiry设置为datetime对象时,您可以拥有modification值,通过手工计算过期时间更为直接。

expires_at = modification + timedelta(seconds=settings.SESSION_COOKIE_AGE)
Django 5.1 中的更改

添加了aget_expiry_age()函数。

get_expiry_date()
aget_expiry_date()

异步版本aget_expiry_date()

返回此会话将过期的日期。对于没有自定义过期时间(或设置为在浏览器关闭时过期)的会话,这将等于SESSION_COOKIE_AGE秒后的日期。

此函数接受与get_expiry_age()相同的关键字参数,并应用类似的使用说明。

Django 5.1 中的更改

添加了aget_expiry_date()函数。

get_expire_at_browser_close()
aget_expire_at_browser_close()

异步版本aget_expire_at_browser_close()

返回TrueFalse,取决于用户会话cookie是否会在用户关闭网页浏览器时过期。

Django 5.1 中的更改

添加了aget_expire_at_browser_close()函数。

clear_expired()
aclear_expired()

异步版本aclear_expired()

从会话存储中删除过期的会话。此类方法由clearsessions调用。

Django 5.1 中的更改

添加了aclear_expired()函数。

cycle_key()
acycle_key()

异步版本acycle_key()

创建一个新的会话密钥,同时保留当前会话数据。django.contrib.auth.login()调用此方法以减轻会话固定问题。

Django 5.1 中的更改

添加了acycle_key()函数。

会话序列化

默认情况下,Django 使用 JSON 序列化会话数据。您可以使用SESSION_SERIALIZER设置来自定义会话序列化格式。即使存在编写您自己的序列化器中描述的警告,我们也强烈建议坚持使用 JSON 序列化,尤其是在您使用cookie后端时

例如,如果您使用pickle序列化会话数据,则这是一个攻击场景。如果您使用签名cookie会话后端SECRET_KEY(或SECRET_KEY_FALLBACKS的任何密钥),并且攻击者知道该密钥(Django 中没有导致其泄漏的固有漏洞),则攻击者可以将其字符串插入到其会话中,该字符串在取消pickle后会在服务器上执行任意代码。执行此操作的技术很简单,并且在互联网上很容易获得。虽然cookie会话存储会对cookie存储的数据进行签名以防止篡改,但SECRET_KEY泄漏会立即升级为远程代码执行漏洞。

捆绑的序列化器

class serializers.JSONSerializer

围绕来自django.core.signing的JSON序列化器的包装器。只能序列化基本数据类型。

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

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

同样,无法编码为JSON的数据(例如非UTF8字节,如'\xd9'(这会引发UnicodeDecodeError))无法存储。

有关JSON序列化的限制的更多详细信息,请参见编写您自己的序列化器部分。

编写您自己的序列化器

请注意,JSONSerializer无法处理任意Python数据类型。通常情况下,便利性和安全性之间存在权衡。如果您希望存储更高级的数据类型,包括datetimeDecimal在JSON支持的会话中,您需要编写一个自定义序列化器(或者在将这些值存储在request.session中之前将其转换为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() 函数实际上比这做的更多,以防止意外的数据泄露。它调用 request.sessionflush() 方法。我们使用此示例来演示如何使用会话对象,而不是作为完整的 logout() 实现。

设置测试 Cookie

为了方便起见,Django 提供了一种方法来测试用户的浏览器是否接受 Cookie。在视图中调用 request.sessionset_test_cookie() 方法,并在后续视图中调用 test_cookie_worked() 方法——不要在同一个视图调用中调用。

由于 Cookie 的工作方式,set_test_cookie()test_cookie_worked() 之间的这种笨拙的分割是必要的。当你设置 Cookie 时,你实际上无法知道浏览器是否接受它,直到浏览器的下一个请求。

最好使用 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")
Django 5.1 中的更改

增加了对在异步视图函数中设置测试 Cookie 的支持。

在视图外部使用会话

注意

本节中的示例直接从 django.contrib.sessions.backends.db 后端导入 SessionStore 对象。在您自己的代码中,您应该考虑从由 SESSION_ENGINE 指定的会话引擎导入 SessionStore,如下所示。

>>> from importlib import import_module
>>> from django.conf import settings
>>> SessionStore = import_module(settings.SESSION_ENGINE).SessionStore

有一个 API 可用于在视图外部操作会话数据。

>>> 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_keycreate() 调用 save() 并循环,直到生成一个未使用的 session_key

如果您使用的是 django.contrib.sessions.backends.db 后端,则每个会话都是一个普通的 Django 模型。Session 模型定义在 django/contrib/sessions/models.py 中。因为它是一个普通的模型,您可以使用普通的 Django 数据库 API 访问会话。

>>> 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() 来获取会话字典。这是必要的,因为字典以编码格式存储。

>>> 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 设置设置为 True。设置为 True 时,Django 将在每个请求中将会话保存到数据库。

请注意,只有在创建或修改会话时才会发送会话 Cookie。如果 SESSION_SAVE_EVERY_REQUESTTrue,则会话 Cookie 将在每次请求时发送。

类似地,每次发送会话 Cookie 时都会更新会话 Cookie 的 expires 部分。

如果响应的状态码为 500,则不会保存会话。

浏览器会话与持久化会话

您可以使用 SESSION_EXPIRE_AT_BROWSER_CLOSE 设置来控制会话框架是使用浏览器会话还是持久化会话。

默认情况下,SESSION_EXPIRE_AT_BROWSER_CLOSE 设置为 False,这意味着会话 Cookie 将存储在用户的浏览器中,只要 SESSION_COOKIE_AGE 指定的时间内。如果您不希望用户每次打开浏览器都必须登录,请使用此设置。

如果 SESSION_EXPIRE_AT_BROWSER_CLOSE 设置为 True,Django 将使用浏览器会话 Cookie——一旦用户关闭浏览器就会过期的 Cookie。如果您希望用户每次打开浏览器都必须登录,请使用此设置。

此设置是一个全局默认值,可以通过显式调用 request.sessionset_expiry() 方法(如上文 在视图中使用会话 中所述)来在每个会话级别覆盖。

注意

某些浏览器(例如 Chrome)提供允许用户在关闭并重新打开浏览器后继续浏览会话的设置。在某些情况下,这可能会干扰 SESSION_EXPIRE_AT_BROWSER_CLOSE 设置,并阻止会话在浏览器关闭时过期。在测试启用了 SESSION_EXPIRE_AT_BROWSER_CLOSE 设置的 Django 应用程序时,请注意这一点。

清除会话存储

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

要理解这个问题,请考虑使用数据库后端时会发生什么。当用户登录时,Django 会向 django_session 数据库表添加一行。每次会话数据更改时,Django 都会更新此行。如果用户手动注销,Django 将删除该行。但如果用户_不_注销,则该行永远不会被删除。文件后端也发生类似的过程。

Django_不_提供过期会话的自动清除。因此,定期清除过期会话是您的工作。Django 为此提供了一个清理管理命令:clearsessions。建议定期调用此命令,例如作为每日 cron 作业。

请注意,缓存后端不会受到此问题的困扰,因为缓存会自动删除陈旧数据。Cookie 后端也不会,因为会话数据由用户的浏览器存储。

设置

一些 Django 设置 可以让您控制会话行为。

会话安全性

站点内的子域名能够为整个域名设置客户端的 Cookie。如果允许来自不受信任用户控制的子域名的 Cookie,则可能导致会话固定。

例如,攻击者可以登录 good.example.com 并获取其帐户的有效会话。如果攻击者控制 bad.example.com,则他们可以使用它将他们的会话密钥发送给您,因为子域名被允许为 *.example.com 设置 Cookie。当您访问 good.example.com 时,您将以攻击者的身份登录,并且可能无意中将您的敏感个人数据(例如信用卡信息)输入攻击者的帐户。

另一种可能的攻击是,如果 good.example.com 将其 SESSION_COOKIE_DOMAIN 设置为 "example.com",这将导致来自该站点的会话 Cookie 被发送到 bad.example.com

技术细节

  • 当使用 JSONSerializer 时,会话字典接受任何 json 可序列化的值。

  • 会话数据存储在名为 django_session 的数据库表中。

  • 只有在需要时,Django 才会发送 Cookie。如果您没有设置任何会话数据,它不会发送会话 Cookie。

SessionStore 对象

在内部处理会话时,Django 使用来自相应会话引擎的会话存储对象。按照约定,会话存储对象类名为 SessionStore,位于 SESSION_ENGINE 指定的模块中。

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

通过使用 sync_to_async() 包装这些方法,提供了一个异步接口。如果可以使用异步原生实现,则可以直接实现它们。

为了构建自定义会话引擎或自定义现有引擎,您可以创建一个继承自 SessionBase 或任何其他现有 SessionStore 类的新的类。

您可以扩展会话引擎,但是使用数据库支持的会话引擎这样做通常需要一些额外的努力(有关详细信息,请参见下一节)。

Django 5.1 中的更改

添加了 aexists()acreate()asave()adelete()aload()aclear_expired() 方法。

扩展数据库支持的会话引擎

通过继承 AbstractBaseSessionSessionStore 类,可以创建基于 Django 中包含的引擎(即 dbcached_db)的自定义数据库支持的会话引擎。

可以从 django.contrib.sessions.base_session 导入 AbstractBaseSessionBaseSessionManager,这样就可以在不包含 django.contrib.sessionsINSTALLED_APPS 中的情况下导入它们。

class base_session.AbstractBaseSession

抽象的基础会话模型。

session_key

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

session_data

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

expire_date

指定会话过期时间的日期时间。

用户无法使用已过期的会话,但是,它们可能仍会存储在数据库中,直到运行 clearsessions 管理命令。

classmethod get_session_store_class()

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

get_decoded()

返回解码后的会话数据。

解码由会话存储类执行。

您还可以通过子类化 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 窃取的攻击。

返回顶部