如何使用会话

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 获取详情。

警告

如果你正在使用 Memcached 缓存后端,则应该只使用基于缓存的会话。本地内存缓存后端保留数据的时间不足以成为一个好的选择,直接使用文件或数据库会话而不是通过文件或数据库缓存后端发送所有内容会更快。另外,本地内存缓存后端并不安全,因此对生产环境来说或许不是一个好的选择。

如果你在 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"

你可能想设置 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)

比如: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() 函数调用它)。

设置一个测试cookie来确定用户的浏览器是否支持cookie。由于测试通过,你不需要在下一个页面请求时再次测试它。查看 Setting test cookies 获取更多信息。

返回 TrueFalse ,这取决于用户浏览器是否接受测试cookie。由于 cookie 的工作方式,你将必须在上一个独立的页面请求里调用 set_test_cookie() 。查看 Setting test cookies 获取更多信息。

删除测试cookie。使用完测试cookie后用它来删除。

set_expiry(value)

为会话设置过期时间。你可以传递很多不同值:

  • 如果 value 是整型,会话将在闲置数秒后过期。比如,调用 request.session.set_expiry(300) 会使得会话在5分钟后过期。
  • 如果 value 是一个 datetimetimedelta 对象,会话将在指定的 date/time 过期。注意,如果你正在使用 PickleSerializer ,那么 datetimetimedelta 的值只能序列化。
  • 如果 value0 ,则当浏览器关闭后,用户会话 cookie 将过期。
  • 如果 valueNone ,会话会恢复为全局会话过期策略。

出于过期目的,读取会话不被视为活动。会话过期时间会在会话最后一次*修改*后开始计算。

get_expiry_age()

返回该会话过期的秒数。对于没有自定义过期时间的会话(或者那些设置为浏览器关闭时过期的),这等同于 SESSION_COOKIE_AGE

这个函数接受两个可选的关键参数:

  • modification :会话的最后一次修改,当做一个 datetime 对象。默认是当前时间。
  • expiry :会话的过期信息,如一个 datetime 对象,整数(秒)或 None。默认为通过 set_expiry() 存储在会话中的值,或 None
get_expiry_date()

返回该会话的到期日期。对于没有自定义过期的会话(或那些设置为在浏览器关闭时过期的会话),这将等于从现在开始的SESSION_COOKIE_AGE秒的日期。

这个函数接受与 get_expiry_age() 相同的参数。

get_expire_at_browser_close()

返回 TrueFalse ,取决于用户会话 cookie 是否在浏览器关闭的时候过期。

clear_expired()

从会话存储中移除过期会话。这个类方法通过 clearsessions 调用。

cycle_key()

在保留当前会话的同时创建新的会话秘钥。django.contrib.auth.login() 调用这个方法来防止会话固定攻击。

会话序列化

默认情况下,Django 序列会话数据使用 JSON 。你可以设置 SESSION_SERIALIZER 来自定义会话序列化格式。即使在编写你自己的序列化程序中描述了警告,我们仍然强烈建议您坚持JSON序列化,尤其是在您使用cookie后端的情况下。

比如,如果你使用 pickle 来序列化会话数据,那么这里一个攻击场景。如果你正在使用 signed cookie session backend 并且攻击者已经知道了 SECRET_KEY (Django 并不存在会导致其泄露的固有漏洞),攻击者可以在会话里插入一个字符串,当 unpickled 时,在服务器上执行任意代码。这样做的技术很简单,在互联网上也很容易获得。尽管cookie会话存储会对cookie数据进行签名防止篡改,但是泄露 SECRET_KEY 会立即升级为远程代码执行的漏洞。

Bundled serializers

class serializers.JSONSerializer

A wrapper around the JSON serializer from django.core.signing. Can only serialize basic data types.

In addition, as JSON supports only string keys, note that using non-string keys in request.session won’t work as expected:

