如何使用会话 — Django 文档

来自菜鸟教程
Django/docs/3.2.x/topics/http/sessions
跳转至:导航、​搜索

如何使用会话

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

启用会话

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

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

  • 编辑 :setting:`MIDDLEWARE` 设置并确保它包含 'django.contrib.sessions.middleware.SessionMiddleware'django-admin startproject创建的默认settings.py激活了SessionMiddleware

如果您不想使用会话,您不妨从 :setting:`MIDDLEWARE`'django.contrib.sessions' 中删除 :setting 中的 SessionMiddleware 行:`INSTALLED_APPS`。 它会为您节省一点开销。


配置会话引擎

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

使用数据库支持的会话

如果要使用数据库支持的会话,则需要将 'django.contrib.sessions' 添加到 :setting:`INSTALLED_APPS` 设置。

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


使用缓存会话

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

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

警告

如果您使用的是 Memcached 缓存后端,则应仅使用基于缓存的会话。 本地内存缓存后端不会保留足够长的数据,因此不是一个好的选择,直接使用文件或数据库会话而不是通过文件或数据库缓存后端发送所有内容会更快。 此外,本地内存缓存后端不是多进程安全的,因此可能不是生产环境的好选择。


如果在 :setting:`CACHES` 中定义了多个缓存,Django 将使用默认缓存。 要使用另一个缓存,请将 :setting:`SESSION_CACHE_ALIAS` 设置为该缓存的名称。

配置缓存后,您有两种选择如何在缓存中存储数据:

  • :setting:`SESSION_ENGINE` 设置为 "django.contrib.sessions.backends.cache" 以实现简单的缓存会话存储。 会话数据将直接存储在您的缓存中。 但是,会话数据可能不是持久的:如果缓存填满或缓存服务器重新启动,缓存的数据可能会被逐出。
  • 对于持久的缓存数据,将 :setting:`SESSION_ENGINE` 设置为 "django.contrib.sessions.backends.cached_db"。 这使用直写缓存——每次写入缓存也将写入数据库。 如果数据不在缓存中,会话读取仅使用数据库。

两个会话存储都非常快,但简单缓存更快,因为它不考虑持久性。 在大多数情况下,cached_db 后端足够快,但如果您需要最后一点性能,并愿意不时清除会话数据,cache 后端是为你。

如果您使用 cached_db 会话后端,您还需要按照 使用数据库支持的会话 的配置说明进行操作。


使用基于文件的会话

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

您可能还想设置 :setting:`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() 函数调用它),则使用此方法。

set_test_cookie()

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

test_cookie_worked()

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

delete_test_cookie()

删除测试cookie。 用它来清理你自己。

get_session_cookie_age()

返回会话 cookie 的年龄,以秒为单位。 默认为 :setting:`SESSION_COOKIE_AGE`

set_expiry(value)

设置会话的到期时间。 您可以传递许多不同的值:

  • 如果 value 是一个整数,会话将在闲置数秒后过期。 例如,调用 request.session.set_expiry(300) 将使会话在 5 分钟后过期。

  • 如果 valuedatetimetimedelta 对象,则会话将在该特定日期/时间到期。 请注意,datetimetimedelta 值仅在您使用 PickleSerializer 时才可序列化。

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

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

出于到期目的,读取会话不被视为活动。 会话过期时间是从会话最后一次 修改 开始计算的。

get_expiry_age()

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

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

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

  • expiry:会话的到期信息,作为 datetime 对象、int(以秒为单位)或 None。 默认为 set_expiry() 存储在会话中的值,如果有的话,或者 None

get_expiry_date()

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

此函数接受与 get_expiry_age() 相同的关键字参数。

get_expire_at_browser_close()

返回 TrueFalse,这取决于用户的 Web 浏览器关闭时用户的会话 cookie 是否会过期。

clear_expired()

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

cycle_key()

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

会话序列化

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

例如,如果您使用 pickle 序列化会话数据,则这是一个攻击场景。 如果您使用 签名的 cookie 会话后端:setting:`SECRET_KEY` 被攻击者知道(Django 中不存在导致其泄漏的固有漏洞) ),攻击者可以在他们的会话中插入一个字符串,当它解压时,在服务器上执行任意代码。 这样做的技术很简单,而且很容易在互联网上找到。 尽管 cookie 会话存储对 cookie 存储的数据进行签名以防止篡改,但 :setting:`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 序列化限制的更多详细信息,请参阅 编写您自己的序列化程序 部分。

