如何使用会话

Django 提供了对匿名会话的全面支持。会话框架允许你在每个站点访问者的基础上存储和检索任意数据。它将数据存储在服务器端并抽象了 cookie 的发送和接收。Cookies 包含会话 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)使用写透缓存 - 会话写入同时应用于缓存和数据库。会话读取使用缓存,或者如果数据已从缓存中逐出,则使用数据库。要使用此后端,请将 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)

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

pop(key, default=__not_given)

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

keys()
items()
setdefault()
clear()

它也有以下方法:

flush()

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

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

返回 TrueFalse,取决于用户的浏览器是否接受了测试 cookie。由于 cookie 的工作方式,你必须在前一个独立的页面请求上调用 set_test_cookie()。有关更多信息,请参阅下面的 设置测试 cookie

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

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

set_expiry(value)

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

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

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

get_expiry_age()

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

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

  • modification:会话的最后修改时间,作为一个 datetime 对象。默认为当前时间。
  • expiry:会话的过期信息,作为一个 datetime 对象、一个 int`(以秒为单位)或 ``None`。默认为由 set_expiry() 存储在会话中的值(如果存在),否则为 None

备注

这个方法用于会话后端在保存会话时确定会话过期时间的秒数。它实际上并不打算在那种上下文之外使用。

特别是,尽管在你拥有正确的 modification expiry 设置为 datetime 对象的情况下可以 可能 确定会话的剩余寿命,但在你有 modification 值的情况下,手动计算到期时间更加直接:

expires_at = modification + timedelta(seconds=settings.SESSION_COOKIE_AGE)
get_expiry_date()

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

此函数接受与 get_expiry_age() 相同的关键字参数,并适用类似的用法注意事项。

get_expire_at_browser_close()

根据用户的会话 cookie 是否在用户关闭 Web 浏览器时过期,返回 TrueFalse

clear_expired()

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

cycle_key()

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

会话序列化

默认情况下,Django使用 JSON 对会话数据进行序列化。你可以使用 SESSION_SERIALIZER 设置来自定义会话序列化格式。尽管在 编写你自己的序列化器 中描述了一些注意事项,但我们强烈建议 特别是如果你使用的是基于 cookie 的后端,坚持使用 JSON 序列化。

例如,如果你使用 pickle 来序列化会话数据,以下是一个攻击场景。如果你使用的是 签名 cookie 会话后端 并且 SECRET_KEY (或 SECRET_KEY_FALLBACKS 的任何密钥)被攻击者知道(Django 本身没有内在的漏洞会导致其泄露),攻击者可以在他们的会话中插入一个字符串,当反序列化时,它在服务器上执行任意代码。这种技巧很简单,而且在互联网上很容易找到。尽管 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 的数据,例如不能编码为 UTF-8 的字节,如 '\xd9' (会引发 UnicodeDecodeError),也无法存储。

请参阅 编写你自己的序列化器 部分以获取关于 JSON 序列化的限制的更多详细信息。

class serializers.PickleSerializer

支持任意的 Python 对象,但如上所述,如果 SECRET_KEYSECRET_KEY_FALLBACKS 的任何密钥被攻击者知道,可能会导致远程代码执行漏洞。

4.1 版后已移除: 由于远程代码执行的风险,这个序列化器已经被弃用,并将在 Django 5.0 中删除。

编写你自己的序列化器

请注意, JSONSerializer 不能处理任意的 Python 数据类型。通常情况下,方便和安全之间存在权衡。如果你希望在 JSON 支持的会话中存储更高级的数据类型,包括 datetimeDecimal,你需要编写一个自定义的序列化器(或在存储到 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() -- 不要在同一个视图调用中执行。

这种 set_test_cookie()test_cookie_worked() 之间的分离方式是由于 cookie 的工作方式所必需的。当你设置一个 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.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 的 expires 部分在每次发送会话 cookie 时都会更新。

如果响应状态代码为 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 后端也不会受到影响,因为会话数据是由用户的浏览器存储的。

会话安全

站点内的子域名可以在整个域上为客户端设置 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 类都继承自 SessionBase 并实现了数据操作方法,具体如下:

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

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

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

可以通过继承 AbstractBaseSession 和任何 SessionStore 类来创建一个基于 Django 中包含的数据库后端会话引擎(即 dbcached_db )的自定义会话引擎。

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

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 窃取的攻击。

Back to Top