如何使用会话

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

启用会话

会话通过一个 middleware .

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

  • 编辑 MIDDLEWARE 设置并确保它包含 'django.contrib.sessions.middleware.SessionMiddleware' .违约 settings.py 创建的 django-admin startprojectSessionMiddleware 激活。

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

配置会话引擎

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

使用数据库支持的会话

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

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

使用缓存会话

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

要使用django的缓存系统存储会话数据,首先需要确保已配置缓存;请参见 cache documentation 有关详细信息。

警告

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

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

一旦配置了缓存,您就必须在数据库支持的缓存和非持久缓存之间进行选择。

缓存的数据库后端 (cached_db )使用直写缓存--会话写入同时应用于缓存和数据库。会话读取使用缓存,如果数据已从缓存中逐出,则使用数据库。要使用此后端,请设置 SESSION_ENGINE"django.contrib.sessions.backends.cached_db" ,并按照的配置说明操作 using database-backed sessions

缓存后端 (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'] . 这提出 KeyError 如果给定 key 尚未在会话中。

__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。由于cookies的工作方式,在用户的下一个页面请求之前,您将无法测试这个问题。见 Setting test cookies 详情请参见下文。

也返回 TrueFalse ,取决于用户的浏览器是否接受测试cookie。由于 cookies 的工作方式,你必须调用给 set_test_cookie() 在上一个单独的页面请求上。见 Setting test cookies 详情请参见下文。

删除测试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 .

备注

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

特别是,虽然它是 possible 确定会话的剩余生存期 just when 你答对了 modification 价值 and 这个 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()

返回以下任一 TrueFalse ,取决于用户的会话Cookie在用户的Web浏览器关闭时是否会过期。

clear_expired()

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

cycle_key()

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

会话序列化

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

例如,以下是一个攻击场景,如果使用 pickle 序列化会话数据。如果您正在使用 signed cookie session backendSECRET_KEY (或任何密钥 SECRET_KEY_FALLBACKS )是攻击者所知道的(Django中没有导致泄漏的固有漏洞),攻击者可以将一个字符串插入到他们的会话中,当未选中该字符串时,将在服务器上执行任意代码。这样做的技术很简单,很容易在互联网上找到。尽管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数据类型。就像经常发生的情况一样,在便利性和安全性之间存在权衡。如果您希望存储更高级的数据类型,包括 datetimeDecimal 在JSON支持的会话中,您将需要编写一个定制的序列化程序(或者在将这些值存储到 request.session )。虽然序列化这些值通常很简单 (DjangoJSONEncoder 可能是有帮助的),编写一个解码器,可以可靠地得到你放入的相同的东西是更脆弱的。例如,您冒着返回一个 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.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() 函数实际上做的比这多一点,以防止意外的数据泄漏。它叫 flush() 方法 request.session . 我们使用这个例子来演示如何使用会话对象,而不是完整的 logout() 实施。

设置测试cookie

为了方便起见,Django提供了一种测试用户浏览器是否接受cookie的方法。打电话给 set_test_cookie() 方法 request.session 在一个视图中,然后调用 test_cookie_worked() 在随后的视图中——不是在同一个视图调用中。

这种尴尬的分裂 set_test_cookie()test_cookie_worked() 是必要的,因为 cookies 的工作方式。当你设置一个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")

使用视图外的会话

备注

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

>>> 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=Nonesave() 用于保存现有会话(即从会话存储区加载的会话)。调用 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将在每次请求时发送。

同样, expires 每次发送会话cookie时都会更新会话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() 方法 request.session 如上所述 using sessions in views .

备注

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

清除会话存储

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

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

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

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

设置

少许 Django settings 允许您控制会话行为:

会话安全

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

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

另一个可能的攻击是 good.example.com 设置其 SESSION_COOKIE_DOMAIN"example.com" 这将导致来自该站点的会话cookie发送到 bad.example.com .

技术细节

  • 会话词典接受任何 json 使用时可序列化的值 JSONSerializer

  • 会话数据存储在名为 django_session .

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

这个 SessionStore 对象

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

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

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

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

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

创建一个基于django中包含的自定义数据库支持的会话引擎(即 dbcached_db )可以通过继承 AbstractBaseSession 而且 SessionStore 类。

AbstractBaseSessionBaseSessionManager 可从导入 django.contrib.sessions.base_session 以便进口时不包括 django.contrib.sessions 在里面 INSTALLED_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。