>>> # initial assignment
>>> request.session[0] = 'bar'
>>> # subsequent requests following serialization & deserialization
>>> # of session data
>>> request.session[0]  # KeyError
>>> request.session['0']
'bar'

Similarly, data that can’t be encoded in JSON, such as non-UTF8 bytes like '\xd9' (which raises UnicodeDecodeError), can’t be stored.

See the Write your own serializer section for more details on limitations of JSON serialization.

class serializers.PickleSerializer

Supports arbitrary Python objects, but, as described above, can lead to a remote code execution vulnerability if SECRET_KEY becomes known by an attacker.

Write your own serializer

Note that unlike PickleSerializer, 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 fairly 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 datetimes).

Your serializer class must implement two methods, dumps(self, obj) and loads(self, data), to serialize and deserialize the dictionary of session data, respectively.

Session object guidelines

  • Use normal Python strings as dictionary keys on request.session. This is more of a convention than a hard-and-fast rule.
  • Session dictionary keys that begin with an underscore are reserved for internal use by Django.
  • Don’t override request.session with a new object, and don’t access or set its attributes. Use it like a Python dictionary.

示例

This simplistic view sets a has_commented variable to True after a user posts a comment. It doesn’t let a user post a comment more than once:

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!')

This simplistic view logs in a “member” of the site:

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.")

…And this one logs a member out, according to login() above:

def logout(request):
    try:
        del request.session['member_id']
    except KeyError:
        pass
    return HttpResponse("You're logged out.")

The standard django.contrib.auth.logout() function actually does a bit more than this to prevent inadvertent data leakage. It calls the flush() method of request.session. We are using this example as a demonstration of how to work with session objects, not as a full logout() implementation.

Setting test cookies

As a convenience, Django provides an easy way to test whether the user’s browser accepts cookies. Just call the set_test_cookie() method of request.session in a view, and call test_cookie_worked() in a subsequent view – not in the same view call.

This awkward split between set_test_cookie() and test_cookie_worked() is necessary due to the way cookies work. When you set a cookie, you can’t actually tell whether a browser accepted it until the browser’s next request.

It’s good practice to use delete_test_cookie() to clean up after yourself. Do this after you’ve verified that the test cookie worked.

Here’s a typical usage example:

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')

Using sessions out of views

注解

The examples in this section import the SessionStore object directly from the django.contrib.sessions.backends.db backend. In your own code, you should consider importing SessionStore from the session engine designated by SESSION_ENGINE, as below:

>>> from importlib import import_module
>>> from django.conf import settings
>>> SessionStore = import_module(settings.SESSION_ENGINE).SessionStore

An API is available to manipulate session data outside of a view:

>>> 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() is designed to create a new session (i.e. one not loaded from the session store and with session_key=None). save() is designed to save an existing session (i.e. one loaded from the session store). Calling save() on a new session may also work but has a small chance of generating a session_key that collides with an existing one. create() calls save() and loops until an unused session_key is generated.

If you’re using the django.contrib.sessions.backends.db backend, each session is just a normal Django model. The Session model is defined in django/contrib/sessions/models.py. Because it’s a normal model, you can access sessions using the normal Django database 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)

Note that you’ll need to call get_decoded() to get the session dictionary. This is necessary because the dictionary is stored in an encoded format:

>>> s.session_data
'KGRwMQpTJ19hdXRoX3VzZXJfaWQnCnAyCkkxCnMuMTExY2ZjODI2Yj...'
>>> s.get_decoded()
{'user_id': 42}

When sessions are saved

By default, Django only saves to the session database when the session has been modified – that is if any of its dictionary values have been assigned or deleted:

# 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'

In the last case of the above example, we can tell the session object explicitly that it has been modified by setting the modified attribute on the session object:

request.session.modified = True

To change this default behavior, set the SESSION_SAVE_EVERY_REQUEST setting to True. When set to True, Django will save the session to the database on every single request.

