Fixed #2066: session data can now be stored in the cache or on the filesystem. This should be fully backwards-compatible (the database cache store is still the default). A big thanks to John D'Agostino for the bulk of this code.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@6333 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Jacob Kaplan-Moss 2007-09-15 21:29:14 +00:00
parent e6460e4134
commit bcf7e9a9fe
13 changed files with 469 additions and 122 deletions

View File

@ -87,6 +87,7 @@ answer newbie questions, and generally made Django that much better:
Matt Croydon <http://www.postneo.com/>
flavio.curella@gmail.com
Jure Cuhalev <gandalf@owca.info>
John D'Agostino <john.dagostino@gmail.com>
dackze+django@gmail.com
David Danier <goliath.mailinglist@gmx.de>
Dirk Datzert <dummy@habmalnefrage.de>

View File

@ -277,6 +277,8 @@ SESSION_COOKIE_DOMAIN = None # A string like ".lawrence.com", or No
SESSION_COOKIE_SECURE = False # Whether the session cookie should be secure (https:// only).
SESSION_SAVE_EVERY_REQUEST = False # Whether to save the session data on every request.
SESSION_EXPIRE_AT_BROWSER_CLOSE = False # Whether sessions expire when a user closes his browser.
SESSION_ENGINE = 'django.contrib.sessions.backends.db' # The module to store session data
SESSION_FILE_PATH = '/tmp/' # Directory to store session files if using the file session module
#########
# CACHE #

View File

@ -0,0 +1,143 @@
import base64
import md5
import os
import random
import sys
import time
from django.conf import settings
from django.core.exceptions import SuspiciousOperation
try:
import cPickle as pickle
except ImportError:
import pickle
class SessionBase(object):
"""
Base class for all Session classes.
"""
TEST_COOKIE_NAME = 'testcookie'
TEST_COOKIE_VALUE = 'worked'
def __init__(self, session_key=None):
self._session_key = session_key
self.accessed = False
self.modified = False
def __contains__(self, key):
return key in self._session
def __getitem__(self, key):
return self._session[key]
def __setitem__(self, key, value):
self._session[key] = value
self.modified = True
def __delitem__(self, key):
del self._session[key]
self.modified = True
def keys(self):
return self._session.keys()
def items(self):
return self._session.items()
def get(self, key, default=None):
return self._session.get(key, default)
def pop(self, key, *args):
return self._session.pop(key, *args)
def set_test_cookie(self):
self[self.TEST_COOKIE_NAME] = self.TEST_COOKIE_VALUE
def test_cookie_worked(self):
return self.get(self.TEST_COOKIE_NAME) == self.TEST_COOKIE_VALUE
def delete_test_cookie(self):
del self[self.TEST_COOKIE_NAME]
def encode(self, session_dict):
"Returns the given session dictionary pickled and encoded as a string."
pickled = pickle.dumps(session_dict, pickle.HIGHEST_PROTOCOL)
pickled_md5 = md5.new(pickled + settings.SECRET_KEY).hexdigest()
return base64.encodestring(pickled + pickled_md5)
def decode(self, session_data):
encoded_data = base64.decodestring(session_data)
pickled, tamper_check = encoded_data[:-32], encoded_data[-32:]
if md5.new(pickled + settings.SECRET_KEY).hexdigest() != tamper_check:
raise SuspiciousOperation("User tampered with session cookie.")
try:
return pickle.loads(pickled)
# Unpickling can cause a variety of exceptions. If something happens,
# just return an empty dictionary (an empty session).
except:
return {}
def _get_new_session_key(self):
"Returns session key that isn't being used."
# The random module is seeded when this Apache child is created.
# Use settings.SECRET_KEY as added salt.
while 1:
session_key = md5.new("%s%s%s%s" % (random.randint(0, sys.maxint - 1),
os.getpid(), time.time(), settings.SECRET_KEY)).hexdigest()
if not self.exists(session_key):
break
return session_key
def _get_session_key(self):
if self._session_key:
return self._session_key
else:
self._session_key = self._get_new_session_key()
return self._session_key
def _set_session_key(self, session_key):
self._session_key = session_key
session_key = property(_get_session_key, _set_session_key)
def _get_session(self):
# Lazily loads session from storage.
self.accessed = True
try:
return self._session_cache
except AttributeError:
if self.session_key is None:
self._session_cache = {}
else:
self._session_cache = self.load()
return self._session_cache
_session = property(_get_session)
# Methods that child classes must implement.
def exists(self, session_key):
"""
Returns True if the given session_key already exists.
"""
raise NotImplementedError
def save(self):
"""
Saves the session data.
"""
raise NotImplementedError
def delete(self, session_key):
"""
Clears out the session data under this key.
"""
raise NotImplementedError
def load(self):
"""
Loads the session data and returns a dictionary.
"""
raise NotImplementedError

