如何使用会话

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)使用直写缓存——会话写入应用于缓存和数据库。会话读取使用缓存,或在数据从缓存中驱逐时使用数据库。要使用此后端,将 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

根据用户的浏览器是否接受测试 cookie,返回 TrueFalse。由于 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

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

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

注意

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

特别是,虽然有可能恰好拥有正确的 修改并且 过期 设置为 datetime 对象时确定会话的剩余生存期,但您确实拥有 修改 值,通过手工计算过期时间会更加直接

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 设置自定义会话序列化格式。即使存在 编写你自己的序列化器 中描述的警告,我们强烈建议坚持使用 JSON 序列化,特别是如果你正在使用 cookie 后端

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

捆绑的序列化器

class serializers.JSONSerializer

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

此外,由于 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 数据类型。通常情况下,便利性和安全性之间存在权衡。如果你希望在 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(),而不是在同一视图调用中。

由于 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 对象。在您自己的代码中,您应考虑从 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。如果你希望用户每次打开浏览器时都必须登录,请使用此设置。

此设置是一个全局默认设置,可以通过显式调用 set_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 类。

您可以扩展会话引擎,但使用基于数据库的会话引擎执行此操作通常需要一些额外的精力(有关详细信息,请参见下一部分)。

扩展基于数据库的会话引擎

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

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 管理命令。

类方法 get_session_store_class()

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

get_decoded()

返回解码的会话数据。

解码由会话存储类执行。

您还可以通过子类化 BaseSessionManager来自定义模型管理器

base_session.BaseSessionManager
encode(session_dict)

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

编码由与模型类绑定的会话存储类执行。

save(session_key, session_dict, expire_date)

为提供的会话键保存会话数据,或在数据为空的情况下删除会话。

通过覆盖下面描述的方法和属性来实现 SessionStore 类的自定义

backends.db.SessionStore

实现数据库后备会话存储。

类方法 get_model_class()

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

create_model_instance(data)

返回会话模型对象的实例,它表示当前会话状态。

覆盖此方法可以修改会话模型数据,然后将其保存到数据库。

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 那样退而求其次,在 URL 中放置会话 ID。这是一个有意的设计决策。这种行为不仅会使 URL 变得丑陋,还会使你的网站容易受到通过“Referer”标头进行会话 ID 盗窃的攻击。

返回顶部