Note that the session cookie is only sent when a session has been created or modified. If SESSION_SAVE_EVERY_REQUEST is True, the session cookie will be sent on every request.

Similarly, the expires part of a session cookie is updated each time the session cookie is sent.

The session is not saved if the response’s status code is 500.

Browser-length sessions vs. persistent sessions

You can control whether the session framework uses browser-length sessions vs. persistent sessions with the SESSION_EXPIRE_AT_BROWSER_CLOSE setting.

By default, SESSION_EXPIRE_AT_BROWSER_CLOSE is set to False, which means session cookies will be stored in users’ browsers for as long as SESSION_COOKIE_AGE. Use this if you don’t want people to have to log in every time they open a browser.

If SESSION_EXPIRE_AT_BROWSER_CLOSE is set to True, Django will use browser-length cookies – cookies that expire as soon as the user closes their browser. Use this if you want people to have to log in every time they open a browser.

This setting is a global default and can be overwritten at a per-session level by explicitly calling the set_expiry() method of request.session as described above in using sessions in views.

注解

Some browsers (Chrome, for example) provide settings that allow users to continue browsing sessions after closing and re-opening 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.

Clearing the session store

As users create new sessions on your website, session data can accumulate in your session store. If you’re using the database backend, the django_session database table will grow. If you’re using the file backend, your temporary directory will contain an increasing number of files.

To understand this problem, consider what happens with the database backend. When a user logs in, Django adds a row to the django_session database table. Django updates this row each time the session data changes. If the user logs out manually, Django deletes the row. But if the user does not log out, the row never gets deleted. A similar process happens with the file backend.

Django does not provide automatic purging of expired sessions. Therefore, it’s your job to purge expired sessions on a regular basis. Django provides a clean-up management command for this purpose: clearsessions. It’s recommended to call this command on a regular basis, for example as a daily cron job.

Note that the cache backend isn’t vulnerable to this problem, because caches automatically delete stale data. Neither is the cookie backend, because the session data is stored by the users’ browsers.

Settings

A few Django settings give you control over session behavior:

Session security

Subdomains within a site are able to set cookies on the client for the whole domain. This makes session fixation possible if cookies are permitted from subdomains not controlled by trusted users.

For example, an attacker could log into good.example.com and get a valid session for their account. If the attacker has control over bad.example.com, they can use it to send their session key to you since a subdomain is permitted to set cookies on *.example.com. When you visit good.example.com, you’ll be logged in as the attacker and might inadvertently enter your sensitive personal data (e.g. credit card info) into the attacker’s account.

Another possible attack would be if good.example.com sets its SESSION_COOKIE_DOMAIN to "example.com" which would cause session cookies from that site to be sent to bad.example.com.

Technical details

  • The session dictionary accepts any json serializable value when using JSONSerializer or any picklable Python object when using PickleSerializer. See the pickle module for more information.
  • Session data is stored in a database table named django_session .
  • Django only sends a cookie if it needs to. If you don’t set any session data, it won’t send a session cookie.

The SessionStore object

When working with sessions internally, Django uses a session store object from the corresponding session engine. By convention, the session store object class is named SessionStore and is located in the module designated by SESSION_ENGINE.

All SessionStore classes available in Django inherit from SessionBase and implement data manipulation methods, namely:

In order to build a custom session engine or to customize an existing one, you may create a new class inheriting from SessionBase or any other existing SessionStore class.

Extending most of the session engines is quite straightforward, but doing so with database-backed session engines generally requires some extra effort (see the next section for details).

Extending database-backed session engines

Creating a custom database-backed session engine built upon those included in Django (namely db and cached_db) may be done by inheriting AbstractBaseSession and either SessionStore class.

AbstractBaseSession and BaseSessionManager are importable from django.contrib.sessions.base_session so that they can be imported without including django.contrib.sessions in 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)

返回会话模型对象的新实例,该实例表示当前会话状态。

重写此方法提供了在将会话模型数据保存到数据库之前修改它的能力。

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的盗用。