Skip to main content

如何使用会话

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

启用会话

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

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

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

如果您不想使用会话,您可以从 MIDDLEWARE'django.contrib.sessions' 中删除 SessionMiddleware 线从您的 INSTALLED_APPS。它会节省你一点点开销。

配置会话引擎

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

使用数据库支持的会话

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

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

使用缓存会话

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

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

警告

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

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

配置缓存后,您可以选择两种方式在缓存中存储数据:

  • 对于简单的缓存会话存储,将 SESSION_ENGINE 设置为 "django.contrib.sessions.backends.cache"。会话数据将直接存储在缓存中。但是,会话数据可能不是持久的:如果缓存填满或缓存服务器重新启动,缓存数据可能会被清除。

  • 对于持久性,缓存的数据,将 SESSION_ENGINE 设置为 "django.contrib.sessions.backends.cached_db"。这使用直写缓存 - 每次对缓存的写入也将写入数据库。会话读取仅在数据尚未在缓存中时才使用数据库。

两个会话存储都很快,但简单的缓存更快,因为它忽略持久性。在大多数情况下,cached_db 后端将足够快,但如果您需要最后一点性能,并且愿意让会话数据不时被清除,cache 后端适合您。

如果使用 cached_db 会话后端,还需要遵循 using database-backed sessions 的配置说明。

使用基于文件的会话

要使用基于文件的会话,请将 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的工作方式,您将无法在用户下一页请求之前对其进行测试。有关详细信息,请参阅下面的 Setting test cookies

返回 TrueFalse,具体取决于用户的浏览器是否接受测试Cookie。由于Cookie的工作方式,您必须在以前的单独页面请求中调用 set_test_cookie()。有关详细信息,请参阅下面的 Setting test cookies

删除测试Cookie。使用它来清理自己。

set_expiry(value)

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

  • 如果 value 是一个整数,会话将在多少秒不活动后过期。例如,呼叫 request.session.set_expiry(300) 将使会话在5分钟后过期。

  • 如果 valuedatetimetimedelta 对象,则会话将在该特定日期/时间到期。请注意,如果使用 PickleSerializerdatetimetimedelta 值只能序列化。

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

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

读取会话不视为到期目的的活动。会话到期时间是从最后一次会话是 modified 计算的。

get_expiry_age()

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

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

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

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

get_expiry_date()

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

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

get_expire_at_browser_close()

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

clear_expired()

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

cycle_key()

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

会话序列化

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

例如,下面是一个攻击场景,如果您使用 pickle 来序列化会话数据。如果你使用 签名的cookie会话后端SECRET_KEY 被攻击者知道(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编码的数据,例如非UTF8字节,如 '\xd9' (引发 UnicodeDecodeError)。

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

class serializers.PickleSerializer

支持任意Python对象,但是,如上所述,如果攻击者知道 SECRET_KEY,则可能导致远程代码执行漏洞。

编写自己的序列化程序

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

您的序列化类必须实现两个方法,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 对象。在您自己的代码中,您应该考虑从 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

要更改此默认行为,请将 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将在用户关闭浏览器时过期。如果您希望用户每次打开浏览器时都必须登录,请使用此选项。

此设置是全局默认值,可以通过显式调用 using sessions in views 中上述的 request.sessionset_expiry() 方法在每个会话级别覆盖。

注解

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

清除会话存储

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

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

Django not 提供自动清除过期的会话。因此,您的工作是定期清除已过期的会话。 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

技术细节

  • 会话字典在使用 PickleSerializer 时使用 JSONSerializer 或任何可选的Python对象时接受任何 json 可序列化值。有关详细信息,请参阅 pickle 模块。

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

  • Django只需要发送一个cookie。如果您未设置任何会话数据,则不会发送会话Cookie。

SessionStore 对象

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

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

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

扩展大多数会话引擎是相当简单的,但是这样做与数据库支持的会话引擎通常需要一些额外的努力(详见下一节)。

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

New in Django 1.9.

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

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

class base_session.AbstractBaseSession
New in Django 1.9.

抽象基本会话模型。

session_key

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

session_data

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

expire_date

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

过期会话对用户不可用,但是,它们仍然可以存储在数据库中,直到运行 clearsessions 管理命令。

classmethod get_session_store_class()

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

get_decoded()

返回已解析的会话数据。

解码由会话存储类执行。

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

class base_session.BaseSessionManager
New in Django 1.9.
encode(session_dict)

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

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

save(session_key, session_dict, expire_date)

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

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

class backends.db.SessionStore

实现数据库支持的会话存储。

classmethod get_model_class()
New in Django 1.9.

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

create_model_instance(data)
New in Django 1.9.

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

覆盖此方法提供了在会话模型数据保存到数据库之前修改会话模型数据的功能。

class backends.cached_db.SessionStore

实现缓存的数据库支持的会话存储。

cache_key_prefix
New in Django 1.9.

添加到会话密钥以创建缓存密钥字符串的前缀。

下面的示例显示了一个自定义的数据库支持的会话引擎,其中包含一个用于存储帐户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(SessionStore, self).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'

    # ...

网址中的会话ID

Django会话框架是完全,基于cookie的。它不回落到会话ID作为最后的手段,如PHP。这是一个有意设计的决定。这种行为不仅会使网址丑陋,它使您的网站容易受到会话ID的窃取通过“Referer”头。