如何使用会话

Django 提供对匿名会话的完全支持。 这个会话框架让你可以存储和取回每个站点访客任意数据。 它在服务器端存储数据, 并以cookies的形式进行发送和接受数据. Cookie 包含会话的ID —— 而不是数据本身(除非你使用cookie based backend)。

启用会话

会话是通过一个middleware实现的。

为了启用会话功能,需要这样做:

  • 编辑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的缓存系统存储会话数据,您首先需要确保已配置缓存;有关详细信息,请参阅cache documentation

警告

你应该只在使用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 会话后端,你还需要遵循使用数据库支持的会话中的配置说明。

使用基于文件的会话

要使用基于文件的会话,请设置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') / T0>

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

它还具有这些方法:

flush()

从会话中删除当前会话数据,并删除会话cookie。 这用于确保前面的会话数据不可以再次被用户的浏览器访问(例如,django.contrib.auth.logout() 函数中就会调用它)。

设置一个测试的Cookie 来验证用户的浏览器是否支持Cookie。 因为Cookie 的工作方式,只有到用户的下一个页面才能验证。 更多信息参见下文的设置测试的Cookie

返回TrueFalse,取决于用户的浏览器是否接受测试的Cookie。 因为Cookie的工作方式,你必须在前面一个单独的页面请求中调用set_test_cookie() 更多信息参见下文的设置测试的Cookie

删除测试的Cookie。 使用这个函数来自己清理。

set_expiry(value)

设置会话的超时时间。 你可以传递一系列不同的值:

  • 如果value是一个整数,会话将在这么多秒没有活动后过期。 例如,调用request.session.set_expiry(300)将使得会话在5分钟后过期。
  • 若果value是一个datetimetimedelta 对象,会话将在这个指定的日期/时间过期。 注意datetimetimedelta值只有在你使用PickleSerializer时才可序列化。
  • 如果value0,那么会话的Cookie将在用户的浏览器关闭时过期。
  • 如果valueNone,那么会话转向使用全局的会话过期策略。

过期的计算不考虑读取会话的操作。 会话的过期从会话上次修改的时间开始计算。

get_expiry_age()

返回会话离过期的秒数。 对于没有自定义过期的会话(或者设置为浏览器关闭时过期的会话),它将等于SESSION_COOKIE_AGE

该函数接收两个可选的关键字参数:

  • modification:会话的最后一次修改时间,类型为一个datetime 对象。 默认为当前的时间。
  • None:会话的过期信息,类型为一个datetime 对象、一个int(以秒为单位)或expiry 默认为通过set_expiry()保存在会话中的值,如果没有则为None
get_expiry_date()

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

这个函数接受与get_expiry_age()一样的关键字参数。

get_expire_at_browser_close()

返回TrueFalse,取决于用户的会话Cookie在用户浏览器关闭时会不会过期。

clear_expired()

从会话的存储中清除过期的会话。 这个类方法被clearsessions调用。

cycle_key()

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

会话序列化

默认情况下,Django使用JSON序列化会话数据。 您可以使用SESSION_SERIALIZER设置自定义会话序列化格式。 即使使用Write your own serializer中描述的注意事项,我们强烈建议您使用JSON序列化,特别是在使用cookie后端时。

例如,如果您使用pickle序列化会话数据,则会出现攻击情形。 如果你使用的是signed cookie session backend 并且SECRET_KEY 被攻击者知道(Django 本身没有漏洞会导致它被泄漏),攻击者就可以在会话中插入一个字符串,在unpickle 之后可以在服务器上执行任何代码。 在因特网上这个攻击技术很简单并很容易查到。 尽管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序列化的限制的更多详细信息,请参阅Write your own serializer部分。

class serializers.PickleSerializer

支持任意Python 对象,但是正如上面描述的,可能导致远端执行代码的漏洞,如果攻击者知道了SECRET_KEY

编写自己的串行器

注意,与PickleSerializer不同,JSONSerializer 不可以处理任意的Python 数据类型。 这是常见的情况,需要在便利性和安全性之间权衡。 如果你希望在JSON 格式的会话中存储更高级的数据类型比如request.sessiondatetime,你需要编写一个自定义的序列化器(或者在保存它们到Decimal中之前转换这些值到一个可JSON 序列化的对象)。 虽然串行化这些值是相当简单的(DjangoJSONEncoder可能是有帮助的),编写可以可靠地获取相同内容的解码器更加脆弱。 例如,返回一个datetime 时,它可能实际上是与datetime 格式碰巧相同的一个字符串)。

你的序列化类必须实现两个方法,dumps(self, obj)loads(self, data) 来分别序列化和去序列化会话数据的字典。

会话对象指南

  • request.session 上使用普通的Python 字符串作为字典的键。 这主要是为了方便而不是一条必须遵守的规则。
  • 以一个下划线开始的会话字典的键被Django保留作为内部使用。
  • 不要用新的对象覆盖request.session,且不要访问或设置它的属性。 要像Python 字典一样使用它。

实例¶ T0>

下面这个简单的视图在一个用户提交一个评论后设置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是一个很好的实践。 请在你已经验证测试的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 对象。 在你的代码中,你应该从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_REQUESTTrue 当设置为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 —— 用户关闭他们的浏览器时立即过期。 如果你想让大家在每次打开浏览器时都需要登录时可以这样使用。

这个设置是一个全局的默认值,可以通过显式地调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的控制权,那么他可以使用这个域名来发送他的会话ID给你,因为子域名允许在*.example.com上设置Cookie。 当你访问good.example.com时,你将以攻击者身份登录且不会察觉到并输入你的敏感的个人信息(例如,信用卡信息)到攻击者的账号中。

另外一个可能的攻击是,如果bad.example.com设置它的 SESSION_COOKIE_DOMAINgood.example.com ,这将导致来自该站点的会话Cookie 被发送到".example.com"

技术细节

  • 当使用PickleSerializer时,会话字典使用JSONSerializer或任何可挑选的Python对象时,可以接受任何json可序列化值。 更多信息参见pickle 模块。
  • 会话数据存储在数据中名为django_session 的表中。
  • Django 只发送它需要的Cookie。 如果你没有设置任何会话数据,它将不会发送会话Cookie。

SessionStore对象

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

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

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

扩展大多数会话引擎是非常简单的,但是使用数据库支持的会话引擎通常需要一些额外的工作(详见下一节)。

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

Creating a custom database-backed session engine built upon those included in Django (namely db and cached_db) may be done by inheriting AbstractBaseSession and either SessionStore class.

AbstractBaseSession and BaseSessionManager are importable from django.contrib.sessions.base_session so that they can be imported without including django.contrib.sessions in INSTALLED_APPS.

class base_session.AbstractBaseSession

抽象基础会话模型。

session_key

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

session_data

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

expire_date

datetime指定会话到期时间。

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

classmethod get_session_store_class()

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

get_decoded T0>()¶ T1>

返回解码的会话数据。

会话存储类执行解码。

您还可以通过子类化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

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

实施例¶ T0>

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

    # ...

URL中的会话ID

Django 会话框架完全地、唯一地基于Cookie。 它不像PHP一样,实在没办法就把会话的ID放在URL 中。 这是一个故意的设计。 这个行为不仅使得URL变得丑陋,还使得你的网站易于受到通过"Referer" 头部窃取会话ID的攻击。