class serializers.PickleSerializer
支持任意 Python 对象,但如上所述,如果 :setting:`SECRET_KEY` 被攻击者知道,则可能导致远程代码执行漏洞。


编写自己的序列化程序

请注意,与 PickleSerializer 不同,JSONSerializer 不能处理任意 Python 数据类型。 通常情况下,在便利性和安全性之间存在权衡。 如果您希望在 JSON 支持的会话中存储更高级的数据类型,包括 datetimeDecimal,您将需要编写自定义序列化程序(或将这些值转换为 JSON 可序列化对象,然后再将它们存储在request.session)。 虽然序列化这些值通常很简单(DjangoJSONEncoder 可能会有所帮助),但编写一个可以可靠地取回您放入的相同内容的解码器则更加脆弱。 例如,您冒着返回 datetime 的风险,该字符串实际上是一个恰好与为 datetime 选择的格式相同的字符串)。

您的序列化程序类必须实现两个方法,dumps(self, obj)loads(self, data),分别对会话数据字典进行序列化和反序列化。


会话对象指南

  • 使用普通 Python 字符串作为 request.session 上的字典键。 这与其说是硬性规定,不如说是一种惯例。
  • 以下划线开头的会话字典键保留供 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.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.contrib.sessions.backends.db 后端导入 SessionStore 对象。 在您自己的代码中,您应该考虑从 :setting:`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_key 的可能性很小。 create() 调用 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

要更改此默认行为,请将 :setting:`SESSION_SAVE_EVERY_REQUEST` 设置设置为 True。 当设置为 True 时,Django 将在每次请求时将会话保存到数据库中。

请注意,会话 cookie 仅在创建或修改会话时发送。 如果 :setting:`SESSION_SAVE_EVERY_REQUEST`True,会话 cookie 将在每个请求上发送。

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

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


浏览器长度会话对比 持续会话

您可以控制会话框架是使用浏览器长度的会话还是使用浏览器长度的会话。 具有 :setting:`SESSION_EXPIRE_AT_BROWSER_CLOSE` 设置的持久会话。

默认情况下,:setting:`SESSION_EXPIRE_AT_BROWSER_CLOSE` 设置为 False,这意味着会话 cookie 将在用户的浏览器中存储 :setting:`SESSION_COOKIE_AGE`[ X195X]。 如果您不希望人们每次打开浏览器时都必须登录,请使用此选项。

如果 :setting:`SESSION_EXPIRE_AT_BROWSER_CLOSE` 设置为 True,Django 将使用浏览器长度的 cookie——一旦用户关闭浏览器就会过期的 cookie。 如果您希望人们每次打开浏览器时都必须登录,请使用此选项。

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

笔记

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


清除会话存储

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

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

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

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


会话安全

站点内的子域能够在客户端上为整个域设置 cookie。 如果不受受信任用户控制的子域允许 cookie,则这使得会话固定成为可能。

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

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


技术细节

  • 会话字典在使用 JSONSerializer 时接受任何 json 可序列化值,或在使用 PickleSerializer 时接受任何可腌制的 Python 对象。 有关详细信息,请参阅 pickle 模块。
  • 会话数据存储在名为 django_session 的数据库表中。
  • Django 仅在需要时发送 cookie。 如果您不设置任何会话数据,则不会发送会话 cookie。

SessionStore 对象

在内部处理会话时,Django 使用来自相应会话引擎的会话存储对象。 按照惯例,会话存储对象类被命名为 SessionStore,并位于由 :setting:`SESSION_ENGINE` 指定的模块中。

Django 中所有可用的 SessionStore 类都继承自 SessionBase 并实现数据操作方法,即:

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

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


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

可以通过继承 AbstractBaseSessionSessionStore 中包含的那些来创建一个基于 Django 的自定义数据库支持的会话引擎(即 dbcached_db)班级。

AbstractBaseSessionBaseSessionManager 可从 django.contrib.sessions.base_session 导入,因此它们可以在 :setting:`INSTALLED_APPS` 中不包含 django.contrib.sessions 的情况下导入。

class base_session.AbstractBaseSession

抽象的基本会话模型。

session_key

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

session_data

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

expire_date

指定会话何时到期的日期时间。

过期的会话对用户不可用,但是,它们可能仍存储在数据库中,直到运行 :djadmin:`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。