Chapter 18 · Authentication, sessions and messages

18.3 The session backends

The session backends are Django’s own modules that SESSION_ENGINE can name: django.contrib.sessions.backends.db, the default, and the cache, cached database, file and signed-cookie backends. Each keeps a session’s data in a place of its own, and they differ most in what happens when that place loses a session or fails.

Each module defines a class called SessionStore, a subclass of SessionBase supplying the storage, under one contract: a save that must create raises CreateError when the key is taken, and one that must update raises UpdateError when the session has gone (The session store: SessionBase). One login of nora’s under each engine, by the test client with a LocMemCache as the cache, left this behind:

Table 18.3 Nora’s login under each engine; the logout removed what each server-side backend had stored.
engine the session after the login the cookie’s value the next page’s queries
django.contrib.sessions.backends.db the session after the login a row of "django_session" the cookie’s value the key, 32 characters the next page’s queries the session’s row; the user
django.contrib.sessions.backends.cache the session after the login an entry of the cache the cookie’s value the key the next page’s queries the user
django.contrib.sessions.backends.cached_db the session after the login the row, and an entry of the cache the cookie’s value the key the next page’s queries the user
django.contrib.sessions.backends.file the session after the login a file in SESSION_FILE_PATH: "sessionid" and the key the cookie’s value the key the next page’s queries the user
django.contrib.sessions.backends.signed_cookies the session after the login nothing on the server the cookie’s value the data, signed: about 225 characters the next page’s queries the user

The database backend

django.contrib.sessions.backends.db stores a session as a row of Session, in the table "django_session", with the fields of the abstract AbstractBaseSession: session_key, the primary key, of up to 40 characters; session_data, the string SessionBase.encode makes; and expire_date, indexed, the moment the session ends as get_expiry_date computed it at the last save. SessionStore.get_model_class imports the model inside itself, which, its comment says, avoids a circular import and lets the store be imported without django.contrib.sessions installed.

Loading asks for the row with the key whose expire_date is later than now. Finding none, missing or expired, the store drops its key and returns an empty dictionary, so a later save makes a new key; a SuspiciousOperation met on the way is logged. The expired row is left in the table: SessionStore.delete deletes one row by its key, and clear_expired every row whose expire_date has passed.

SessionStore.create takes a key that exists reports free and saves at once with must_create=True, to be sure of a unique entry, says its comment; on CreateError it tries another key. SessionStore.save writes inside atomic, on the database ConnectionRouter.db_for_write chooses, forcing an insert when creating and an update otherwise (Transactions: autocommit, atomic and savepoints). An IntegrityError on the insert, a key taken since the check, becomes CreateError. A forced update that finds no row raises Model.NotUpdated, a DatabaseError, which becomes UpdateError: the row was deleted meanwhile, by a concurrent logout, say.

A project wanting more columns subclasses AbstractBaseSession and the database or cached database store, each naming the other, in AbstractBaseSession.get_session_store_class and get_model_class. AbstractBaseSession.get_decoded decodes a row through that store; BaseSessionManager.save, which saves a row, or deletes it for an empty dictionary, Django never calls.

The cache backend

django.contrib.sessions.backends.cache keeps the dictionary itself, unsigned, as an entry of the cache SESSION_CACHE_ALIAS names, "default" unless set, under KEY_PREFIX, "django.contrib.sessions.cache", and the session key (The cache API: caches and BaseCache). SessionStore.save uses the cache’s own operations: creating, it calls add, which stores only under an absent key, and a refusal is CreateError; updating, it sets the entry only if the cache still has it, and otherwise raises UpdateError. The timeout is get_expiry_age, so the cache’s expiry ends the session, and clear_expired does nothing.

SessionStore.load takes any exception from the cache for a miss: some caches, its comment says, memcache among them, raise on a key they refuse (ticket 17810), and the key comes from a cookie; a miss drops the key and gives an empty session. SessionStore.create tries 10,000 keys before raising RuntimeError; a cache can fail silently, says its comment, so a refused add cannot tell a taken key from a missing cache.

So a session lives as long as the cache keeps its entry: one evicted from a full cache or lost at a restart is a user logged out, the documentation warns; and one lost between a load and the save makes the save raise UpdateError, as a store made by hand did, the cache cleared in between. The documentation advises the cached database backend unless the cache is configured to persist (The cache backends).

