Django 1.10 中文文档------3.3.8 会话sessions

欢迎大家访问我的个人网站《刘江的博客和教程》:www.liujiangblog.com

主要分享Python 及Django教程以及相关的博客


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的攻击。

posted @ 2016-10-18 19:16 大江东流去 阅读(...) 评论(...) 编辑 收藏

作者:刘江

版权所有,出自www.cnblogs.com/feixuelove1009