Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

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
  • Loading branch information...
commit c817f2f5449058c2787298d984167bf590ca7967 1 parent bc56c76
@jezdez jezdez authored
View
93 django/contrib/sessions/backends/signed_cookies.py
@@ -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)
View
77 django/contrib/sessions/tests.py
@@ -7,11 +7,13 @@
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 @@ def test_custom_expiry_reset(self):
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
-
- 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())
+ 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(None)
- self.assertFalse(self.session.get_expire_at_browser_close())
+ self.session.set_expiry(0)
+ self.assertTrue(self.session.get_expire_at_browser_close())
- settings.SESSION_EXPIRE_AT_BROWSER_CLOSE = True
+ self.session.set_expiry(None)
+ self.assertFalse(self.session.get_expire_at_browser_close())
- self.session.set_expiry(10)
- 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())
- 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.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 @@ def tearDown(self):
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 @@ def test_secure_session_cookie(self):
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 @@ def test_httponly_session_cookie(self):
# 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
View
55 django/core/signing.py
@@ -3,33 +3,33 @@
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 @@ def unsign(self, signed_value):
class TimestampSigner(Signer):
+
def timestamp(self):
return baseconv.base62.encode(int(time.time()))
View
10 docs/releases/1.4.txt
@@ -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
~~~~~~~~~~~~~~~
View
42 docs/topics/http/sessions.txt
@@ -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.
Please sign in to comment.
Something went wrong with that request. Please try again.