如何使用会话
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 设置为该缓存名。
一旦配置好了缓存,你有两种办法在缓存中存储数据:
- 设置 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"
。
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.
使用基于cookie的会话
要使用基于cookies的会话,需要设置 SESSION_ENGINE 为 "django.contrib.sessions.backends.signed_cookies"
。这个会话数据将使用 Django 的加密工具( cryptographic signing ) 和 SECRET_KEY 工具进行保存。
备注
建议将 SESSION_COOKIE_HTTPONLY 设置为 True
来防止通过 JavaScript 访问存储数据。
警告
If the ``SECRET_KEY`` or ``SECRET_KEY_FALLBACKS`` are not kept secret and you are using the django.contrib.sessions.serializers.PickleSerializer
, this can lead to arbitrary remote code execution.
An attacker in possession of the SECRET_KEY or SECRET_KEY_FALLBACKS can not only generate falsified session data, which your site will trust, but also remotely execute arbitrary code, as the data is serialized using pickle.
如果你使用基于 cookie 的会话,一定要注意对于任何可能远程访问的系统,密钥是完全保密的。
会话数据已签名但未被加密
当使用cookie后端时,会话数据可以被客户端读取。
MAC(消息验证代码) 被用来保护数据不被客户端修改,因此会话数据在被篡改时失效。如果存储cookie 的客户端 (比如浏览器) 不能存储所有会话数据并丢弃数据,则会同样发生失效。即使 Django 压缩数据,它仍然完全有可能每个 cookie 超过4096字节的通用限制( common limit of 4096 bytes )。
不保证新鲜度
注意虽然 MAC 可以保证数据(通过站点生成,而不是其他人)真实性和数据完整(它是完整和正确的),但它不能保证新鲜度,也就是说,您最后发送给客户端的东西会被退回。这意味着cookie后端为了使用一些会话数据,可能会面临重播攻击。与其他会话后端(每个会话保持服务端记录,并且当用户退出时使会话失效)不同,基于cookie的会话在用户退出的时候并不会让会话失效。因此攻击者窃取用户cookie,即使用户登出了,攻击者还可以使用cookie登录该用户。如果 Cookie 比 SESSION_COOKIE_AGE 设置的时间还旧时,则cookie会被检测为 ‘陈旧’ 。
性能
最后,cookie 的大小可能会对您网站的速度造成影响。
在视图中使用会话
当激活 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() 函数调用它)。
set_test_cookie
()设置一个测试cookie来确定用户的浏览器是否支持cookie。由于测试通过,你不需要在下一个页面请求时再次测试它。查看 Setting test cookies 获取更多信息。
test_cookie_worked
()返回
True
或False
,这取决于用户浏览器是否接受测试cookie。由于 cookie 的工作方式,你将必须在上一个独立的页面请求里调用set_test_cookie()
。查看 Setting test cookies 获取更多信息。delete_test_cookie
()删除测试cookie。使用完测试cookie后用它来删除。
get_session_cookie_age
()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 adatetime
ortimedelta
object, the session will expire at that specific date/time. - If
value
is0
, the user’s session cookie will expire when the user’s web browser is closed. - 如果
value
是None
,会话会恢复为全局会话过期策略。
出于过期目的,读取会话不被视为活动。会话过期时间会在会话最后一次*修改*后开始计算。
- 如果
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 theexpiry
is set as adatetime
object, where you do have themodification
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
orFalse
, 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序列化器的装饰器。可以只序列化基本数据类型。
另外,因为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
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 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.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.session
的 flush() 方法。我们使用这个例子作为示范如何使用会话对象,而不是完整的 logout()
实现。
测试 cookies
设置
为了方便起见,Django 提供一种方法来测试用户浏览器是否支持cookies。调用视图里 request.session
的 set_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_REQUEST 为 True
。当设置为 True
时,Django 会根据每个请求将会话保存到数据库中。
注意,仅在会话被创建或修改时发送会话 cookie 。如果 SESSION_SAVE_EVERY_REQUEST 为 True
,则会话cookie将在每次请求时发送。
同样地,每次发送会话 cookie 时都会更新会话 cookie 的 expires
部分。
如果响应状态代码为 500,会话不会被保存。
Browser-length 会话 vs 持久会话
你可以通过设置 SESSION_EXPIRE_AT_BROWSER_CLOSE 来控制会话框架是使用 browser-length 会话还是持久会话。
默认情况下, SESSION_EXPIRE_AT_BROWSER_CLOSE 为 False
,这意味着会话 cookies 将保存在用户浏览器中持续 SESSION_COOKIE_AGE 的时间。如果你不想用户每次打开浏览器时必须登录,就用这个。
如果 SESSION_EXPIRE_AT_BROWSER_CLOSE 为 True
,Django 将使用 browser-length cookies —— cookies 在用户关闭浏览器时过期。如果你想让用户每次打开浏览器时必须登录,就用这个。
这个设置是全局默认的,并且可以通过显式调用 request.session
的 set_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 后端也一样,因为会话数据通过浏览器存储。
配置
一些可以用来控制会话行为的 Django settings :
- SESSION_CACHE_ALIAS
- SESSION_COOKIE_AGE
- SESSION_COOKIE_DOMAIN
- SESSION_COOKIE_HTTPONLY
- SESSION_COOKIE_NAME
- SESSION_COOKIE_PATH
- SESSION_COOKIE_SAMESITE
- SESSION_COOKIE_SECURE
- SESSION_ENGINE
- SESSION_EXPIRE_AT_BROWSER_CLOSE
- SESSION_FILE_PATH
- SESSION_SAVE_EVERY_REQUEST
- SESSION_SERIALIZER
会话安全
站点内的子域可以在客户端上为整个域设置 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 并且实现了数据操作方法,即:
exists()
create()
save()
delete()
load()
- clear_expired()
为了搭建自定义的会话引擎或自定义已有的引擎,你可以创建一个继承自 SessionBase 的新类或任何其他已存在的 SessionStore
类。
你可以扩展会话引擎,但对于使用数据库支持的会话引擎通常需要额外的功夫(查看下节来获取更多详情)。
扩展数据库支持的会话引擎
可以通过继承 AbstractBaseSession 和 SessionStore``类来创建基于Django中包含的自定义数据库支持的会话引擎(即 ``db
和 cached_db
)。
AbstractBaseSession
和 BaseSessionManager
可以从 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的盗用。