如何使用会话¶
Django 完全支持匿名会话。会话框架允许您基于每个站点访问者存储和检索任意数据。它在服务器端存储数据,并对发送和接收 Cookie 进行抽象。Cookie 包含会话 ID,而不是数据本身(除非您使用的是基于 Cookie 的后端)。
启用会话¶
会话是通过一段中间件实现的。
要启用会话功能,请执行以下操作
编辑
MIDDLEWARE
设置,并确保它包含'django.contrib.sessions.middleware.SessionMiddleware'
。由django-admin startproject
创建的默认settings.py
已激活SessionMiddleware
。
如果您不想使用会话,您可以从MIDDLEWARE
中删除SessionMiddleware
行,并从您的INSTALLED_APPS
中删除'django.contrib.sessions'
。这将节省您少量开销。
配置会话引擎¶
默认情况下,Django 将会话存储在您的数据库中(使用模型django.contrib.sessions.models.Session
)。虽然这很方便,但在某些设置中,将会话数据存储在其他位置更快,因此可以配置 Django 将会话数据存储在您的文件系统或缓存中。
使用基于数据库的会话¶
如果您想使用基于数据库的会话,您需要将'django.contrib.sessions'
添加到您的INSTALLED_APPS
设置中。
配置好安装后,运行manage.py migrate
以安装存储会话数据的单个数据库表。
使用缓存会话¶
为了获得更好的性能,您可能希望使用基于缓存的会话后端。
要使用 Django 的缓存系统存储会话数据,您首先需要确保已配置您的缓存;有关详细信息,请参阅缓存文档。
警告
只有在使用 Memcached 或 Redis 缓存后端时,才应使用基于缓存的会话。本地内存缓存后端不会长时间保留数据,因此不是一个好的选择,直接使用文件或数据库会话比通过文件或数据库缓存后端发送所有内容更快。此外,本地内存缓存后端不是多进程安全的,因此可能不适合生产环境。
如果您在CACHES
中定义了多个缓存,Django 将使用默认缓存。要使用其他缓存,请将SESSION_CACHE_ALIAS
设置为该缓存的名称。
配置缓存后,您必须在基于数据库的缓存和非持久性缓存之间进行选择。
缓存数据库后端(cached_db
)使用直写缓存——会话写入将按顺序应用于数据库和缓存。如果写入缓存失败,则异常将通过会话记录器进行处理和记录,以避免使其他成功的写入操作失败。
添加了写入缓存时异常的处理和记录。
会话读取使用缓存,或者如果数据已从缓存中逐出则使用数据库。要使用此后端,请将SESSION_ENGINE
设置为"django.contrib.sessions.backends.cached_db"
,并按照使用基于数据库的会话的配置说明进行操作。
缓存后端(cache
)仅将会话数据存储在您的缓存中。这更快,因为它避免了数据库持久性,但是您必须考虑缓存数据被逐出时会发生什么。如果缓存已满或缓存服务器已重新启动,可能会发生逐出,这意味着会话数据将丢失,包括注销用户。要使用此后端,请将SESSION_ENGINE
设置为"django.contrib.sessions.backends.cache"
。
可以通过使用持久性缓存(例如具有适当配置的 Redis)来使缓存后端持久化。但是,除非您的缓存肯定配置了足够的持久性,否则请选择缓存数据库后端。这避免了生产环境中不可靠的数据存储造成的边缘情况。
使用基于文件的会话¶
要使用基于文件的会话,请将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)¶
- aget(key, default=None)¶
异步版本:
aget()
示例:
fav_color = request.session.get('fav_color', 'red')
Django 5.1 中的更改添加了
aget()
函数。
- aset(key, value)¶
- Django 5.1 新特性。
示例:
await request.session.aset('fav_color', 'red')
- update(dict)¶
- aupdate(dict)¶
异步版本:
aupdate()
示例:
request.session.update({'fav_color': 'red'})
Django 5.1 中的更改添加了
aupdate()
函数。
- pop(key, default=__not_given)¶
- apop(key, default=__not_given)¶
异步版本:
apop()
示例:
fav_color = request.session.pop('fav_color', 'blue')
Django 5.1 中的更改添加了
apop()
函数。
- keys()¶
- akeys()¶
异步版本:
akeys()
Django 5.1 中的更改添加了
akeys()
函数。
- values()¶
- avalues()¶
异步版本:
avalues()
Django 5.1 中的更改添加了
avalues()
函数。
- has_key(key)¶
- ahas_key(key)¶
异步版本:
ahas_key()
Django 5.1 中的更改添加了
ahas_key()
函数。
- items()¶
- aitems()¶
异步版本:
aitems()
Django 5.1 中的更改添加了
aitems()
函数。
- setdefault()¶
- asetdefault()¶
异步版本:
asetdefault()
Django 5.1 中的更改添加了
asetdefault()
函数。
- clear()¶
它还具有以下方法:
- flush()¶
- aflush()¶
异步版本:
aflush()
删除会话中的当前会话数据并删除会话cookie。如果您想确保用户浏览器无法再次访问以前的会话数据(例如,
django.contrib.auth.logout()
函数会调用它),则可以使用此方法。Django 5.1 中的更改添加了
aflush()
函数。
- set_test_cookie()¶
- aset_test_cookie()¶
异步版本:
aset_test_cookie()
设置测试cookie以确定用户浏览器是否支持cookie。由于cookie的工作方式,您必须等到用户的下一个页面请求才能测试它。有关更多信息,请参见下面的设置测试cookie。
Django 5.1 中的更改添加了
aset_test_cookie()
函数。
- test_cookie_worked()¶
- atest_cookie_worked()¶
异步版本:
atest_cookie_worked()
根据用户浏览器是否接受测试cookie,返回
True
或False
。由于cookie的工作方式,您必须在之前的单独页面请求中调用set_test_cookie()
或aset_test_cookie()
。有关更多信息,请参见下面的设置测试cookie。Django 5.1 中的更改添加了
atest_cookie_worked()
函数。
- delete_test_cookie()¶
- adelete_test_cookie()¶
异步版本:
adelete_test_cookie()
删除测试cookie。用它来清理。
Django 5.1 中的更改添加了
adelete_test_cookie()
函数。
- get_session_cookie_age()¶
返回设置
SESSION_COOKIE_AGE
的值。这可以在自定义会话后端中被覆盖。
- set_expiry(value)¶
- aset_expiry(value)¶
异步版本:
aset_expiry()
设置会话的过期时间。您可以传递多个不同的值。
如果
value
是整数,则会话将在指定秒数的空闲时间后过期。例如,调用request.session.set_expiry(300)
将使会话在5分钟后过期。如果
value
是datetime
或timedelta
对象,会话将在该特定日期/时间过期。如果
value
是0
,则用户会话cookie将在用户关闭网页浏览器时过期。如果
value
是None
,则会话将恢复为使用全局会话过期策略。
读取会话不视为过期目的的活动。会话过期时间是从上次修改会话的时间计算的。
Django 5.1 中的更改添加了
aset_expiry()
函数。
- get_expiry_age()¶
- aget_expiry_age()¶
异步版本:
aget_expiry_age()
返回此会话过期前剩余的秒数。对于没有自定义过期时间(或设置为在浏览器关闭时过期)的会话,这将等于
SESSION_COOKIE_AGE
。此函数接受两个可选关键字参数
modification
:会话的最后修改时间,作为datetime
对象。默认为当前时间。expiry
:会话的过期信息,作为datetime
对象,一个int
(以秒为单位),或None
。默认为会话中由set_expiry()
/aset_expiry()
存储的值(如果存在),或None
。
注意
此方法由会话后端用于在保存会话时确定会话过期时间(以秒为单位)。它并非真正用于此上下文之外。
特别是,虽然**可以**确定会话的剩余生命周期,**只有当**您拥有正确的
modification
值**并且**expiry
设置为datetime
对象时,您可以拥有modification
值,通过手工计算过期时间更为直接。expires_at = modification + timedelta(seconds=settings.SESSION_COOKIE_AGE)
Django 5.1 中的更改添加了
aget_expiry_age()
函数。
- get_expiry_date()¶
- aget_expiry_date()¶
异步版本:
aget_expiry_date()
返回此会话将过期的日期。对于没有自定义过期时间(或设置为在浏览器关闭时过期)的会话,这将等于
SESSION_COOKIE_AGE
秒后的日期。此函数接受与
get_expiry_age()
相同的关键字参数,并应用类似的使用说明。Django 5.1 中的更改添加了
aget_expiry_date()
函数。
- get_expire_at_browser_close()¶
- aget_expire_at_browser_close()¶
异步版本:
aget_expire_at_browser_close()
返回
True
或False
,取决于用户会话cookie是否会在用户关闭网页浏览器时过期。Django 5.1 中的更改添加了
aget_expire_at_browser_close()
函数。
- clear_expired()¶
- aclear_expired()¶
异步版本:
aclear_expired()
从会话存储中删除过期的会话。此类方法由
clearsessions
调用。Django 5.1 中的更改添加了
aclear_expired()
函数。
- cycle_key()¶
- acycle_key()¶
异步版本:
acycle_key()
创建一个新的会话密钥,同时保留当前会话数据。
django.contrib.auth.login()
调用此方法以减轻会话固定问题。Django 5.1 中的更改添加了
acycle_key()
函数。
会话序列化¶
默认情况下,Django 使用 JSON 序列化会话数据。您可以使用SESSION_SERIALIZER
设置来自定义会话序列化格式。即使存在编写您自己的序列化器中描述的警告,我们也强烈建议坚持使用 JSON 序列化,尤其是在您使用cookie后端时。
例如,如果您使用pickle
序列化会话数据,则这是一个攻击场景。如果您使用签名cookie会话后端和SECRET_KEY
(或SECRET_KEY_FALLBACKS
的任何密钥),并且攻击者知道该密钥(Django 中没有导致其泄漏的固有漏洞),则攻击者可以将其字符串插入到其会话中,该字符串在取消pickle后会在服务器上执行任意代码。执行此操作的技术很简单,并且在互联网上很容易获得。虽然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序列化的限制的更多详细信息,请参见编写您自己的序列化器部分。
编写您自己的序列化器¶
请注意,JSONSerializer
无法处理任意Python数据类型。通常情况下,便利性和安全性之间存在权衡。如果您希望存储更高级的数据类型,包括datetime
和Decimal
在JSON支持的会话中,您需要编写一个自定义序列化器(或者在将这些值存储在request.session
中之前将其转换为JSON可序列化的对象)。虽然序列化这些值通常很简单(DjangoJSONEncoder
可能会有所帮助),但编写一个能够可靠地取回与您放入的内容相同的解码器则更加脆弱。例如,您冒着返回一个datetime
对象的风险,而该对象实际上是一个字符串,碰巧与为datetime
对象选择的格式相同)。
您的序列化器类必须实现两个方法,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.")
…而这个视图根据上面的 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()
实现。
在视图外部使用会话¶
注意
本节中的示例直接从 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,则不会保存会话。
浏览器会话与持久化会话¶
您可以使用 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 应用程序时,请注意这一点。
清除会话存储¶
随着用户在您的网站上创建新的会话,会话数据可能会在您的会话存储中累积。如果您使用的是数据库后端,则 django_session
数据库表将增大。如果您使用的是文件后端,则您的临时目录将包含越来越多的文件。
要理解这个问题,请考虑使用数据库后端时会发生什么。当用户登录时,Django 会向 django_session
数据库表添加一行。每次会话数据更改时,Django 都会更新此行。如果用户手动注销,Django 将删除该行。但如果用户_不_注销,则该行永远不会被删除。文件后端也发生类似的过程。
Django_不_提供过期会话的自动清除。因此,定期清除过期会话是您的工作。Django 为此提供了一个清理管理命令:clearsessions
。建议定期调用此命令,例如作为每日 cron 作业。
请注意,缓存后端不会受到此问题的困扰,因为缓存会自动删除陈旧数据。Cookie 后端也不会,因为会话数据由用户的浏览器存储。
设置¶
一些 Django 设置 可以让您控制会话行为。
会话安全性¶
站点内的子域名能够为整个域名设置客户端的 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
。
技术细节¶
当使用
JSONSerializer
时,会话字典接受任何json
可序列化的值。会话数据存储在名为
django_session
的数据库表中。只有在需要时,Django 才会发送 Cookie。如果您没有设置任何会话数据,它不会发送会话 Cookie。
SessionStore
对象¶
在内部处理会话时,Django 使用来自相应会话引擎的会话存储对象。按照约定,会话存储对象类名为 SessionStore
,位于 SESSION_ENGINE
指定的模块中。
Django 中可用的所有 SessionStore
子类都实现了以下数据操作方法
exists()
create()
save()
delete()
load()
通过使用 sync_to_async()
包装这些方法,提供了一个异步接口。如果可以使用异步原生实现,则可以直接实现它们。
aexists()
acreate()
asave()
adelete()
aload()
为了构建自定义会话引擎或自定义现有引擎,您可以创建一个继承自 SessionBase
或任何其他现有 SessionStore
类的新的类。
您可以扩展会话引擎,但是使用数据库支持的会话引擎这样做通常需要一些额外的努力(有关详细信息,请参见下一节)。
添加了 aexists()
、acreate()
、asave()
、adelete()
、aload()
和 aclear_expired()
方法。
扩展数据库支持的会话引擎¶
通过继承 AbstractBaseSession
和 SessionStore
类,可以创建基于 Django 中包含的引擎(即 db
和 cached_db
)的自定义数据库支持的会话引擎。
可以从 django.contrib.sessions.base_session
导入 AbstractBaseSession
和 BaseSessionManager
,这样就可以在不包含 django.contrib.sessions
在 INSTALLED_APPS
中的情况下导入它们。
- 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)¶
返回会话模型对象的新的实例,该实例表示当前会话状态。
重写此方法可以修改会话模型数据,然后将其保存到数据库。
示例¶
下面的示例显示了一个自定义的数据库支持的会话引擎,它包含一个额外的数据库列来存储帐户 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 难看,还会使您的站点容易受到通过“Referer”标头进行的会话 ID 窃取的攻击。