View File

@ -0,0 +1,26 @@
from django.conf import settings
from django.contrib.sessions.backends.base import SessionBase
from django.core.cache import cache
class SessionStore(SessionBase):
"""
A cache-based session store.
"""
def __init__(self, session_key=None):
self._cache = cache
super(SessionStore, self).__init__(session_key)
def load(self):
session_data = self._cache.get(self.session_key)
return session_data or {}
def save(self):
self._cache.set(self.session_key, self._session, settings.SESSION_COOKIE_AGE)
def exists(self, session_key):
if self._cache.get(session_key):
return True
return False
def delete(self, session_key):
self._cache.delete(session_key)

View File

@ -0,0 +1,49 @@
from django.conf import settings
from django.contrib.sessions.models import Session
from django.contrib.sessions.backends.base import SessionBase
from django.core.exceptions import SuspiciousOperation
import datetime
class SessionStore(SessionBase):
"""
Implements database session store
"""
def __init__(self, session_key=None):
super(SessionStore, self).__init__(session_key)
def load(self):
try:
s = Session.objects.get(
session_key = self.session_key,
expire_date__gt=datetime.datetime.now()
)
return self.decode(s.session_data)
except (Session.DoesNotExist, SuspiciousOperation):
# Create a new session_key for extra security.
self.session_key = self._get_new_session_key()
self._session_cache = {}
# Save immediately to minimize collision
self.save()
return {}
def exists(self, session_key):
try:
Session.objects.get(session_key=session_key)
except Session.DoesNotExist:
return False
return True
def save(self):
Session.objects.create(
session_key = self.session_key,
session_data = self.encode(self._session),
expire_date = datetime.datetime.now() + datetime.timedelta(seconds=settings.SESSION_COOKIE_AGE)
)
def delete(self, session_key):
try:
Session.objects.get(session_key=session_key).delete()
except Session.DoesNotExist:
pass

View File

@ -0,0 +1,67 @@
import os
from django.conf import settings
from django.contrib.sessions.backends.base import SessionBase
from django.core.exceptions import SuspiciousOperation
class SessionStore(SessionBase):
"""
Implements a file based session store.
"""
def __init__(self, session_key=None):
self.storage_path = settings.SESSION_FILE_PATH
self.file_prefix = settings.SESSION_COOKIE_NAME
super(SessionStore, self).__init__(session_key)
def _key_to_file(self, session_key=None):
"""
Get the file associated with this session key.
"""
if session_key is None:
session_key = self.session_key
# Make sure we're not vulnerable to directory traversal. Session keys
# should always be md5s, so they should never contain directory components.
if os.path.sep in session_key:
raise SuspiciousOperation("Invalid characters (directory components) in session key")
return os.path.join(self.storage_path, self.file_prefix + session_key)
def load(self):
session_data = {}
try:
session_file = open(self._key_to_file(), "rb")
try:
session_data = self.decode(session_file.read())
except(EOFError, SuspiciousOperation):
self._session_key = self._get_new_session_key()
self._session_cache = {}
self.save()
finally:
session_file.close()
except(IOError):
pass
return session_data
def save(self):
try:
f = open(self._key_to_file(self.session_key), "wb")
try:
f.write(self.encode(self._session))
finally:
f.close()
except(IOError, EOFError):
pass
def exists(self, session_key):
if os.path.exists(self._key_to_file(session_key)):
return True
return False
def delete(self, session_key):
try:
os.unlink(self._key_to_file(session_key))
except OSError:
pass
def clean(self):
pass

View File

