Ticket #16199: 16199.7.diff
File 16199.7.diff, 17.4 KB (added by , 13 years ago) |
---|
-
new file django/contrib/sessions/backends/signed_cookies.py
diff --git a/django/contrib/sessions/backends/signed_cookies.py b/django/contrib/sessions/backends/signed_cookies.py new file mode 100644 index 0000000..966cbf6
- + 1 try: 2 import cPickle as pickle 3 except ImportError: 4 import pickle 5 6 from django.conf import settings 7 from django.core import signing 8 9 from django.contrib.sessions.backends.base import SessionBase 10 11 12 class PickleSerializer(object): 13 """ 14 Simple wrapper around pickle to be used in signing.dumps and 15 signing.loads. 16 """ 17 def dumps(self, obj): 18 return pickle.dumps(obj, pickle.HIGHEST_PROTOCOL) 19 20 def loads(self, data): 21 return pickle.loads(data) 22 23 24 class SessionStore(SessionBase): 25 26 def load(self): 27 """ 28 We load the data from the key itself instead of fetching from 29 some external data store. Opposite of _get_session_key(), 30 raises BadSignature if signature fails. 31 """ 32 try: 33 return signing.loads(self._session_key, 34 serializer=PickleSerializer, 35 max_age=settings.SESSION_COOKIE_AGE, 36 salt='django.contrib.sessions.backends.cookies') 37 except (signing.BadSignature, ValueError): 38 self.create() 39 return {} 40 41 def create(self): 42 """ 43 To create a new key, we simply make sure that the modified flag is set 44 so that the cookie is set on the client for the current request. 45 """ 46 self.modified = True 47 48 def save(self, must_create=False): 49 """ 50 To save, we get the session key as a securely signed string and then 51 set the modified flag so that the cookie is set on the client for the 52 current request. 53 """ 54 self._session_key = self._get_session_key() 55 self.modified = True 56 57 def exists(self, session_key=None): 58 """ 59 This method makes sense when you're talking to a shared resource, but 60 it doesn't matter when you're storing the information in the client's 61 cookie. 62 """ 63 return False 64 65 def delete(self, session_key=None): 66 """ 67 To delete, we clear the session key and the underlying data structure 68 and set the modified flag so that the cookie is set on the client for 69 the current request. 70 """ 71 self._session_key = '' 72 self._session_cache = {} 73 self.modified = True 74 75 def cycle_key(self): 76 """ 77 Keeps the same data but with a new key. To do this, we just have to 78 call ``save()`` and it will automatically save a cookie with a new key 79 at the end of the request. 80 """ 81 self.save() 82 83 def _get_session_key(self): 84 """ 85 Most session backends don't need to override this method, but we do, 86 because instead of generating a random string, we want to actually 87 generate a secure url-safe Base64-encoded string of data as our 88 session key. 89 """ 90 session_cache = getattr(self, '_session_cache', {}) 91 return signing.dumps(session_cache, compress=True, 92 salt='django.contrib.sessions.backends.cookies', 93 serializer=PickleSerializer) -
django/contrib/sessions/tests.py
diff --git a/django/contrib/sessions/tests.py b/django/contrib/sessions/tests.py index 2eb43f3..2556275 100644
a b from django.contrib.sessions.backends.db import SessionStore as DatabaseSession 7 7 from django.contrib.sessions.backends.cache import SessionStore as CacheSession 8 8 from django.contrib.sessions.backends.cached_db import SessionStore as CacheDBSession 9 9 from django.contrib.sessions.backends.file import SessionStore as FileSession 10 from django.contrib.sessions.backends.cookies import SessionStore as CookieSession 10 11 from django.contrib.sessions.models import Session 11 12 from django.contrib.sessions.middleware import SessionMiddleware 12 13 from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation 13 14 from django.http import HttpResponse 14 15 from django.test import TestCase, RequestFactory 16 from django.test.utils import override_settings 15 17 from django.utils import unittest 16 18 17 19 … … class SessionTestsMixin(object): 213 215 def test_get_expire_at_browser_close(self): 214 216 # Tests get_expire_at_browser_close with different settings and different 215 217 # set_expiry calls 216 try: 217 try: 218 original_expire_at_browser_close = settings.SESSION_EXPIRE_AT_BROWSER_CLOSE 219 settings.SESSION_EXPIRE_AT_BROWSER_CLOSE = False 220 221 self.session.set_expiry(10) 222 self.assertFalse(self.session.get_expire_at_browser_close()) 223 224 self.session.set_expiry(0) 225 self.assertTrue(self.session.get_expire_at_browser_close()) 218 with override_settings(SESSION_EXPIRE_AT_BROWSER_CLOSE=False): 219 self.session.set_expiry(10) 220 self.assertFalse(self.session.get_expire_at_browser_close()) 226 221 227 self.session.set_expiry(None)228 self.assertFalse(self.session.get_expire_at_browser_close())222 self.session.set_expiry(0) 223 self.assertTrue(self.session.get_expire_at_browser_close()) 229 224 230 settings.SESSION_EXPIRE_AT_BROWSER_CLOSE = True 225 self.session.set_expiry(None) 226 self.assertFalse(self.session.get_expire_at_browser_close()) 231 227 232 self.session.set_expiry(10) 233 self.assertFalse(self.session.get_expire_at_browser_close()) 228 with override_settings(SESSION_EXPIRE_AT_BROWSER_CLOSE=True): 229 self.session.set_expiry(10) 230 self.assertFalse(self.session.get_expire_at_browser_close()) 234 231 235 236 232 self.session.set_expiry(0) 233 self.assertTrue(self.session.get_expire_at_browser_close()) 237 234 238 self.session.set_expiry(None) 239 self.assertTrue(self.session.get_expire_at_browser_close()) 240 241 except: 242 raise 243 finally: 244 settings.SESSION_EXPIRE_AT_BROWSER_CLOSE = original_expire_at_browser_close 235 self.session.set_expiry(None) 236 self.assertTrue(self.session.get_expire_at_browser_close()) 245 237 246 238 def test_decode(self): 247 239 # Ensure we can decode what we encode … … class FileSessionTests(SessionTestsMixin, unittest.TestCase): 302 294 shutil.rmtree(self.temp_session_store) 303 295 super(FileSessionTests, self).tearDown() 304 296 297 @override_settings( 298 SESSION_FILE_PATH="/if/this/directory/exists/you/have/a/weird/computer") 305 299 def test_configuration_check(self): 306 300 # Make sure the file backend checks for a good storage dir 307 settings.SESSION_FILE_PATH = "/if/this/directory/exists/you/have/a/weird/computer"308 301 self.assertRaises(ImproperlyConfigured, self.backend) 309 302 310 303 def test_invalid_key_backslash(self): … … class CacheSessionTests(SessionTestsMixin, unittest.TestCase): 324 317 325 318 326 319 class SessionMiddlewareTests(unittest.TestCase): 327 def setUp(self):328 self.old_SESSION_COOKIE_SECURE = settings.SESSION_COOKIE_SECURE329 self.old_SESSION_COOKIE_HTTPONLY = settings.SESSION_COOKIE_HTTPONLY330 331 def tearDown(self):332 settings.SESSION_COOKIE_SECURE = self.old_SESSION_COOKIE_SECURE333 settings.SESSION_COOKIE_HTTPONLY = self.old_SESSION_COOKIE_HTTPONLY334 320 321 @override_settings(SESSION_COOKIE_SECURE=True) 335 322 def test_secure_session_cookie(self): 336 settings.SESSION_COOKIE_SECURE = True337 338 323 request = RequestFactory().get('/') 339 324 response = HttpResponse('Session test') 340 325 middleware = SessionMiddleware() … … class SessionMiddlewareTests(unittest.TestCase): 347 332 response = middleware.process_response(request, response) 348 333 self.assertTrue(response.cookies[settings.SESSION_COOKIE_NAME]['secure']) 349 334 335 @override_settings(SESSION_COOKIE_HTTPONLY=True) 350 336 def test_httponly_session_cookie(self): 351 settings.SESSION_COOKIE_HTTPONLY = True352 353 337 request = RequestFactory().get('/') 354 338 response = HttpResponse('Session test') 355 339 middleware = SessionMiddleware() … … class SessionMiddlewareTests(unittest.TestCase): 361 345 # Handle the response through the middleware 362 346 response = middleware.process_response(request, response) 363 347 self.assertTrue(response.cookies[settings.SESSION_COOKIE_NAME]['httponly']) 348 349 350 class CookieSessionTests(SessionTestsMixin, TestCase): 351 352 backend = CookieSession 353 354 def test_save(self): 355 """ 356 This test tested exists() in the other session backends, but that 357 doesn't make sense for us. 358 """ 359 pass 360 361 def test_cycle(self): 362 """ 363 This test tested cycle_key() which would create a new session 364 key for the same session data. But we can't invalidate previously 365 signed cookies (other than letting them expire naturally) so 366 testing for this behaviour is meaningless. 367 """ 368 pass -
django/core/signing.py
diff --git a/django/core/signing.py b/django/core/signing.py index 054777a..8987d9d 100644
a b Functions for creating and restoring url-safe signed JSON objects. 3 3 4 4 The format used looks like this: 5 5 6 >>> sign ed.dumps("hello")7 'ImhlbGxvIg .RjVSUCt6S64WBilMYxG89-l0OA8'6 >>> signing.dumps("hello") 7 'ImhlbGxvIg:1QaUZC:YIye-ze3TTx7gtSv422nZA4sgmk' 8 8 9 There are two components here, separatad by a ' .'. The first component is a9 There are two components here, separatad by a ':'. The first component is a 10 10 URLsafe base64 encoded JSON of the object passed to dumps(). The second 11 component is a base64 encoded hmac/SHA1 hash of "$first_component .$secret"11 component is a base64 encoded hmac/SHA1 hash of "$first_component:$secret" 12 12 13 sign ed.loads(s) checks the signature and returns the deserialised object.13 signing.loads(s) checks the signature and returns the deserialised object. 14 14 If the signature fails, a BadSignature exception is raised. 15 15 16 >>> sign ed.loads("ImhlbGxvIg.RjVSUCt6S64WBilMYxG89-l0OA8")16 >>> signing.loads("ImhlbGxvIg:1QaUZC:YIye-ze3TTx7gtSv422nZA4sgmk") 17 17 u'hello' 18 >>> sign ed.loads("ImhlbGxvIg.RjVSUCt6S64WBilMYxG89-l0OA8-modified")18 >>> signing.loads("ImhlbGxvIg:1QaUZC:YIye-ze3TTx7gtSv422nZA4sgmk-modified") 19 19 ... 20 BadSignature: Signature failed: RjVSUCt6S64WBilMYxG89-l0OA8-modified20 BadSignature: Signature failed: ImhlbGxvIg:1QaUZC:YIye-ze3TTx7gtSv422nZA4sgmk-modified 21 21 22 22 You can optionally compress the JSON prior to base64 encoding it to save 23 23 space, using the compress=True argument. This checks if compression actually 24 24 helps and only applies compression if the result is a shorter string: 25 25 26 >>> sign ed.dumps(range(1, 20), compress=True)27 '.eJwFwcERACAIwLCF-rCiILN47r-GyZVJsNgkxaFxoDgxcOHGxMKD_T7vhAml .oFq6lAAEbkHXBHfGnVX7Qx6NlZ8'26 >>> signing.dumps(range(1, 20), compress=True) 27 '.eJwFwcERACAIwLCF-rCiILN47r-GyZVJsNgkxaFxoDgxcOHGxMKD_T7vhAml:1QaUaL:BA0thEZrp4FQVXIXuOvYJtLJSrQ' 28 28 29 29 The fact that the string is compressed is signalled by the prefixed '.' at the 30 30 start of the base64 JSON. 31 31 32 There are 65 url-safe characters: the 64 used by url-safe base64 and the ' .'.32 There are 65 url-safe characters: the 64 used by url-safe base64 and the ':'. 33 33 These functions make use of all of them. 34 34 """ 35 35 import base64 … … def get_cookie_signer(salt='django.core.signing.get_cookie_signer'): 87 87 return Signer('django.http.cookies' + settings.SECRET_KEY, salt=salt) 88 88 89 89 90 def dumps(obj, key=None, salt='django.core.signing', compress=False): 90 class JSONSerializer(object): 91 """ 92 Simple wrapper around simplejson to be used in signing.dumps and 93 signing.loads. 94 """ 95 def dumps(self, obj): 96 return simplejson.dumps(obj, separators=(',', ':')) 97 98 def loads(self, data): 99 return simplejson.loads(data) 100 101 102 def dumps(obj, key=None, salt='django.core.signing', serializer=JSONSerializer, compress=False): 91 103 """ 92 104 Returns URL-safe, sha1 signed base64 compressed JSON string. If key is 93 105 None, settings.SECRET_KEY is used instead. … … def dumps(obj, key=None, salt='django.core.signing', compress=False): 99 111 Salt can be used to further salt the hash, in case you're worried 100 112 that the NSA might try to brute-force your SHA-1 protected secret. 101 113 """ 102 json = simplejson.dumps(obj, separators=(',', ':'))114 data = serializer().dumps(obj) 103 115 104 116 # Flag for if it's been compressed or not 105 117 is_compressed = False 106 118 107 119 if compress: 108 120 # Avoid zlib dependency unless compress is being used 109 compressed = zlib.compress( json)110 if len(compressed) < (len( json) - 1):111 json= compressed121 compressed = zlib.compress(data) 122 if len(compressed) < (len(data) - 1): 123 data = compressed 112 124 is_compressed = True 113 base64d = b64_encode( json)125 base64d = b64_encode(data) 114 126 if is_compressed: 115 127 base64d = '.' + base64d 116 128 return TimestampSigner(key, salt=salt).sign(base64d) 117 129 118 130 119 def loads(s, key=None, salt='django.core.signing', max_age=None):131 def loads(s, key=None, salt='django.core.signing', serializer=JSONSerializer, max_age=None): 120 132 """ 121 133 Reverse of dumps(), raises BadSignature if signature fails 122 134 """ … … def loads(s, key=None, salt='django.core.signing', max_age=None): 127 139 # It's compressed; uncompress it first 128 140 base64d = base64d[1:] 129 141 decompress = True 130 json= b64_decode(base64d)142 data = b64_decode(base64d) 131 143 if decompress: 132 json = zlib.decompress(json)133 return s implejson.loads(json)144 data = zlib.decompress(data) 145 return serializer().loads(data) 134 146 135 147 136 148 class Signer(object): … … class Signer(object): 158 170 159 171 160 172 class TimestampSigner(Signer): 173 161 174 def timestamp(self): 162 175 return baseconv.base62.encode(int(time.time())) 163 176 -
docs/releases/1.4.txt
diff --git a/docs/releases/1.4.txt b/docs/releases/1.4.txt index 0635ece..0ab5c05 100644
a b signing in Web applications. 89 89 90 90 See :doc:`cryptographic signing </topics/signing>` docs for more information. 91 91 92 Cookie-based session backend 93 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 94 95 Django 1.4 introduces a new cookie based backend for the session framework 96 which uses the tools for :doc:`cryptographic signing </topics/signing>` to 97 store the session data in the client's browser. 98 99 See the :ref:`cookie-based backend <cookie-session-backend>` docs for 100 more information. 101 92 102 New form wizard 93 103 ~~~~~~~~~~~~~~~ 94 104 -
docs/topics/http/sessions.txt
diff --git a/docs/topics/http/sessions.txt b/docs/topics/http/sessions.txt index 8529f53..19b1e0d 100644
a b How to use sessions 5 5 .. module:: django.contrib.sessions 6 6 :synopsis: Provides session management for Django projects. 7 7 8 Django provides full support for anonymous sessions. The session framework lets 9 you store and retrieve arbitrary data on a per-site-visitor basis. It stores 10 data on the server side and abstracts the sending and receiving of cookies. 11 Cookies contain a session ID -- not the data itself. 8 Django provides full support for anonymous sessions. The session framework 9 lets you store and retrieve arbitrary data on a per-site-visitor basis. It 10 stores data on the server side and abstracts the sending and receiving of 11 cookies. Cookies contain a session ID -- not the data itself (unless you're 12 using the :ref:`cookie based backend<cookie-session-backend>`). 12 13 13 14 Enabling sessions 14 15 ================= … … defaults to output from ``tempfile.gettempdir()``, most likely ``/tmp``) to 95 96 control where Django stores session files. Be sure to check that your Web 96 97 server has permissions to read and write to this location. 97 98 99 .. _cookie-session-backend: 100 101 Using cookie-based sessions 102 --------------------------- 103 104 .. versionadded:: 1.4 105 106 To use cookies-based sessions, set the :setting:`SESSION_ENGINE` setting to 107 ``"django.contrib.sessions.backends.cookies"``. The session data will be 108 stored using Django's tools for :doc:`cryptographic signing </topics/signing>` 109 and the :setting:`SECRET_KEY` setting. It's recommended to set the 110 :setting:`SESSION_COOKIE_HTTPONLY` to ``True`` to prevent tampering from 111 JavaScript. 112 113 .. warning:: 114 115 **The session data is signed but not encrypted!** 116 117 When using the cookies backend the session data can be read out 118 and will be invalidated when being tampered with. The same invalidation 119 happens if the client storing the cookie (e.g. your user's browser) 120 can't store all of the session cookie and drops data. Even though 121 Django compresses the data, it's still entirely possible to exceed 122 the `common limit of 4096 bytes`_ per cookie. 123 124 Also, the size of a cookie can have an impact on the `speed of your site`_. 125 126 .. _`common limit of 4096 bytes`: http://tools.ietf.org/html/rfc2965#section-5.3 127 .. _`speed of your site`: http://yuiblog.com/blog/2007/03/01/performance-research-part-3/ 98 128 99 129 Using sessions in views 100 130 ======================= … … Controls where Django stores session data. Valid values are: 420 450 * ``'django.contrib.sessions.backends.file'`` 421 451 * ``'django.contrib.sessions.backends.cache'`` 422 452 * ``'django.contrib.sessions.backends.cached_db'`` 453 * ``'django.contrib.sessions.backends.signed_cookies'`` 423 454 424 455 See `configuring the session engine`_ for more details. 425 456