The cached database backend

django.contrib.sessions.backends.cached_db subclasses the database store and puts the cache of SESSION_CACHE_ALIAS before it, under "django.contrib.sessions.cached_db". SessionStore.save writes through: the row first, then the cache entry. SessionStore.load reads the cache first and, on a miss or an exception, the row, and puts the row’s data in the cache for the time the row has left.

An exception from setting the entry after a save, or from deleting it after the row, is logged to "django.contrib.sessions", and the save or the delete stands. The refill after a read is not guarded, and SessionStore.exists asks the cache before the row: with a cache raising on every call, stores made by hand for an existing row updated and deleted it, but a load raised, and so did a new session’s save.

The file backend

django.contrib.sessions.backends.file keeps a session in a file named SESSION_COOKIE_NAME and the key, in SESSION_FILE_PATH, or tempfile.gettempdir when that is None, its default; a directory that does not exist raises ImproperlyConfigured. A key holding any character outside VALID_KEY_CHARS, a slash or a dot say, raises InvalidSessionKey first, against directory traversal, says the comment; loading takes it for no session.

The file holds only the encoded data, and its modification time stands for the last save: the session expires SESSION_COOKIE_AGE seconds after it unless the data holds an expiry of its own. Loading an expired session deletes the file and at once creates a new session under a new key. An expiry given to set_expiry in seconds, other than 0, is taken as the age remaining whatever the file’s time: a store made by hand given 60 seconds, its file’s time set back past SESSION_COOKIE_AGE, still loaded, and clear_expired kept it.

SessionStore.save meets the contract with the file system: creating, it opens the file with os.O_CREAT and os.O_EXCL, and a file already there is CreateError; updating, without them, and a missing file is UpdateError. It then writes a temporary file beside it and moves it onto the name, which its comment prefers to locking, rejected in part for needing a long-lived lock file for each session (ticket 8616); where os.rename cannot replace a file, as on Windows, the move copies and unlinks (ticket 9084), as a save on Windows did. An OSError in the writing is passed over. SessionStore.clear_expired loads every file whose name begins with the cookie’s name, with create disabled so that, as its comment says, an expired file is removed and not replaced.

django.contrib.sessions.backends.signed_cookies keeps nothing on the server: the key is the data. SessionStore.save makes it with django.core.signing.dumps, compressed, under the salt "django.contrib.sessions.backends.signed_cookies", and marks the session modified, so the middleware sets the cookie (Signing: Signer, TimestampSigner, dumps and loads). SessionStore.load reads it back with loads, accepting an age of SESSION_COOKIE_AGE since the signing whatever set_expiry said, a limit its comment admits (ticket 19201). Any failure gives an empty session marked modified, so a changed cookie made nora anonymous and was replaced by a signed empty session.

Its exists always answers no, create only marks the session modified, and delete empties the key and the data. The base’s cycle_key ends by deleting the old key, which here would empty the data; this backend’s SessionStore.cycle_key only saves, making the key again from the same data.

The documentation warns of what follows. A logout has nothing on the server to delete: nora’s cookie, copied before the logout and sent after it by another test client, still signed nora in, and such a session ends with its age, or when nora’s password changes (The user of a request: login, logout and AuthenticationMiddleware). The data travels with every request, and a cookie the browser cannot keep whole fails its signature, as one cut short did; the documentation gives 4,096 bytes as a common limit. And the data is signed, not encrypted, so the client can read it:

# the first part of nora's "sessionid" cookie, decoded from base64 and decompressed
{"_auth_user_id":"1","_auth_user_backend":"django.contrib.auth.backends.ModelBackend","_auth_user_hash":"8cb0ebc75bc4cfa7e0a1ea65e2fc30bed5c10404a4ca15a459d09e2a3e4c488f"}

Clearing expired sessions

The clearsessions command calls clear_expired on the store of the module SESSION_ENGINE names, and turns the base’s NotImplementedError into a CommandError. The database backends delete expired rows and the file backend loads every file; the cache and signed-cookie backends do nothing, a cache deleting stale data itself and the browser holding the data, as the documentation says. Django provides no automatic purge, says the documentation, which recommends running the command regularly (The commands Django ships).