Fixed #16199 -- Added a Cookie based session backend. Many thanks to Eric Florenzano for his initial work and Florian Apollaner for reviewing.
git-svn-id: http://code.djangoproject.com/svn/django/trunk@16466 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
parent
bc56c76a17
commit
c817f2f544
|
@ -0,0 +1,93 @@
|
|||
try:
|
||||
import cPickle as pickle
|
||||
except ImportError:
|
||||
import pickle
|
||||
|
||||
from django.conf import settings
|
||||
from django.core import signing
|
||||
|
||||
from django.contrib.sessions.backends.base import SessionBase
|
||||
|
||||
|
||||
class PickleSerializer(object):
|
||||
"""
|
||||
Simple wrapper around pickle to be used in signing.dumps and
|
||||
signing.loads.
|
||||
"""
|
||||
def dumps(self, obj):
|
||||
return pickle.dumps(obj, pickle.HIGHEST_PROTOCOL)
|
||||
|
||||
def loads(self, data):
|
||||
return pickle.loads(data)
|
||||
|
||||
|
||||
class SessionStore(SessionBase):
|
||||
|
||||
def load(self):
|
||||
"""
|
||||
We load the data from the key itself instead of fetching from
|
||||
some external data store. Opposite of _get_session_key(),
|
||||
raises BadSignature if signature fails.
|
||||
"""
|
||||
try:
|
||||
return signing.loads(self._session_key,
|
||||
serializer=PickleSerializer,
|
||||
max_age=settings.SESSION_COOKIE_AGE,
|
||||
salt='django.contrib.sessions.backends.cookies')
|
||||
except (signing.BadSignature, ValueError):
|
||||
self.create()
|
||||
return {}
|
||||
|
||||
def create(self):
|
||||
"""
|
||||
To create a new key, we simply make sure that the modified flag is set
|
||||
so that the cookie is set on the client for the current request.
|
||||
"""
|
||||
self.modified = True
|
||||
|
||||
def save(self, must_create=False):
|
||||
"""
|
||||
To save, we get the session key as a securely signed string and then
|
||||
set the modified flag so that the cookie is set on the client for the
|
||||
current request.
|
||||
"""
|
||||
self._session_key = self._get_session_key()
|
||||
self.modified = True
|
||||
|
||||
def exists(self, session_key=None):
|
||||
"""
|
||||
This method makes sense when you're talking to a shared resource, but
|
||||
it doesn't matter when you're storing the information in the client's
|
||||
cookie.
|
||||
"""
|
||||
return False
|
||||
|
||||
def delete(self, session_key=None):
|
||||
"""
|
||||
To delete, we clear the session key and the underlying data structure
|
||||
and set the modified flag so that the cookie is set on the client for
|
||||
the current request.
|
||||
"""
|
||||
self._session_key = ''
|
||||
self._session_cache = {}
|
||||
self.modified = True
|
||||
|
||||
def cycle_key(self):
|
||||
"""
|
||||
Keeps the same data but with a new key. To do this, we just have to
|
||||
call ``save()`` and it will automatically save a cookie with a new key
|
||||
at the end of the request.
|
||||
"""
|
||||
self.save()
|
||||
|
||||
def _get_session_key(self):
|
||||
"""
|
||||
Most session backends don't need to override this method, but we do,
|
||||
because instead of generating a random string, we want to actually
|
||||
generate a secure url-safe Base64-encoded string of data as our
|
||||
session key.
|
||||
"""
|
||||
session_cache = getattr(self, '_session_cache', {})
|
||||
return signing.dumps(session_cache, compress=True,
|
||||
salt='django.contrib.sessions.backends.cookies',
|
||||
serializer=PickleSerializer)
|
|
@ -7,11 +7,13 @@ from django.contrib.sessions.backends.db import SessionStore as DatabaseSession
|
|||
from django.contrib.sessions.backends.cache import SessionStore as CacheSession
|
||||
from django.contrib.sessions.backends.cached_db import SessionStore as CacheDBSession
|
||||
from django.contrib.sessions.backends.file import SessionStore as FileSession
|
||||
from django.contrib.sessions.backends.cookies import SessionStore as CookieSession
|
||||
from django.contrib.sessions.models import Session
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation
|
||||
from django.http import HttpResponse
|
||||
from django.test import TestCase, RequestFactory
|
||||
from django.test.utils import override_settings
|
||||
from django.utils import unittest
|
||||
|
||||
|
||||
|
@ -213,35 +215,25 @@ class SessionTestsMixin(object):
|
|||
def test_get_expire_at_browser_close(self):
|
||||
# Tests get_expire_at_browser_close with different settings and different
|
||||
# set_expiry calls
|
||||
try:
|
||||
try:
|
||||
original_expire_at_browser_close = settings.SESSION_EXPIRE_AT_BROWSER_CLOSE
|
||||
settings.SESSION_EXPIRE_AT_BROWSER_CLOSE = False
|
||||
with override_settings(SESSION_EXPIRE_AT_BROWSER_CLOSE=False):
|
||||
self.session.set_expiry(10)
|
||||
self.assertFalse(self.session.get_expire_at_browser_close())
|
||||
|
||||
self.session.set_expiry(10)
|
||||
self.assertFalse(self.session.get_expire_at_browser_close())
|
||||
self.session.set_expiry(0)
|
||||
self.assertTrue(self.session.get_expire_at_browser_close())
|
||||
|
||||
self.session.set_expiry(0)
|
||||
self.assertTrue(self.session.get_expire_at_browser_close())
|
||||
self.session.set_expiry(None)
|
||||
self.assertFalse(self.session.get_expire_at_browser_close())
|
||||
|
||||
self.session.set_expiry(None)
|
||||
self.assertFalse(self.session.get_expire_at_browser_close())
|
||||
with override_settings(SESSION_EXPIRE_AT_BROWSER_CLOSE=True):
|
||||
self.session.set_expiry(10)
|
||||
self.assertFalse(self.session.get_expire_at_browser_close())
|
||||
|
||||
settings.SESSION_EXPIRE_AT_BROWSER_CLOSE = True
|
||||
self.session.set_expiry(0)
|
||||
self.assertTrue(self.session.get_expire_at_browser_close())
|
||||
|
||||
self.session.set_expiry(10)
|
||||
self.assertFalse(self.session.get_expire_at_browser_close())
|
||||
|
||||
self.session.set_expiry(0)
|
||||
self.assertTrue(self.session.get_expire_at_browser_close())
|
||||
|
||||
self.session.set_expiry(None)
|
||||
self.assertTrue(self.session.get_expire_at_browser_close())
|
||||
|
||||
except:
|
||||
raise
|
||||
finally:
|
||||
settings.SESSION_EXPIRE_AT_BROWSER_CLOSE = original_expire_at_browser_close
|
||||
self.session.set_expiry(None)
|
||||
self.assertTrue(self.session.get_expire_at_browser_close())
|
||||
|
||||
def test_decode(self):
|
||||
# Ensure we can decode what we encode
|
||||
|
@ -302,9 +294,10 @@ class FileSessionTests(SessionTestsMixin, unittest.TestCase):
|
|||
shutil.rmtree(self.temp_session_store)
|
||||
super(FileSessionTests, self).tearDown()
|
||||
|
||||
@override_settings(
|
||||
SESSION_FILE_PATH="/if/this/directory/exists/you/have/a/weird/computer")
|
||||
def test_configuration_check(self):
|
||||
# Make sure the file backend checks for a good storage dir
|
||||
settings.SESSION_FILE_PATH = "/if/this/directory/exists/you/have/a/weird/computer"
|
||||
self.assertRaises(ImproperlyConfigured, self.backend)
|
||||
|
||||
def test_invalid_key_backslash(self):
|
||||
|
@ -324,17 +317,9 @@ class CacheSessionTests(SessionTestsMixin, unittest.TestCase):
|
|||
|
||||
|
||||
class SessionMiddlewareTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.old_SESSION_COOKIE_SECURE = settings.SESSION_COOKIE_SECURE
|
||||
self.old_SESSION_COOKIE_HTTPONLY = settings.SESSION_COOKIE_HTTPONLY
|
||||
|
||||
def tearDown(self):
|
||||
settings.SESSION_COOKIE_SECURE = self.old_SESSION_COOKIE_SECURE
|
||||
settings.SESSION_COOKIE_HTTPONLY = self.old_SESSION_COOKIE_HTTPONLY
|
||||
|
||||
@override_settings(SESSION_COOKIE_SECURE=True)
|
||||
def test_secure_session_cookie(self):
|
||||
settings.SESSION_COOKIE_SECURE = True
|
||||
|
||||
request = RequestFactory().get('/')
|
||||
response = HttpResponse('Session test')
|
||||
middleware = SessionMiddleware()
|
||||
|
@ -347,9 +332,8 @@ class SessionMiddlewareTests(unittest.TestCase):
|
|||
response = middleware.process_response(request, response)
|
||||
self.assertTrue(response.cookies[settings.SESSION_COOKIE_NAME]['secure'])
|
||||
|
||||
@override_settings(SESSION_COOKIE_HTTPONLY=True)
|
||||
def test_httponly_session_cookie(self):
|
||||
settings.SESSION_COOKIE_HTTPONLY = True
|
||||
|
||||
request = RequestFactory().get('/')
|
||||
response = HttpResponse('Session test')
|
||||
middleware = SessionMiddleware()
|
||||
|
@ -361,3 +345,24 @@ class SessionMiddlewareTests(unittest.TestCase):
|
|||
# Handle the response through the middleware
|
||||
response = middleware.process_response(request, response)
|
||||
self.assertTrue(response.cookies[settings.SESSION_COOKIE_NAME]['httponly'])
|
||||
|
||||
|
||||
class CookieSessionTests(SessionTestsMixin, TestCase):
|
||||
|
||||
backend = CookieSession
|
||||
|
||||
def test_save(self):
|
||||
"""
|
||||
This test tested exists() in the other session backends, but that
|
||||
doesn't make sense for us.
|
||||
"""
|
||||
pass
|
||||
|
||||
def test_cycle(self):
|
||||
"""
|
||||
This test tested cycle_key() which would create a new session
|
||||
key for the same session data. But we can't invalidate previously
|
||||
signed cookies (other than letting them expire naturally) so
|
||||
testing for this behaviour is meaningless.
|
||||
"""
|
||||
pass
|
||||
|
|
|
@ -3,33 +3,33 @@ Functions for creating and restoring url-safe signed JSON objects.
|
|||
|
||||
The format used looks like this:
|
||||
|
||||
>>> signed.dumps("hello")
|
||||
'ImhlbGxvIg.RjVSUCt6S64WBilMYxG89-l0OA8'
|
||||
>>> signing.dumps("hello")
|
||||
'ImhlbGxvIg:1QaUZC:YIye-ze3TTx7gtSv422nZA4sgmk'
|
||||
|
||||
There are two components here, separatad by a '.'. The first component is a
|
||||
There are two components here, separatad by a ':'. The first component is a
|
||||
URLsafe base64 encoded JSON of the object passed to dumps(). The second
|
||||
component is a base64 encoded hmac/SHA1 hash of "$first_component.$secret"
|
||||
component is a base64 encoded hmac/SHA1 hash of "$first_component:$secret"
|
||||
|
||||
signed.loads(s) checks the signature and returns the deserialised object.
|
||||
signing.loads(s) checks the signature and returns the deserialised object.
|
||||
If the signature fails, a BadSignature exception is raised.
|
||||
|
||||
>>> signed.loads("ImhlbGxvIg.RjVSUCt6S64WBilMYxG89-l0OA8")
|
||||
>>> signing.loads("ImhlbGxvIg:1QaUZC:YIye-ze3TTx7gtSv422nZA4sgmk")
|
||||
u'hello'
|
||||
>>> signed.loads("ImhlbGxvIg.RjVSUCt6S64WBilMYxG89-l0OA8-modified")
|
||||
>>> signing.loads("ImhlbGxvIg:1QaUZC:YIye-ze3TTx7gtSv422nZA4sgmk-modified")
|
||||
...
|
||||
BadSignature: Signature failed: RjVSUCt6S64WBilMYxG89-l0OA8-modified
|
||||
BadSignature: Signature failed: ImhlbGxvIg:1QaUZC:YIye-ze3TTx7gtSv422nZA4sgmk-modified
|
||||
|
||||
You can optionally compress the JSON prior to base64 encoding it to save
|
||||
space, using the compress=True argument. This checks if compression actually
|
||||
helps and only applies compression if the result is a shorter string:
|
||||
|
||||
>>> signed.dumps(range(1, 20), compress=True)
|
||||
'.eJwFwcERACAIwLCF-rCiILN47r-GyZVJsNgkxaFxoDgxcOHGxMKD_T7vhAml.oFq6lAAEbkHXBHfGnVX7Qx6NlZ8'
|
||||
>>> signing.dumps(range(1, 20), compress=True)
|
||||
'.eJwFwcERACAIwLCF-rCiILN47r-GyZVJsNgkxaFxoDgxcOHGxMKD_T7vhAml:1QaUaL:BA0thEZrp4FQVXIXuOvYJtLJSrQ'
|
||||
|
||||
The fact that the string is compressed is signalled by the prefixed '.' at the
|
||||
start of the base64 JSON.
|
||||
|
||||
There are 65 url-safe characters: the 64 used by url-safe base64 and the '.'.
|
||||
There are 65 url-safe characters: the 64 used by url-safe base64 and the ':'.
|
||||
These functions make use of all of them.
|
||||
"""
|
||||
import base64
|
||||
|
@ -87,7 +87,19 @@ def get_cookie_signer(salt='django.core.signing.get_cookie_signer'):
|
|||
return Signer('django.http.cookies' + settings.SECRET_KEY, salt=salt)
|
||||
|
||||
|
||||
def dumps(obj, key=None, salt='django.core.signing', compress=False):
|
||||
class JSONSerializer(object):
|
||||
"""
|
||||
Simple wrapper around simplejson to be used in signing.dumps and
|
||||
signing.loads.
|
||||
"""
|
||||
def dumps(self, obj):
|
||||
return simplejson.dumps(obj, separators=(',', ':'))
|
||||
|
||||
def loads(self, data):
|
||||
return simplejson.loads(data)
|
||||
|
||||
|
||||
def dumps(obj, key=None, salt='django.core.signing', serializer=JSONSerializer, compress=False):
|
||||
"""
|
||||
Returns URL-safe, sha1 signed base64 compressed JSON string. If key is
|
||||
None, settings.SECRET_KEY is used instead.
|
||||
|
@ -101,24 +113,24 @@ def dumps(obj, key=None, salt='django.core.signing', compress=False):
|
|||
value or re-using a salt value across different parts of your
|
||||
application without good cause is a security risk.
|
||||
"""
|
||||
json = simplejson.dumps(obj, separators=(',', ':'))
|
||||
data = serializer().dumps(obj)
|
||||
|
||||
# Flag for if it's been compressed or not
|
||||
is_compressed = False
|
||||
|
||||
if compress:
|
||||
# Avoid zlib dependency unless compress is being used
|
||||
compressed = zlib.compress(json)
|
||||
if len(compressed) < (len(json) - 1):
|
||||
json = compressed
|
||||
compressed = zlib.compress(data)
|
||||
if len(compressed) < (len(data) - 1):
|
||||
data = compressed
|
||||
is_compressed = True
|
||||
base64d = b64_encode(json)
|
||||
base64d = b64_encode(data)
|
||||
if is_compressed:
|
||||
base64d = '.' + base64d
|
||||
return TimestampSigner(key, salt=salt).sign(base64d)
|
||||
|
||||
|
||||
def loads(s, key=None, salt='django.core.signing', max_age=None):
|
||||
def loads(s, key=None, salt='django.core.signing', serializer=JSONSerializer, max_age=None):
|
||||
"""
|
||||
Reverse of dumps(), raises BadSignature if signature fails
|
||||
"""
|
||||
|
@ -129,10 +141,10 @@ def loads(s, key=None, salt='django.core.signing', max_age=None):
|
|||
# It's compressed; uncompress it first
|
||||
base64d = base64d[1:]
|
||||
decompress = True
|
||||
json = b64_decode(base64d)
|
||||
data = b64_decode(base64d)
|
||||
if decompress:
|
||||
json = zlib.decompress(json)
|
||||
return simplejson.loads(json)
|
||||
data = zlib.decompress(data)
|
||||
return serializer().loads(data)
|
||||
|
||||
|
||||
class Signer(object):
|
||||
|
@ -160,6 +172,7 @@ class Signer(object):
|
|||
|
||||
|
||||
class TimestampSigner(Signer):
|
||||
|
||||
def timestamp(self):
|
||||
return baseconv.base62.encode(int(time.time()))
|
||||
|
||||
|
|
|
@ -89,6 +89,16 @@ signing in Web applications.
|
|||
|
||||
See :doc:`cryptographic signing </topics/signing>` docs for more information.
|
||||
|
||||
Cookie-based session backend
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Django 1.4 introduces a new cookie based backend for the session framework
|
||||
which uses the tools for :doc:`cryptographic signing </topics/signing>` to
|
||||
store the session data in the client's browser.
|
||||
|
||||
See the :ref:`cookie-based backend <cookie-session-backend>` docs for
|
||||
more information.
|
||||
|
||||
New form wizard
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
|
|
|
@ -5,10 +5,11 @@ How to use sessions
|
|||
.. module:: django.contrib.sessions
|
||||
:synopsis: Provides session management for Django projects.
|
||||
|
||||
Django provides full support for anonymous sessions. The session framework lets
|
||||
you store and retrieve arbitrary data on a per-site-visitor basis. It stores
|
||||
data on the server side and abstracts the sending and receiving of cookies.
|
||||
Cookies contain a session ID -- not the data itself.
|
||||
Django provides full support for anonymous sessions. The session framework
|
||||
lets you store and retrieve arbitrary data on a per-site-visitor basis. It
|
||||
stores data on the server side and abstracts the sending and receiving of
|
||||
cookies. Cookies contain a session ID -- not the data itself (unless you're
|
||||
using the :ref:`cookie based backend<cookie-session-backend>`).
|
||||
|
||||
Enabling sessions
|
||||
=================
|
||||
|
@ -95,6 +96,38 @@ defaults to output from ``tempfile.gettempdir()``, most likely ``/tmp``) to
|
|||
control where Django stores session files. Be sure to check that your Web
|
||||
server has permissions to read and write to this location.
|
||||
|
||||
.. _cookie-session-backend:
|
||||
|
||||
Using cookie-based sessions
|
||||
---------------------------
|
||||
|
||||
.. versionadded:: 1.4
|
||||
|
||||
To use cookies-based sessions, set the :setting:`SESSION_ENGINE` setting to
|
||||
``"django.contrib.sessions.backends.cookies"``. The session data will be
|
||||
stored using Django's tools for :doc:`cryptographic signing </topics/signing>`
|
||||
and the :setting:`SECRET_KEY` setting.
|
||||
|
||||
.. note::
|
||||
|
||||
It's recommended to set the :setting:`SESSION_COOKIE_HTTPONLY` setting
|
||||
to ``True`` to prevent tampering of the stored data from JavaScript.
|
||||
|
||||
.. warning::
|
||||
|
||||
**The session data is signed but not encrypted!**
|
||||
|
||||
When using the cookies backend the session data can be read out
|
||||
and will be invalidated when being tampered with. The same invalidation
|
||||
happens if the client storing the cookie (e.g. your user's browser)
|
||||
can't store all of the session cookie and drops data. Even though
|
||||
Django compresses the data, it's still entirely possible to exceed
|
||||
the `common limit of 4096 bytes`_ per cookie.
|
||||
|
||||
Also, the size of a cookie can have an impact on the `speed of your site`_.
|
||||
|
||||
.. _`common limit of 4096 bytes`: http://tools.ietf.org/html/rfc2965#section-5.3
|
||||
.. _`speed of your site`: http://yuiblog.com/blog/2007/03/01/performance-research-part-3/
|
||||
|
||||
Using sessions in views
|
||||
=======================
|
||||
|
@ -420,6 +453,7 @@ Controls where Django stores session data. Valid values are:
|
|||
* ``'django.contrib.sessions.backends.file'``
|
||||
* ``'django.contrib.sessions.backends.cache'``
|
||||
* ``'django.contrib.sessions.backends.cached_db'``
|
||||
* ``'django.contrib.sessions.backends.signed_cookies'``
|
||||
|
||||
See `configuring the session engine`_ for more details.
|
||||
|
||||
|
|
Loading…
Reference in New Issue