@ -1,6 +1,4 @@
from django.conf import settings
from django.contrib.sessions.models import Session
from django.core.exceptions import SuspiciousOperation
from django.utils.cache import patch_vary_headers
from email.Utils import formatdate
import datetime
@ -9,73 +7,11 @@ import time
TEST_COOKIE_NAME = 'testcookie'
TEST_COOKIE_VALUE = 'worked'
class SessionWrapper(object):
def __init__(self, session_key):
self.session_key = session_key
self.accessed = False
self.modified = False
def __contains__(self, key):
return key in self._session
def __getitem__(self, key):
return self._session[key]
def __setitem__(self, key, value):
self._session[key] = value
self.modified = True
def __delitem__(self, key):
del self._session[key]
self.modified = True
def keys(self):
return self._session.keys()
def items(self):
return self._session.items()
def get(self, key, default=None):
return self._session.get(key, default)
def pop(self, key, *args):
self.modified = self.modified or key in self._session
return self._session.pop(key, *args)
def set_test_cookie(self):
self[TEST_COOKIE_NAME] = TEST_COOKIE_VALUE
def test_cookie_worked(self):
return self.get(TEST_COOKIE_NAME) == TEST_COOKIE_VALUE
def delete_test_cookie(self):
del self[TEST_COOKIE_NAME]
def _get_session(self):
# Lazily loads session from storage.
self.accessed = True
try:
return self._session_cache
except AttributeError:
if self.session_key is None:
self._session_cache = {}
else:
try:
s = Session.objects.get(session_key=self.session_key,
expire_date__gt=datetime.datetime.now())
self._session_cache = s.get_decoded()
except (Session.DoesNotExist, SuspiciousOperation):
self._session_cache = {}
# Set the session_key to None to force creation of a new
# key, for extra security.
self.session_key = None
return self._session_cache
_session = property(_get_session)
class SessionMiddleware(object):
def process_request(self, request):
request.session = SessionWrapper(request.COOKIES.get(settings.SESSION_COOKIE_NAME, None))
engine = __import__(settings.SESSION_ENGINE, {}, {}, [''])
request.session = engine.SessionStore(request.COOKIES.get(settings.SESSION_COOKIE_NAME, None))
def process_response(self, request, response):
# If request.session was modified, or if response.session was set, save
@ -89,25 +25,22 @@ class SessionMiddleware(object):
if accessed:
patch_vary_headers(response, ('Cookie',))
if modified or settings.SESSION_SAVE_EVERY_REQUEST:
if request.session.session_key:
session_key = request.session.session_key
else:
obj = Session.objects.get_new_session_object()
session_key = obj.session_key
if settings.SESSION_EXPIRE_AT_BROWSER_CLOSE:
max_age = None
expires = None
else:
max_age = settings.SESSION_COOKIE_AGE
rfcdate = formatdate(time.time() + settings.SESSION_COOKIE_AGE)
# Fixed length date must have '-' separation in the format
# DD-MMM-YYYY for compliance with Netscape cookie standard
expires = (rfcdate[:7] + "-" + rfcdate[8:11]
+ "-" + rfcdate[12:26] + "GMT")
new_session = Session.objects.save(session_key, request.session._session,
datetime.datetime.now() + datetime.timedelta(seconds=settings.SESSION_COOKIE_AGE))
response.set_cookie(settings.SESSION_COOKIE_NAME, session_key,
expires = datetime.datetime.strftime(datetime.datetime.utcnow() + \
datetime.timedelta(seconds=settings.SESSION_COOKIE_AGE), "%a, %d-%b-%Y %H:%M:%S GMT")
# Save the seesion data and refresh the client cookie.
request.session.save()
response.set_cookie(settings.SESSION_COOKIE_NAME, request.session.session_key,
max_age=max_age, expires=expires, domain=settings.SESSION_COOKIE_DOMAIN,
secure=settings.SESSION_COOKIE_SECURE or None)
return response

View File

@ -1,4 +1,4 @@
import base64, md5, random, sys, datetime, os, time
import base64, md5, random, sys, datetime
import cPickle as pickle
from django.db import models
from django.utils.translation import ugettext_lazy as _
@ -74,6 +74,7 @@ class Session(models.Model):
session_data = models.TextField(_('session data'))
expire_date = models.DateTimeField(_('expire date'))
objects = SessionManager()
class Meta:
db_table = 'django_session'
verbose_name = _('session')

View File

@ -1,35 +1,59 @@
r"""
>>> s = SessionWrapper(None)
Inject data into the session cache.
>>> s._session_cache = {}
>>> s._session_cache['some key'] = 'exists'
>>> 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.file import SessionStore as FileSession
>>> s.accessed
>>> db_session = DatabaseSession()
>>> db_session.modified
False
>>> s.modified
False
>>> s.pop('non existant key', 'does not exist')
>>> db_session['cat'] = "dog"
>>> db_session.modified
True
>>> db_session.pop('cat')
'dog'
>>> db_session.pop('some key', 'does not exist')
'does not exist'
>>> s.accessed
>>> db_session.save()
>>> db_session.exists(db_session.session_key)
True
>>> s.modified
>>> db_session.delete(db_session.session_key)
>>> db_session.exists(db_session.session_key)
False
>>> s.pop('some key')
'exists'
>>> s.accessed
>>> file_session = FileSession()
>>> file_session.modified
False
>>> file_session['cat'] = "dog"
>>> file_session.modified
True
>>> s.modified
True
>>> s.pop('some key', 'does not exist')
>>> file_session.pop('cat')
'dog'
>>> file_session.pop('some key', 'does not exist')
'does not exist'
>>> file_session.save()
>>> file_session.exists(file_session.session_key)
True
>>> file_session.delete(file_session.session_key)
>>> file_session.exists(file_session.session_key)
False
>>> cache_session = CacheSession()
>>> cache_session.modified
False
>>> cache_session['cat'] = "dog"
>>> cache_session.modified
True
>>> cache_session.pop('cat')
'dog'
>>> cache_session.pop('some key', 'does not exist')
'does not exist'
>>> cache_session.save()
>>> cache_session.delete(cache_session.session_key)
>>> cache_session.exists(cache_session.session_key)
False
"""
from django.contrib.sessions.middleware import SessionWrapper
if __name__ == '__main__':
import doctest
doctest.testmod()

View File

@ -4,8 +4,6 @@ from cStringIO import StringIO
from urlparse import urlparse
from django.conf import settings
from django.contrib.auth import authenticate, login
from django.contrib.sessions.models import Session
from django.contrib.sessions.middleware import SessionWrapper
from django.core.handlers.base import BaseHandler
from django.core.handlers.wsgi import WSGIRequest
from django.core.signals import got_request_exception
@ -132,9 +130,10 @@ class Client:
def _session(self):
"Obtain the current session variables"
if 'django.contrib.sessions' in settings.INSTALLED_APPS:
engine = __import__(settings.SESSION_ENGINE, {}, {}, [''])
cookie = self.cookies.get(settings.SESSION_COOKIE_NAME, None)
if cookie:
return SessionWrapper(cookie.value)
return engine.SessionClass(cookie.value)
return {}
session = property(_session)
@ -247,24 +246,23 @@ class Client:
"""
user = authenticate(**credentials)
if user and user.is_active and 'django.contrib.sessions' in settings.INSTALLED_APPS:
obj = Session.objects.get_new_session_object()
engine = __import__(settings.SESSION_ENGINE, {}, {}, [''])
# Create a fake request to store login details
request = HttpRequest()
request.session = SessionWrapper(obj.session_key)
request.session = engine.SessionClass()
login(request, user)
# Set the cookie to represent the session
self.cookies[settings.SESSION_COOKIE_NAME] = obj.session_key
self.cookies[settings.SESSION_COOKIE_NAME] = request.session.session_key
self.cookies[settings.SESSION_COOKIE_NAME]['max-age'] = None
self.cookies[settings.SESSION_COOKIE_NAME]['path'] = '/'
self.cookies[settings.SESSION_COOKIE_NAME]['domain'] = settings.SESSION_COOKIE_DOMAIN
self.cookies[settings.SESSION_COOKIE_NAME]['secure'] = settings.SESSION_COOKIE_SECURE or None
self.cookies[settings.SESSION_COOKIE_NAME]['expires'] = None
# Set the session values
Session.objects.save(obj.session_key, request.session._session,
datetime.datetime.now() + datetime.timedelta(seconds=settings.SESSION_COOKIE_AGE))
# Save the session values
request.session.save()
return True
else:

View File

@ -10,18 +10,21 @@ Cookies contain a session ID -- not the data itself.
Enabling sessions
=================
Sessions are implemented via a piece of middleware_ and a Django model.
Sessions are implemented via a piece of middleware_.
To enable session functionality, do these two things:
To enable session functionality, do the following:
* Edit the ``MIDDLEWARE_CLASSES`` setting and make sure
``MIDDLEWARE_CLASSES`` contains ``'django.contrib.sessions.middleware.SessionMiddleware'``.
The default ``settings.py`` created by ``django-admin.py startproject`` has
``SessionMiddleware`` activated.
* Add ``'django.contrib.sessions'`` to your ``INSTALLED_APPS`` setting, and
run ``manage.py syncdb`` to install the single database table that stores
session data.
* Add ``'django.contrib.sessions'`` to your ``INSTALLED_APPS`` setting,
and run ``manage.py syncdb`` to install the single database table
that stores session data.
**New in development version**: this step is optional if you're not using
the database session backend; see `configuring the session engine`_.
If you don't want to use sessions, you might as well remove the
``SessionMiddleware`` line from ``MIDDLEWARE_CLASSES`` and ``'django.contrib.sessions'``
@ -29,6 +32,44 @@ from your ``INSTALLED_APPS``. It'll save you a small bit of overhead.
.. _middleware: ../middleware/
Configuring the session engine
==============================
**New in development version**.
By default, Django stores sessions in your database (using the model
``django.contrib.sessions.models.Session``). Though this is convenient, in
some setups it's faster to store session data elsewhere, so Django can be
configured to store session data on your filesystem or in your cache.
Using file-based sessions
-------------------------
To use file-based sessions, set the ``SESSION_ENGINE`` setting to
``"django.contrib.sessions.backends.file"``.
You might also want to set the ``SESSION_FILE_PATH`` setting (which
defaults to ``/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.
Using cache-based sessions
--------------------------
To store session data using Django's cache system, set ``SESSION_ENGINE``
to ``"django.contrib.sessions.backends.cache"``. You'll want to make sure
you've configured your cache; see the `cache documentation`_ for details.
.. _cache documentation: ../cache/
.. note::
You probably don't want to use cache-based sessions if you're not using
the memcached cache backend. The local memory and simple cache backends
don't retain data long enough to be good choices, and it'll be faster
to use file or database sessions directly instead of sending everything
through the file or database cache backends.
Using sessions in views
=======================
@ -153,7 +194,18 @@ Here's a typical usage example::
Using sessions out of views
===========================
Internally, each session is just a normal Django model. The ``Session`` model
The ``SessionStore`` which implements the session storage method can be imported
and a API is available to manipulate the session data outside of a view::
>>> from django.contrib.sessions.engines.db import SessionStore
>>> s = SessionStore(session_key='2b1189a188b44ad18c35e113ac6ceead')
>>> s['last_login'] = datetime.datetime(2005, 8, 20, 13, 35, 10)
>>> s['last_login']
datetime.datetime(2005, 8, 20, 13, 35, 0)
>>> s.save()
Or if you are using the ``django.contrib.sessions.engine.db`` 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::
@ -245,6 +297,31 @@ Settings
A few `Django settings`_ give you control over session behavior:
SESSION_ENGINE
--------------
**New in Django development version**
Default: ``django.contrib.sessions.backends.db``
Controls where Django stores session data. Valid values are:
* ``'django.contrib.sessions.backends.db'``
* ``'django.contrib.sessions.backends.file'``
* ``'django.contrib.sessions.backends.cache'``
See `configuring the session engine`_ for more details.
SESSION_FILE_PATH
-----------------
**New in Django development version**
Default: ``/tmp/``
If you're using file-based session storage, this sets the directory in
which Django will store session data.
SESSION_COOKIE_AGE
------------------

View File

@ -733,6 +733,21 @@ Default: ``'root@localhost'``
The e-mail address that error messages come from, such as those sent to
``ADMINS`` and ``MANAGERS``.
SESSION_ENGINE
--------------
**New in Django development version**
Default: ``django.contrib.sessions.backends.db``
Controls where Django stores session data. Valid values are:
* ``'django.contrib.sessions.backends.db'``
* ``'django.contrib.sessions.backends.file'``
* ``'django.contrib.sessions.backends.cache'``
See the `session docs`_ for more details.
SESSION_COOKIE_AGE
------------------
@ -775,6 +790,17 @@ Default: ``False``
Whether to expire the session when the user closes his or her browser.
See the `session docs`_.
SESSION_FILE_PATH
-----------------
**New in Django development version**
Default: ``/tmp/``
If you're using file-based session storage, this sets the directory in
which Django will store session data. See the `session docs`_ for
more details.
SESSION_SAVE_EVERY_REQUEST
--------------------------