如何使用会话

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 获取详情。

警告

You should only use cache-based sessions if you're using the Memcached or Redis cache backend. The local-memory cache backend doesn't retain data long enough to be a good choice, and it'll be faster to use file or database sessions directly instead of sending everything through the file or database cache backends. Additionally, the local-memory cache backend is NOT multi-process safe, therefore probably not a good choice for production environments.

如果你在 CACHES 定义了多缓存,Django 会使用默认缓存。如果要使用其他缓存,请将 SESSION_CACHE_ALIAS 设置为该缓存名。

Once your cache is configured, you have to choose between a database-backed cache or a non-persistent cache.

The cached database backend (cached_db) uses a write-through cache -- session writes are applied to both the cache and the database. Session reads use the cache, or the database if the data has been evicted from the cache. To use this backend, set SESSION_ENGINE to "django.contrib.sessions.backends.cached_db", and follow the configuration instructions for the using database-backed sessions.

The cache backend (cache) stores session data only in your cache. This is faster because it avoids database persistence, but you will have to consider what happens when cache data is evicted. Eviction can occur if the cache fills up or the cache server is restarted, and it will mean session data is lost, including logging out users. To use this backend, set SESSION_ENGINE to "django.contrib.sessions.backends.cache".

The cache backend can be made persistent by using a persistent cache, such as Redis with appropriate configuration. But unless your cache is definitely configured for sufficient persistence, opt for the cached database backend. This avoids edge cases caused by unreliable data storage in production.

使用基于文件的会话

要使用基于文件的会话,需要设置 SESSION_ENGINE"django.contrib.sessions.backends.file"

You might also want to set the SESSION_FILE_PATH setting (which defaults to output from tempfile.gettempdir(), most likely /tmp) to control where Django stores session files. Be sure to check that your web server has permissions to read and write to this location.

在视图中使用会话

当激活 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后用它来删除。

Returns the value of the setting SESSION_COOKIE_AGE. This can be overridden in a custom session backend.

set_expiry(value)

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

  • 如果 value 是整型,会话将在闲置数秒后过期。比如,调用 request.session.set_expiry(300) 会使得会话在5分钟后过期。
  • If value is a datetime or timedelta object, the session will expire at that specific date/time.
  • If value is 0, the user's session cookie will expire when the user's web browser is closed.
  • 如果 valueNone ,会话会恢复为全局会话过期策略。

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

get_expiry_age()

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

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

  • modification :会话的最后一次修改,当做一个 datetime 对象。默认是当前时间。
  • expiry :会话的过期信息,如一个 datetime 对象,整数(秒)或 None。默认为通过 set_expiry() 存储在会话中的值,或 None

备注

This method is used by session backends to determine the session expiry age in seconds when saving the session. It is not really intended for usage outside of that context.

In particular, while it is possible to determine the remaining lifetime of a session just when you have the correct modification value and the expiry is set as a datetime object, where you do have the modification value, it is more straight-forward to calculate the expiry by-hand:

expires_at = modification + timedelta(seconds=settings.SESSION_COOKIE_AGE)
get_expiry_date()

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

This function accepts the same keyword arguments as get_expiry_age(), and similar notes on usage apply.

get_expire_at_browser_close()

Returns either True or False, depending on whether the user's session cookie will expire when the user's web browser is closed.

clear_expired()

从会话存储中移除过期会话。这个类方法通过 clearsessions 调用。

cycle_key()

在保留当前会话的同时创建新的会话秘钥。django.contrib.auth.login() 调用这个方法来防止会话固定攻击。

会话序列化

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

For example, here's an attack scenario if you use pickle to serialize session data. If you're using the signed cookie session backend and SECRET_KEY (or any key of SECRET_KEY_FALLBACKS) is known by an attacker (there isn't an inherent vulnerability in Django that would cause it to leak), the attacker could insert a string into their session which, when unpickled, executes arbitrary code on the server. The technique for doing so is simple and easily available on the internet. Although the cookie session storage signs the cookie-stored data to prevent tampering, a SECRET_KEY leak immediately escalates to a remote code execution vulnerability.

绑定序列化

class serializers.JSONSerializer

来自 django.core.signing 的JSON序列化器的装饰器。可以只序列化基本数据类型。

In addition, as JSON supports only string keys, note that using non-string keys in request.session won't work as expected:

>>> # 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

Supports arbitrary Python objects, but, as described above, can lead to a remote code execution vulnerability if SECRET_KEY or any key of SECRET_KEY_FALLBACKS becomes known by an attacker.

4.1 版后已移除: Due to the risk of remote code execution, this serializer is deprecated and will be removed in Django 5.0.

编写自定义的序列化器

Note that the JSONSerializer cannot handle arbitrary Python data types. As is often the case, there is a trade-off between convenience and security. If you wish to store more advanced data types including datetime and Decimal in JSON backed sessions, you will need to write a custom serializer (or convert such values to a JSON serializable object before storing them in request.session). While serializing these values is often straightforward (DjangoJSONEncoder may be helpful), writing a decoder that can reliably get back the same thing that you put in is more fragile. For example, you run the risk of returning a datetime that was actually a string that just happened to be in the same format chosen for datetimes).

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

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

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

An API is available to manipulate session data outside of a view:

>>> 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

If you're using the django.contrib.sessions.backends.db backend, each session is a normal Django model. The Session model is defined in django/contrib/sessions/models.py. Because it's a normal model, you can access sessions using the normal Django database 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)

Note that you'll need to call get_decoded() to get the session dictionary. This is necessary because the dictionary is stored in an encoded format:

>>> 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 里描述的一样。

备注

Some browsers (Chrome, for example) provide settings that allow users to continue browsing sessions after closing and reopening the browser. In some cases, this can interfere with the SESSION_EXPIRE_AT_BROWSER_CLOSE setting and prevent sessions from expiring on browser close. Please be aware of this while testing Django applications which have the SESSION_EXPIRE_AT_BROWSER_CLOSE setting enabled.

清除会话存储

当用户创建了新会话,会话数据会累积在会话存储中。如果你正在使用数据库后端,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

技术细节

  • The session dictionary accepts any json serializable value when using JSONSerializer.
  • 会话数据保存在名为 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的盗用。