Cookie-based Session Storage for Django
28 May 2009
20:34
as Python
Here's a simple cookie-based session backend for Django. Session data is JSON encoded and GZIP compressed, and then signed with a MD5 hash based on a server-side secret key. To use this, the default Django session middleware needs to be replaced with the one in this module. The SESSION_ENGINE is henceforth ignored.
| 1 | from base64 import b64decode, b64encode |
|---|---|
| 2 | import md5 |
| 3 | from time import time |
| 4 | import zlib |
| 5 | |
| 6 | from django.conf import settings |
| 7 | from django.contrib.sessions.backends.base import SessionBase |
| 8 | from django.core.exceptions import SuspiciousOperation |
| 9 | from django.utils import simplejson |
| 10 | from django.utils.cache import patch_vary_headers |
| 11 | from django.utils.encoding import force_unicode |
| 12 | from django.utils.http import cookie_date |
| 13 | |
| 14 | MAX_COOKIE_SIZE = 4096 |
| 15 | |
| 16 | |
| 17 | class SessionMiddleware(object): |
| 18 | |
| 19 | def process_request(self, request): |
| 20 | cookie = request.COOKIES.get(settings.SESSION_COOKIE_NAME) |
| 21 | request.session = SessionStore(cookie) |
| 22 | |
| 23 | def process_response(self, request, response): |
| 24 | session = request.session |
| 25 | if session.deleted: |
| 26 | response.delete_cookie(settings.SESSION_COOKIE_NAME) |
| 27 | else: |
| 28 | if session.accessed: |
| 29 | patch_vary_headers(response, ('Cookie',)) |
| 30 | if session.modified or settings.SESSION_SAVE_EVERY_REQUEST: |
| 31 | if session.get_expire_at_browser_close(): |
| 32 | max_age = None |
| 33 | expires = None |
| 34 | else: |
| 35 | max_age = session.get_expiry_age() |
| 36 | expires = cookie_date(time() + max_age) |
| 37 | cookie = session.encode(session._session) |
| 38 | if len(cookie) <= MAX_COOKIE_SIZE: |
| 39 | response.set_cookie(settings.SESSION_COOKIE_NAME, cookie, |
| 40 | max_age = max_age, expires=expires, |
| 41 | domain = settings.SESSION_COOKIE_DOMAIN, |
| 42 | path = settings.SESSION_COOKIE_PATH, |
| 43 | secure = settings.SESSION_COOKIE_SECURE or None |
| 44 | ) |
| 45 | else: |
| 46 | # The data doesn't fit into a cookie, not sure what's the |
| 47 | # best thing to do in this case. Right now, we just leave |
| 48 | # the old cookie intact if there was one. If Django had |
| 49 | # some kind of standard logging interface, we could also |
| 50 | # log a warning here. |
| 51 | pass |
| 52 | return response |
| 53 | |
| 54 | |
| 55 | class SessionStore(SessionBase): |
| 56 | |
| 57 | def __init__(self, cookie): |
| 58 | SessionBase.__init__(self, 'cookie') |
| 59 | self.cookie = cookie |
| 60 | self.deleted = False |
| 61 | |
| 62 | def exists(self, session_key): |
| 63 | return self.cookie and not self.deleted |
| 64 | |
| 65 | def create(self): |
| 66 | pass |
| 67 | |
| 68 | def save(self, must_create=False): |
| 69 | pass |
| 70 | |
| 71 | def delete(self, session_key=None): |
| 72 | self.deleted = True |
| 73 | |
| 74 | def load(self): |
| 75 | if self.cookie: |
| 76 | return self.decode(self.cookie) |
| 77 | return {} |
| 78 | |
| 79 | def cycle_key(self): |
| 80 | pass |
| 81 | |
| 82 | def encode(self, session_dict): |
| 83 | json = simplejson.dumps(session_dict) |
| 84 | json_md5 = md5.new(json + settings.SECRET_KEY).hexdigest() |
| 85 | try: |
| 86 | return b64encode(zlib.compress(json + json_md5)) |
| 87 | except Exception, e: |
| 88 | return '' |
| 89 | |
| 90 | def decode(self, session_data): |
| 91 | try: |
| 92 | data = zlib.decompress(b64decode(session_data)) |
| 93 | except Exception, e: |
| 94 | return {} |
| 95 | json, json_md5 = data[:-32], data[-32:] |
| 96 | if md5.new(json + settings.SECRET_KEY).hexdigest() != json_md5: |
| 97 | raise SuspiciousOperation('User tampered with session cookie') |
| 98 | return simplejson.loads(json) |
Comments
-
cmlenz says:
28 May 2009
21:01Ideally, this would use httpOnly, but I don't think the Django cookies API supports that yet.
-
anonymous says:
21 July 2009
20:51Hello, I am new to django. Can you contribute this code to the django community, perhaps in the way that user can use this code if he / she has SESSION_ENGINE = django.contrib.sessions.backends.cookie ?
-
clay@daemons.net says:
5 August 2009
05:40Very nice, thanks for this, I was just about to implement my own version but have found yours eminently readable. I'm curious, why are signed session cookies more prevalent than encrypted session cookies? It seems to me that encryption would provide the same tamper resistance as signing, while also obscuring the cookie contents from prying users. Is simple symmetric key encryption based on a static secret not strong enough?
-
justquick says:
21 October 2009
17:14I have cleaned up the code and fixed a bug: users could login, but since the domain was not passed when deleting the cookie, users could not logout sucessfully. Here is a full list of the changes: 1) Delete cookie bug fixed. 2) Uses SHA1 instead of MD5. 3) Uses django.utils.hashcompat instead of native module. 4) MAX_COOKIE_SIZE can be overridden in settings.py. 5) Uses encode/decode string methods instead of zlib module. The source code of the updated file is published as a gist here http://gist.github.com/215274
-
mzl says:
19 February 2010
12:52This looks like a nice way to add some simple session info in django. Is there any open-source license attached to the code so that one could use it in a project?
