Thursday, February 26, 2009

Django without cookies but putting the sessionid in the URL

Django sessions require cookies

Django is written to assume that its client is a browser, and that its browser supports cookies. The "How to use sessions" document makes it clear (here) that this is an intentional design decision that use should be well aware of, especially if you want to break those rules as described here. The reasons for that decision are clear and correct, but were not good for our uses.

The Django cookie requirement was not good for our project

But in our own project, a community-based online radio network called YiqYaq, which must support a wide range of client types, I found more and more reasons to want to use cookies. These reasons were:
  • Our clients were often not browsers, but different tools. We provide a simple url-based API to most functionality and forcing those clients to understand and support cookies too much work on the client's shoulders. I.E. we want to make our API as easy as possible so more people will want to use it.
  • A surprising number of early testers (friends who we asked to preview our site and services) where on browsers that did not support cookies for random URLs, usually because that was IT corporate policy at their work, but sometimes because well-meaning any-malware supported that policy.
  • We were moving do Django from a tomcat implementation, and missed the convenence of having jsession id inserted and used (almost) automatically whenever cookies were not supported.
So, for our project, we did the near-equivalent of tomcat's "jsessionid" for our web pages, and for our non-web clients expect them always to use jsessionid.

Our implementation of "jsessionid" Django version 0.96.2

Note: applies to Django version 0.96.2 -- I've no idea what other versions this works with -- I also have not idea whether this is the best way to accomplish what I wanted, but it's the way that works for me

I won't describe our utility functions for how we do the equivalent of tomcat's encodeURL, so that the sessionid is in the URL whenever cookies are not supported. That is a fairly trivial issue, and is bound to be different on each site. This article is about the primary problem, which is how to pass a string to the client to represent a session, how to get that string back from the client, and how to turn that string into a django session.

1) Create the session_key magic value

Once the authentication has happened, in standard django ways, we use this code to generate the "session_key" string


from django.contrib.sessions.models import Session
from django.contrib.sessions.middleware import SessionWrapper
...
...
...
request.session = SessionWrapper("")
auth.login(request, authuser)
obj = Session.objects.get_new_session_object()
session_key = obj.session_key
new_session = Session.objects.save(session_key, request.session._session,
datetime.datetime.utcnow() + datetime.timedelta(seconds=(10 * 60 * 60)))
# good for 10 hours


The session_key is then passed on to the client as the magic value to represent this session.

2) Create the session_key magic value

When the client contacts the server, and is not uing cookeis, it must then pass back the magic session_key value to the server as a parameter in its URL (or however else you want to pass it). Turning that magic string into a session so that all your other django code can use it normally is trivial:

request.session = SessionWrapper(sessionid)

what is less trivial is how to write data back to the session, in the same was as the cookie middleware would do it, so that changes to the session are preserved along with the magic cookie string. In other words, the following code must be applied before your server returns to the client or else all the changes it may have made to the cookies will be lost:

# sessions are written to work with browser cookies, but we don't want to work
# with browswer cookies - so fake it

# this code is copied from django.contrib.session.middleware - the difference is
# this code ignores cookies

# If request.session was modified, or if response.session was set, save
# those changes and set a session cookie.
try:
accessed = request.session.accessed
modified = request.session.modified
except AttributeError:
pass
else:
if modified:
print "MODIFIED!!!"
if request.session.session_key:
print "### USE OLD KEY ###"
session_key = request.session.session_key
else:
print "### CREATE NEW KEY ###"
obj = Session.objects.get_new_session_object()
session_key = obj.session_key

print "### AND SO ON ###"
new_session = Session.objects.save(session_key, request.session._session,
datetime.datetime.utcnow() + datetime.timedelta(seconds=(10 * 60 * 60)))
# good for 10 hours



Good luck

That's it. I hope it works well for you. It's been working well at YiqYaq so far. And if you have better ways to do this I hope you let me know.

No comments:

Post a Comment