django支持匿名会话。它将数据存放在服务器端,并抽象cookies的发送和接收过程。cookie包含一个会话ID而不是数据本身(除非你使用的是基于后端的cookie)。
3.3.8.1 启用会话
Django通过一个中间件来实现会话功能。要启用会话就要先启用该中间件。
编辑MIDDLEWARE设置,确保存在django.contrib.sessions.middleware.SessionMiddleware这一行。默认情况在新建的项目中它是存在的。
如果你不想使用会话功能,那么在settings文件中,将SessionMiddleware从MIDDLEWARE中删除,将django.contrib.sessions从 INSTALLED_APPS中删除就OK了。
3.3.8.2 配置会话引擎
默认情况下,django将会话数据保存在数据库内(通过使用django.contrib.sessions.models.Session模型)。当然,你也可以将数据保存在文件系统或缓存内。
1. 基于数据库的会话
先在INSTALLED_APPS设置中,确保django.contrib.sessions的存在,然后运行manage.py migrate命令在数据库内创建sessions表。
2. 基于缓存的会话
从性能角度考虑,也许你想使用基于缓存的会话。
首先,你得先配置好你的缓存,请参考3.11节查看详细。
警告:
本地缓存不是多进程安全的,因此对于生产环境不是一个好的选择。
如果你定义有多个缓存,django将使用默认的那个。如果你想用其它的,请将SESSION_CACHE_ALIAS参数设置为那个缓存的名字。
配置好缓存后,你可以选择两种保存数据的方法:
- 一是将SESSION_ENGINE设置为"django.contrib.sessions.backends.cache",简单的对会话进行保存。但是这种方法不是很可靠,因为当缓存数据存满时将清除部分数据,或者遇到缓存服务器重启。
- 为了数据安全保障,你可以将SESSION_ENGINE设置为"django.contrib.sessions.backends.cached_db"。这种方式在每次缓存的时候会同时将数据在数据库内写一份。当缓存不可用时,会话会从数据库内读取数据。
两种方法都很迅速,但是第一种简单的缓存更快一些,因为它忽略了数据的持久性。如果你使用缓存+数据库的方式,你同样需要按上面小节所述对数据库进行配置。
3. 基于文件的会话
将SESSION_ENGINE设置为"django.contrib.sessions.backends.file"。同时,你必须查看
SESSION_FILE_PATH配置(默认根据tempfile.gettempdir()生产,就像/tmp目录),确保你的文件存储目录,以及Web服务器对该目录具有读写权限。
4. 基于cookie的会话
将SESSION_ENGINE设置为"django.contrib.sessions.backends.signed_cookies"。Django将使用加密签名工具和安全秘钥设置保存会话的数据。
注意:
建议将SESSION_COOKIE_HTTPONLY设置为True,阻止javascript对会话数据的访问。
3.3.8.3 在视图中使用会话
当会话中间件启用后,传递给视图request参数的HttpRequest对象将包含一个session属性,就像一个字典对象一样。
你可以在视图的任何地方读写request.session属性,或者多次编辑使用它。
class backends.base.SessionBase
# 这是所有会话对象的基类,包含标准的字典方法:
__getitem__(key)
Example: fav_color = request.session[’fav_color’]
__setitem__(key, value)
Example: request.session[’fav_color’] = ’blue’
__delitem__(key)
Example: del request.session[’fav_color’]. 如果不存在会抛出异常
__contains__(key)
Example: ’fav_color’ in request.session
get(key, default=None)
Example: fav_color = request.session.get(’fav_color’, ’red’)
pop(key, default=__not_given)
Example: fav_color = request.session.pop(’fav_color’, ’blue’)
keys()
items()
setdefault()
clear()
# 它还有下面的方法:
flush()
# 删除当前的会话数据和会话cookie。经常用在用户退出后,删除会话。
set_test_cookie()
# 设置一个测试cookie,用于探测用户浏览器是否支持cookies。由于cookie的工作机制,你只有在下次用户请求的时候才可以测试。
test_cookie_worked()
# 返回True或者False,取决于用户的浏览器是否接受测试cookie。你必须在之前先调用set_test_cookie()方法。
delete_test_cookie()
# 删除测试cookie。
set_expiry(value)
# 设置cookie的有效期。可以传递不同类型的参数值:
• 如果值是一个整数,session将在对应的秒数后失效。例如request.session.set_expiry(300) 将在300秒后失效.
• 如果值是一个datetime或者timedelta对象, 会话将在指定的日期失效
• 如果为0,在用户关闭浏览器后失效
• 如果为None,则将使用全局会话失效策略
失效时间从上一次会话被修改的时刻开始计时。
get_expiry_age()
# 返回多少秒后失效的秒数。对于没有自定义失效时间的会话,这等同于SESSION_COOKIE_AGE.
# 这个方法接受2个可选的关键字参数
• modification:会话的最后修改时间(datetime对象)。默认是当前时间。
•expiry: 会话失效信息,可以是datetime对象,也可以是int或None
get_expiry_date()
# 和上面的方法类似,只是返回的是日期
get_expire_at_browser_close()
# 返回True或False,根据用户会话是否是浏览器关闭后就结束。
clear_expired()
# 删除已经失效的会话数据。
cycle_key()
# 创建一个新的会话秘钥用于保持当前的会话数据。django.contrib.auth.login() 会调用这个方法。
1. 会话序列化
Django默认使用JSON序列化会话数据。你可以在SESSION_SERIALIZER设置中自定义序列化格式,甚至写入警告说明。但是我们强烈建议你还是使用JSON,尤其是以cookie的方式进行会话时。
举个例子,这里有一个使用pickle序列化会话数据的攻击场景。如果你使用的是已签名的Cookie会话并且SECRET_KEY被攻击者知道了(通过其它手段),攻击者就可以在会话中插入一个字符串,在pickle反序列化时,可以在服务器上执行危险的代码。在因特网上这个攻击技术很简单并很容易使用。尽管Cookie会话会对数据进行签名以防止篡改,但是SECRET_KEY的泄漏却使得一切前功尽弃。
绑定的序列化方法
class serializers.JSONSerializer
对 django.core.signing中JSON序列化方法的一个包装。只可以序列化基本的数据类型。另外,JSON只支持以字符串作为键值,使用其它的类型会导致异常
>>> # 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异常。
class serializers.PickleSerializer
支持任意类型的python对象,但是就像前面说的,可能导致远端执行代码的漏洞,如果攻击者知道了SECRET_KEY。
编写你自己的序列化方法
你的序列化类必须分别实现dumps(self, obj)和loads(self, data)方法,用来实现序列化和反序列化会话数据字典。
2. 会话对象使用建议
- 使用普通的python字符串作为request.session字典的键值。这不是一条硬性规则而是为方便起见。
- 以一个下划线开始的会话字典的键被Django保留作为内部使用。
- 不要用新对象覆盖request.session,不要访问或设置它的属性。像一个python字典一样的使用它。
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!')
下面是一个简单的用户登录视图:
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.session的flush()方法。我们使用这个例子只是演示如何利用会话对象来工作,而不是一个完整的logout()实现。
3.3.8.4 设置测试cookie
为了方便,Django 提供一个简单的方法来测试用户的浏览器是否接受Cookie。只需在一个视图中调用request.session的set_test_cookie()方法,并在随后的视图中调用test_cookie_worked()获取测试结果(True或False)。注意,不能在同一个视图中调用这两个方法。
造成这种分割调用的原因是cookie的工作机制。当你设置一个cookie时,你无法立刻得到结果,知道浏览器发送下一个请求。
在测试后,记得使用delete_test_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')
3.8.3.5 在视图外使用session
注意:
在下面的例子中,我们直接从django.contrib.sessions.backends.db中导入了SessionStore对象。在你的实际代码中,你应该采用下面的导入方法,根据SESSION_ENGINE的设置进行导入,如下所示:
>>> 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()用于创建一个新的会话。save()方法用于保存一个已经存在的会话。create方法会调用save方法并循环直到生成一个未使用的session_key。直接调用save方法也可以创建一个新的会话,但在生成session_key的时候有可能和已经存在的发生冲突。
如果你使用的是django.contrib.sessions.backends.db模式,那么每一个会话其实就是一个普通的Django模型,你可以使用普通的Django数据库API访问它。会话模型的定义在django/contrib/sessions/models.py文件里。例如:
>>> 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}
3.3.8.6 何时保存会话
默认情况下,只有当会话字典的任何值被指定或删除的时候,Django才会将会话内容保存到会话数据库内。
# 会话被修改
request.session['foo'] = 'bar'
# 会话被修改
del request.session['foo']
# 会话被修改
request.session['foo'] = {}
# 会话没有被修改,只是修改了request.session['foo']
request.session['foo']['bar'] = 'baz'
要理解上面最后一种情况有点费劲。我们可以通过设置会话对象的modified属性值,显式地告诉会话对象它已经被修改过:request.session.modified = True
要改变上面的默认行为,将SESSION_SAVE_EVERY_REQUEST设置为True,那么每一次单独的请求过来,Django都会保存会话到数据库。
注意,会话的Cookie只有在一个会话被创建或修改后才会再次发送。如果SESSION_SAVE_EVERY_REQUEST为True,每个请求都会发送cookie。
类似地,会话Cookie的失效部分在每次发送会话Cookie时都会更新。
如果响应的状态码为500,则会话不会被保存。
3.3.8.7 浏览器生存期间会话 VS 持久会话
默认情况下,SESSION_EXPIRE_AT_BROWSER_CLOSE设置为False,也就是说cookie保存在用户的浏览器内,直到失效日期,这样用户就不必每次打开浏览器后都要再登录一次。
相反的SESSION_EXPIRE_AT_BROWSER_CLOSE设置为True,则意味着浏览器一关闭,cookie就失效,每次重新打开浏览器,你就得重新登录。
这个设置是一个全局的默认值,可以通过显式地调request.session的set_expiry()方法来覆盖,前面我们已经描述过了。
注意:有些浏览器(比如Chrome)具有在关闭后重新打开浏览器,会话依然保持的功能。这会与Django的SESSION_EXPIRE_AT_BROWSER_CLOSE设置发生冲突。请一定要小心。
3.3.8.8 清除已保存的会话
随着用户的访问,会话数据会越来越庞大。如果你使用的是数据库保存模式,那么django_session表的内容会逐渐增长。如果你使用的是文件模式,那么你的临时目录内的文件数量会不断增加。
造成这个问题的原因是,如果用户手动退出登录,Django将自动删除会话数据,但是如果用户不退出登录,那么对应的会话数据不会被删除。
Django没有提供自动清除失效会话的机制。因此,你必须自己完成这项工作。Django提供了一个命令clearsessions用于清除会话数据,建议你基于这个命令设置一个周期性的自动清除机制。
不同的是,使用缓存模式的会话不需要你清理数据,因为缓存系统自己有清理过期数据的机制。使用cookie模式的会话也不需要,因为数据都存在用户的浏览器内,不用你帮忙。
3.3.8.9 设置
这里有一些Django的设置,用于帮助你控制会话的行为:
- SESSION_CACHE_ALIAS
- SESSION_COOKIE_AGE
- SESSION_COOKIE_DOMAIN
- SESSION_COOKIE_HTTPONLY
- SESSION_COOKIE_NAME
- SESSION_COOKIE_PATH
- SESSION_COOKIE_SECURE
- SESSION_ENGINE
- SESSION_EXPIRE_AT_BROWSER_CLOSE
- SESSION_FILE_PATH
- SESSION_SAVE_EVERY_REQUEST
- SESSION_SERIALIZER
3.3.8.10 会话安全
一个站点下的子域名能够在为整个域名的客户设置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。
3.3.8.11 技术细节
- 会话字典接收任意的json序列化值,或者任何可通过pickle序列化的python对象
- 会话数据被保存在一张名为django_session的表内
- Django 只发送它需要的Cookie。如果你没有设置任何会话数据,它不会发送任何Cookie
SessionStore对象
在会话内部,Django使用一个与会话引擎对应的会话保存对象。根据管理,这个会话保存对象命名为SessionStore,位于SESSION_ENGINE设置指定的模块内。
所有Django支持的SessionStore类都继承SessionBase类,并实现了下面的数据操作方法:
- exists()
- create()
- save()
- delete()
- load()
- clear_expored()
为了创建一个自定义会话引擎或修改一个现成的引擎,你也许需要创建一个新的类,它继承SessionBase类或任何其他已经存在的SessionStore类。
3.3.8.12 扩展基于数据库的会话引擎
Django 1.9版本以后才有的功能。
要创建一个自定义的基于数据库的会话引擎,需要继承AbstractBaseSession类或者SessionStore类。
AbstractBaseSession和BaseSessionManager可以从django.contrib.sessions.base_session内导入,因此不一定非要在INSTALLED_APPS中包含django.contrib.sessions。
class base_session.AbstractBaseSession # 抽象会话基类
session_key
主键。最多40个字符。目前是一个32为随机数字或字母组合字符串。
session_data
一个包含了编码过的的或序列化过的会话字典的字符串
expire_date
失效日期
get_session_store_class()
这是一个类方法。返回一个会话保存类。
get_decoded()
返回解码后的会话数据。通过会话保存类进行解码。
你也可以自定义模型管理器,通过编写一个BaseSessionManager的子类。
class base_session.BaseSessionManager
encode(session_dict)
通过会话保存类,将会话字典序列化或编码成一个字符串
save(session_key, session_dict, expire_date)
根据一个提供的session秘钥保存会话数据,或者删除一个空的会话
通过重写下面这些SessionStore类的方法和属性,可以进行自定制:
class backends.db.SessionStore
实现基于数据库的会话保存
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(SessionStore, self).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'
# ...
3.3.8.13 URLs中的会话IDs
Django的会话框架完全地、唯一地基于Cookie。它不像PHP一样,把会话的ID放在URL中。它不仅使得URL变得丑陋,还使得你的网站易于受到通过"Referer"头部进行窃取会话ID的攻击。