如何使用会话

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

打开会话

会话通过配置一个中间件实现的

为了打开会话,需要做下面的操作

  • 编辑设置中的 MIDDLEWARE,并确保他包含了 'django.contrib.sessions.middleware.SessionMiddleware'。通过 django-admin startproject 创建的默认 settings.py 文件是已经打开了 SessionMiddleware 这项设置的。

如果你不想使用会话功能,你可以从配置的 MIDDLEWARE 中删除 `SessionMiddleware,并且从 INSTALLED_APPS 中删除 'django.contrib.sessions'。它将会为您节省一点开销。

配置会话(session)引擎

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

使用数据库支持的会话

如果你想使用数据库支持的会话,你需要在 INSTALLED_APPS 里添加 'django.contrib.sessions'

一旦在安装中配置,运行 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 会话后端,你也需要遵循使用数据库支持的会话配置说明( 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。由于测试通过,你不需要在下一个页面请求时再次测试它。查看 Setting test cookies 获取更多信息。

返回 TrueFalse ,这取决于用户浏览器是否接受测试cookie。由于 cookie 的工作方式,你将必须在上一个独立的页面请求里调用 set_test_cookie() 。查看 Setting test cookies 获取更多信息。

删除测试cookie。使用完测试cookie后用它来删除。

返回 session cookies的失效时间,以秒为单位。默认 SESSION_COOKIE_AGE

set_expiry(value)

为会话设置过期时间。你可以传递很多不同值:

  • 如果 value 是整型,会话将在闲置数秒后过期。比如,调用 request.session.set_expiry(300) 会使得会话在5分钟后过期。
  • 如果 value 是一个 datetimetimedelta 对象,会话将在指定的 date/time 过期。注意,如果你正在使用 PickleSerializer ,那么 datetimetimedelta 的值只能序列化。
  • 如果 value0 ,则当浏览器关闭后,用户会话 cookie 将过期。
  • 如果 valueNone ,会话会恢复为全局会话过期策略。

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

get_expiry_age()

返回该会话过期的秒数。对于没有自定义过期时间的会话(或者那些设置为浏览器关闭时过期的),这等同于 SESSION_COOKIE_AGE

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

  • modification :会话的最后一次修改,当做一个 datetime 对象。默认是当前时间。
  • expiry :会话的过期信息,如一个 datetime 对象,整数(秒)或 None。默认为通过 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 来自定义会话序列化格式。即使在编写你自己的序列化程序中描述了警告,我们仍然强烈建议您坚持JSON序列化,尤其是在您使用cookie后端的情况下。

比如,如果你使用 pickle 来序列化会话数据,那么这里一个攻击场景。如果你正在使用 signed cookie session backend 并且攻击者已经知道了 SECRET_KEY (Django 并不存在会导致其泄露的固有漏洞),攻击者可以在会话里插入一个字符串,当 unpickled 时,在服务器上执行任意代码。这样做的技术很简单,在互联网上也很容易获得。尽管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中编码,例如像 '\xd9' 这种非UTF8字节(会引发 UnicodeDecodeError )不会被存储。

查看 编写自定义的序列化器 部分来获取更多有关JSON序列化局限性的内容。

class serializers.PickleSerializer

支持任何Python对象,但是,如上所述,如果 SECRET_KEY 泄露,这会导致攻击者执行远程代码的漏洞。

编写自定义的序列化器

注意这与 PickleSerializer 不同,JSONSerializer 不会处理任何Python数据类型。通常情况下,便利性和安全性之间要做出权衡取舍。如果你想在 JSON 支持的会话里存储任何高级数据类型(比如 datetimeDecimal ),你需要编写自己的序列化器(或者在存储这类值到 request.session 之前把它们转化JSON序列化类型)。虽然序列化这些值通常很简单( DjangoJSONEncoder 或许有帮助),但编写一个解码器来可靠地取回你放进去的东西就更不容易了。 例如,你要返回一个字符串格式的 datetime ,但这恰好与为 datetime s 选择的格式相同,这样会有风险。

你的序列化类必须实现两个方法( 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.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.")

这是记录成员退出的视图:

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() 实现。

测试 cookies 设置

为了方便起见,Django 提供一种方法来测试用户浏览器是否支持cookies。调用视图里 request.sessionset_test_cookie() 方法,并且在后续视图里调用 test_cookie_worked() —— 不是在同一个视图里调用。

由于 cookies 的工作方式, 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_REQUESTTrue 。当设置为 True 时,Django 会根据每个请求将会话保存到数据库中。

注意,仅在会话被创建或修改时发送会话 cookie 。如果 SESSION_SAVE_EVERY_REQUESTTrue ,则会话cookie将在每次请求时发送。

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

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

Browser-length 会话 vs 持久会话

你可以通过设置 SESSION_EXPIRE_AT_BROWSER_CLOSE 来控制会话框架是使用 browser-length 会话还是持久会话。

默认情况下, SESSION_EXPIRE_AT_BROWSER_CLOSEFalse ,这意味着会话 cookies 将保存在用户浏览器中持续 SESSION_COOKIE_AGE 的时间。如果你不想用户每次打开浏览器时必须登录,就用这个。

如果 SESSION_EXPIRE_AT_BROWSER_CLOSETrue,Django 将使用 browser-length cookies —— cookies 在用户关闭浏览器时过期。如果你想让用户每次打开浏览器时必须登录,就用这个。

这个设置是全局默认的,并且可以通过显式调用 request.sessionset_expiry() 在每个会话级别上覆盖,和之前的 using sessions in views 里描述的一样。

注解

有一些浏览器(比如谷歌浏览器)提供允许用户在关闭或重新打开浏览器后继续浏览会话的设置。有时候,这会妨碍 SESSION_EXPIRE_AT_BROWSER_CLOSE 设置,并且阻止会话在浏览器关闭时过期。如果开启了 SESSION_EXPIRE_AT_BROWSER_CLOSE 设置,在测试 Django 程序时要注意这一点。

清除会话存储

当用户创建了新会话,会话数据会累积在会话存储中。如果你正在使用数据库后端,django_session 数据库表会增加。如果你使用的是文件后端,临时目录会包含新增加的文件。

为了理解这个问题,要考虑数据库后端会发生什么。当用户登录时,Django 在 django_session 增加了一行。每次会话更改时,Django 会更新该行。如果用户手动退出,Django 会删除该行。但如果用户不退出,该行就不会被删除。文件后端也是类似的处理。

Django 没有提供过期会话自动清除的功能。因此,你需要定期清除过期会话。Django 提供了一个清除管理命令:clearsessions 。推荐在定期清除时使用该命令,例如在日常的定时任务中。

注意缓存后端不受此问题的影响,因为缓存会自动删除过期数据。cookie 后端也一样,因为会话数据通过浏览器存储。

会话安全

站点内的子域可以在客户端上为整个域设置 cookies。如果 cookies 允许来自不受新人用户控制的子域,这将使会话固定成为可能。

比如,一个攻击者登入了 good.example.com 并且为账户获得了一个有效会话。如果攻击者控制了 bad.example.com ,他们可以使用它来发送他们的会话秘钥给你(会话秘钥是保证用户跟其它计算机或者两台计算机之间安全通信会话而随机产生的加密和解密密钥),因为子域已经允许在 *.example.com 上设置 cookies 。

另一个可能的攻击是如果 good.example.com 设置它的 SESSION_COOKIE_DOMAIN"example.com" ,会导致来自站点的会话 cookies 发送到 bad.example.com

技术细节

  • 该会话目录在使用 JSONSerializer 时接受任何 json 序列化值或者当使用 PickleSerializer 时接受任何 picklable Python对象。查看 pickle 模块获取更多信息。
  • 会话数据保存在名为 django_session 的数据库表中。
  • Django 只有它需要的时候才会发送 cookie 。如果你不想设置任何会话数据,它将不会发送会话 cookie 。

SessionStore 对象

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

所有 SessionStore 类继承了 SessionBase 并且实现了数据操作方法,即:

为了搭建自定义的会话引擎或自定义已有的引擎,你可以创建一个继承自 SessionBase 的新类或任何其他已存在的 SessionStore 类。

你可以扩展会话引擎,但对于使用数据库支持的会话引擎通常需要额外的功夫(查看下节来获取更多详情)。

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

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

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 管理命令之前,它们仍可能存储在数据库中。

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变得很难看,而且使您的站点容易受到会话ID的盗用。