文章目录
Django提供了对匿名会话的全面支持。
会话框架使您可以基于每个站点访问者存储和检索任意数据。
它在服务器端存储数据,并抽象化cookie的发送和接收。
Cookie包含会话ID,而不是数据本身(除非您使用的是基于Cookie的后端)。
1. 启用会话
会话是通过一个中间件实现的。
要启用会话功能,请执行以下操作:
- 编辑MIDDLEWARE设置,并确保它包含“django.contrib.sessions.middleware.SessionMiddleware”。
django-admin startproject创建的默认settings.py已激活SessionMiddleware。
如果您不想使用会话,则最好从INSTALLED_APPS的MIDDLEWARE和’django.contrib.sessions’中删除SessionMiddleware行。 这样可以节省您一点点的开销。
2. 配置会话引擎
默认情况下,Django将会话存储在数据库中(使用模型django.contrib.sessions.models.Session)。
尽管这很方便,但是在某些设置中,将会话数据存储在其他位置更快,因此可以将Django配置为在文件系统或缓存中存储会话数据。
2.1. 使用数据支持的会话
如果要使用数据库支持的会话,则需要将’django.contrib.sessions’添加到INSTALLED_APPS设置中。
配置完安装后,请运行manage.py migrate以安装用于存储会话数据的单个数据库表。
2.2. 使用缓存的会话
为了获得更好的性能,您可能需要使用基于缓存的会话后端。
要使用Django的缓存系统存储会话数据,首先需要确保已配置缓存。
有关详细信息,请参见缓存文档。
警告:
如果您使用的是Memcached缓存后端,则应仅使用基于缓存的会话。
本地内存缓存后端不能保留足够长的数据,因此不是一个很好的选择,并且直接使用文件或数据库会话比通过文件或数据库缓存后端发送所有内容会更快。
此外,本地内存缓存后端不是多进程安全的,因此对于生产环境而言可能不是一个好的选择。
如果您在CACHES中定义了多个缓存,则Django将使用默认缓存。
要使用另一个缓存,请将SESSION_CACHE_ALIAS设置为该缓存的名称。
一旦配置了缓存,就可以在缓存中存储数据的方式有两种:
- 将SESSION_ENGINE设置为“django.contrib.sessions.backends.cache”以获取简单的缓存会话存储。
会话数据将直接存储在您的缓存中。
但是,会话数据可能不是永久性的:如果高速缓存已满或重新启动了高速缓存服务器,则可以逐出高速缓存的数据。 - 对于持久性高速缓存的数据,请将SESSION_ENGINE设置为“django.contrib.sessions.backends.cached_db”。
这使用了直写式高速缓存-对高速缓存的每次写入也将被写入数据库。
如果数据不在缓存中,则会话仅使用数据库进行读取。
两个会话存储都相当快,但是简单的缓存却更快,因为它忽略了持久性。
在大多数情况下,cached_db后端将足够快,但是如果您需要性能的最后一点,并且愿意不时删除会话数据,则缓存后端适合您。
如果使用cached_db会话后端,则还需要遵循配置说明以使用数据库支持的会话。
2.3. 使用基于文件的会话
要使用基于文件的会话,请将SESSION_ENGINE设置设置为“django.contrib.sessions.backends.file”。
您可能还需要设置SESSION_FILE_PATH设置(默认设置为tempfile.gettempdir()的输出,很可能是/tmp)来控制Django存储会话文件的位置。
确保检查您的Web服务器是否具有读取和写入此位置的权限。
2.4. 使用基于cookie的会话
要使用基于cookie的会话,请将SESSION_ENGINE设置设置为“django.contrib.sessions.backends.signed_cookies”。
会话数据将使用Django的用于加密签名和SECRET_KEY设置的工具进行存储。
注解
建议将SESSION_COOKIE_HTTPONLY设置保留为True,以防止从JavaScript访问存储的数据。
警告
如果不对SECRET_KEY保密,而您正在使用PickleSerializer,则可能导致任意远程代码执行。
拥有SECRET_KEY的攻击者不仅可以生成您的站点将信任的伪造会话数据,而且还可以远程执行任意代码,因为这些数据是使用pickle序列化的。
如果您使用基于cookie的会话,请格外小心,因为对于任何可以远程访问的系统,您的密钥始终保持完全秘密。
会话数据已签名但未加密
使用后端cookie时,客户端可以读取会话数据。
MAC(消息身份验证代码)用于保护数据免遭客户端更改,从而使会话数据在被篡改时将失效。如果存储cookie的客户端(例如您的用户的浏览器)无法存储所有会话cookie并丢弃数据,则会发生相同的失效。即使Django压缩数据,也仍然有可能完全超出每个cookie 4096字节的通用限制。
没有实时性保证
还请注意,尽管MAC可以保证数据的真实性(它是由您的站点生成的,而不是其他人生成的),并且可以保证数据的完整性(数据全部存在并且正确),但它不能保证数据的实时性,即您将被发送回发送给客户的最后一件事。这意味着对于会话数据的某些用途,Cookie后端可能会使您重放攻击。与其他会话后端不同,该后端保留每个会话的服务器端记录并在用户注销时使其无效,而在用户注销时,基于cookie的会话不会无效。因此,如果攻击者窃取了用户的cookie,则即使用户注销了,他们也可以使用该cookie登录该用户。如果Cookie的年龄早于您的SESSION_COOKIE_AGE,它们将被检测为“陈旧”。
性能
最后,cookie的大小可能会影响您网站的速度。
3. 在views中使用会话
激活SessionMiddleware时,每个HttpRequest对象(任何Django视图函数的第一个参数)都将具有一个session属性,该属性是一个类似于字典的对象。
您可以在视图中的任何位置阅读并写入request.session。 您可以对其进行多次编辑。
backends.base.SessionBase类
这是所有会话对象的基类。它具有以下标准词典方法:
-
__getitem__(key)
范例:fav_color = request.session[‘fav_color’] -
__setitem__(key)
范例:request.session[‘fav_color’] = ‘blue’ -
__delitem__(key)
范例:del request.session[‘fav_color’]
如果给定的密钥不在会话中,则会引发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。
由于Cookie的工作方式,您将无法在用户的下一页请求之前对其进行测试。
有关更多信息,请参见下面的设置测试Cookie。 -
test_cookie_worked()
根据用户的浏览器是否接受测试cookie,返回True或False。
由于Cookie的工作方式,您必须在上一个单独的页面请求上调用set_test_cookie()。
有关更多信息,请参见下面的设置测试Cookie。 -
delete_test_cookie()
删除测试Cookie。 用它来清理自己。 -
get_session_cookie_age()
Django 3.0的新功能。
返回会话Cookie的生存时间(以秒为单位)。
默认为SESSION_COOKIE_AGE。 -
set_expiry(value)
设置会话的到期时间。您可以传递许多不同的值:-
如果value是整数,那么会话将在闲置许多秒后过期。
例如,调用request.session.set_expiry(300)将使会话在5分钟后过期。 -
如果value是datetime或timedelta对象,则会话将在该特定日期/时间到期。
请注意,只有在使用PickleSerializer时,datetime和timedelta值才能序列化。 -
如果value为0,则当用户的Web浏览器关闭时,用户的会话cookie将过期。
-
如果值是None,则会话将恢复为使用全局会话到期策略。
出于期满目的,阅读会话不视为活动。会话到期时间是从上次修改会话开始计算的。
-
-
get_expiry_age()
返回此会话到期之前的秒数。对于没有自定义到期时间的会话(或设置为在浏览器关闭时到期的会话),这将等于SESSION_COOKIE_AGE。此函数接受两个可选的关键字参数:
-
modification: 会话的最后修改,作为datetime对象。默认为当前时间。
-
expiry: 会话的到期信息,以日期时间对象,整数(以秒为单位)或无。如果为1,则默认为set_expiry()在会话中存储的值,否则为None。
-
-
get_expiry_date()
返回此会话将过期的日期。对于没有自定义到期时间的会话(或设置为在浏览器关闭时到期的会话),此日期等于从现在起SESSION_COOKIE_AGE秒。此函数接受与get_expiry_age()相同的关键字参数。
-
get_expire_at_browser_close()
返回True或False,这取决于关闭用户的Web浏览器时用户的会话cookie是否到期。 -
clear_expired()
从会话存储中删除过期的会话。此类方法由clearsessions调用。 -
cycle_key()
在保留当前会话数据的同时创建一个新的会话密钥。
django.contrib.auth.login()调用此方法来减轻会话固定。
3.1. 会话序列化
默认情况下,Django使用JSON序列化会话数据。
您可以使用SESSION_SERIALIZER设置来自定义会话序列化格式。
即使有编写您自己的序列化程序中所述的注意事项,我们还是强烈建议您坚持使用JSON序列化,尤其是在使用Cookie后端的情况下。
例如,如果您使用pickle序列化会话数据,这是一种攻击情形。
如果您使用签名的Cookie会话后端,并且攻击者知道SECRET_KEY(Django中没有一个固有漏洞会导致其泄漏),那么攻击者可以在其会话中插入一个字符串,该字符串在未腌制时将执行服务器上的任意代码。
这样做的技术很简单,并且可以在Internet上轻松获得。
尽管cookie会话存储对cookie存储的数据进行签名以防止篡改,但是SECRET_KEY泄漏立即升级为远程代码执行漏洞。
3.1.1. 捆绑序列化器
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序列化限制的更多详细信息,请参见“编写自己的序列化器”部分。
serializers.PickleSerializer类
- 支持任意Python对象,但是,如上所述,如果攻击者知道SECRET_KEY,则可能导致远程执行代码漏洞。
3.1.2. 编写自己的序列化器
请注意,与PickleSerializer不同,JSONSerializer无法处理任意Python数据类型。
通常,在便利性和安全性之间需要权衡取舍。
如果您希望在JSON支持的会话中存储包括datetime和Decimal在内的更多高级数据类型,则需要编写一个自定义序列化程序(或在将它们存储在request.session中之前将其转换为JSON可序列化对象)。
虽然序列化这些值通常很简单(DjangoJSONEncoder可能会有所帮助),但编写一个可以可靠地取回所放入内容的解码器则比较脆弱。
例如,您冒着返回一个日期时间的风险,该日期时间实际上是一个刚好与为日期时间选择的格式相同的字符串。
您的序列化程序类必须实现两种方法,分别对转储会话数据的字典进行序列化和反序列化dump(self,obj)和loads(self, data)。
3.2. 会话对象准则
-
在request.session上使用普通的Python字符串作为字典键。这不是一个固定的规则,而是更多的约定。
-
以下划线开头的会话字典键保留供Django内部使用。
-
不要用新对象覆盖request.session,也不要访问或设置其属性。像Python字典一样使用它。
3.3. 范例
用户发布评论后,此简化视图将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!')
这个简单的视图登录了站点的“member”:
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.session的flush()方法。
我们以该示例为例,说明如何使用会话对象,而不是完整的logout()实现。
4. 设置测试cookies
为了方便起见,Django提供了一种方法来测试用户的浏览器是否接受cookie。
在视图中调用request.session的set_test_cookie()方法,然后在后续视图中调用test_cookie_worked(),而不是在同一视图调用中。
由于cookie的工作方式,有必要在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')
5. 在views之外使用会话
注解
本节中的示例直接从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模型。
会话模型在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}
6. 当会话被保存时
默认情况下,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。
如果SESSION_SAVE_EVERY_REQUEST为True,则将在每个请求上发送会话cookie。
同样,每次发送会话cookie时,会话cookie的过期部分都会更新。
如果响应的状态码为500,则不会保存该会话。
7. Browser-length会话 对比 persistent会话
您可以通过SESSION_EXPIRE_AT_BROWSER_CLOSE设置来控制会话框架是使用浏览器长度会话还是持久性会话。
默认情况下,SESSION_EXPIRE_AT_BROWSER_CLOSE设置为False,这意味着会话cookie将在用户的浏览器中存储长达SESSION_COOKIE_AGE。
如果您不希望人们每次打开浏览器时都必须登录,请使用此选项。
如果将SESSION_EXPIRE_AT_BROWSER_CLOSE设置为True,则Django将使用浏览器长度的cookie,这些cookie会在用户关闭浏览器后立即过期。
如果您希望人们每次打开浏览器都必须登录,请使用此选项。
此设置是全局默认设置,可以通过按上面在视图中使用会话中所述显式调用request.session的set_expiry()方法在每个会话级别进行覆盖。
注解
某些浏览器(例如Chrome)提供的设置允许用户在关闭并重新打开浏览器后继续浏览会话。
在某些情况下,这可能会干扰SESSION_EXPIRE_AT_BROWSER_CLOSE设置,并阻止会话在浏览器关闭时过期。
在测试启用了SESSION_EXPIRE_AT_BROWSER_CLOSE设置的Django应用程序时,请注意这一点。
8. 清除存储的会话
当用户在您的网站上创建新会话时,会话数据会累积在您的会话存储区中。
如果您使用的是数据库后端,则django_session数据库表将会增长。
如果您使用的是文件后端,则您的临时目录将包含越来越多的文件。
要了解此问题,请考虑数据库后端会发生什么。
当用户登录时,Django在django_session数据库表中添加一行。
每次会话数据更改时,Django都会更新此行。
如果用户手动注销,则Django删除该行。
但是,如果用户未注销,则该行将永远不会被删除。
文件后端也会发生类似的过程。
Django不提供对过期会话的自动清除。
因此,定期清除过期的会话是您的工作。
Django为此提供了一个清理管理命令:clearsessions。
建议定期调用此命令,例如,作为日常cron作业。
请注意,缓存后端不容易受到此问题的影响,因为缓存会自动删除陈旧数据。
Cookie后端也不是,因为会话数据是由用户的浏览器存储的。
9. 设置
一些Django设置使您可以控制会话行为:
- 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
10. 会话安全
站点内的子域能够在客户端上为整个域设置cookie。
如果允许来自不受信任用户控制的子域的cookie,这将使会话固定成为可能。
例如,攻击者可以登录good.example.com并获得其帐户的有效会话。
如果攻击者控制了bad.example.com,则可以使用它向您发送会话密钥,因为允许子域在*.example.com上设置cookie。
当您访问good.example.com时,您将以攻击者身份登录,并可能无意中将您的敏感个人数据(例如信用卡信息)输入到攻击者的帐户中。
如果good.example.com将其SESSION_COOKIE_DOMAIN设置为“example.com”,则可能会导致来自该站点的会话Cookie发送到bad.example.com。
11. 技术细节
-
会话字典在使用JSONSerializer时接受任何json可序列化的值,在使用PickleSerializer时接受任何可拾取的Python对象。有关更多信息,请参见pickle模块。
-
会话数据存储在名为django_session的数据库表中。
-
Django仅在需要时发送cookie。如果您未设置任何会话数据,则不会发送会话Cookie。
11.1. SessionStore对象
在内部使用会话时,Django使用来自相应会话引擎的会话存储对象。
按照约定,会话存储对象类名为SessionStore,位于SESSION_ENGINE指定的模块中。
Django中所有可用的SessionStore类都继承自SessionBase并实现数据操作方法,即:
- exists()
- create()
- save()
- delete()
- load()
- clear_expired()
为了构建自定义会话引擎或自定义现有会话引擎,您可以创建一个继承自SessionBase或任何其他现有SessionStore类的新类。
您可以扩展会话引擎,但是使用数据库支持的会话引擎通常需要付出额外的努力(有关详细信息,请参阅下一节)。
12. 扩展数据库支持的会话引擎
通过继承AbstractBaseSession和SessionStore类,可以创建基于Django中包含的自定义数据库支持的会话引擎(即db和cached_db)。
可从django.contrib.sessions.base_session导入AbstractBaseSession和BaseSessionManager,以便可以在不将django.contrib.sessions包括在INSTALLED_APPS中的情况下将它们导入。
base_session.AbstractBaseSession类
抽象基础会话模型。
-
session_key
首要的关键。 该字段本身最多可以包含40个字符。当前实现生成一个32个字符的字符串(数字和小写ASCII字母的随机序列)。 -
session_data
包含编码和序列化会话字典的字符串。 -
expire_date
指定会话过期时间的日期时间。过期的会话对用户不可用,但是,它们可能仍存储在数据库中,直到运行clearsessions management命令为止。
-
classmethod get_session_store_class()
返回要用于此会话模型的会话存储类。 -
get_decoded()
返回解码的会话数据。解码是由会话存储类执行的。
您还可以通过继承BaseSessionManager来自定义模型管理器:
base_session.BaseSessionManager类
-
encode(session_dict)
返回序列化并编码为字符串的给定会话字典。编码由绑定到模型类的会话存储类执行。
-
save(session_key, session_dict, expire_date)
保存提供的会话密钥的会话数据,或在数据为空的情况下删除会话。
通过覆盖下面描述的方法和属性,可以实现SessionStore类的自定义:
backends.db.SessionStore类
实现数据库支持的会话存储。
-
classmethod get_model_class()
如果需要,重写此方法以返回自定义会话模型。 -
create_model_instance(data)
返回会话模型对象的新实例,该实例表示当前会话状态。覆盖此方法可以在将会话模型数据保存到数据库之前对其进行修改。
backends.cached_db.SessionStore类
实现缓存的数据库支持的会话存储。
- cache_key_prefix
在会话密钥中添加前缀以构建缓存密钥字符串。
12.1. 范例
下面的示例显示了一个由数据库支持的定制会话引擎,该引擎包括一个附加的数据库列以存储帐户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'
# ...
13. URL中的会话ID
Django会话框架完全且完全基于cookie。
它并没有像PHP那样将会话ID放在URL中作为最后的手段。
这是一个故意的设计决定。
这种行为不仅使URL变得丑陋,而且使您的站点容易通过“Referer”标头盗用会话ID。