Sessions in Django
Last updated on July 27, 2020
In the last chapter (Cookies in Django), we have learned how cookies allow us to store data in browser easily. Although no doubt cookies are useful, they have following problems.
- An attacker can modify contents of a cookie that could potentially break your application
- We can't store sensitive data.
- We can only store a limited amount of data in cookies. Most browsers don't allow a cookie to store more than 4KB of data. Breaking data into multiple cookies causes too much overhead in each request. Further, you can't even rely on a number of cookies allowed by the browser for each domain.
We can overcome these problems easily using Sessions. This is how session work:
When we use sessions the data is not stored directly in the browser instead it is stored in the server. Django creates a unique random string called session id or SID and associates SID with the data. The server then sends a cookie named sessionid containing SID as value to the browser. On requesting a page, the browser sends the request along with the cookie with SID to the server. Django then uses this SID to retrieve session data and makes it accessible in your code. SID generated by Django is a 32 characters long random string, so it is almost impossible to guess by an attacker.
Setting Up Sessions #
In Django, sessions are implemented using middleware. Open settings.py
file and locate MIDDLEWARE
list variable. If it has 'django.contrib.sessions.middleware.SessionMiddleware'
as one of the elements then you are good to go. If you can't see it, just add it now to the MIDDLEWARE
list. At this point, the MIDDLEWARE
list should look like this:
TGDB/django_project/django_project/settings.py
1 2 3 4 5 6 7 8 9 10 11 12 | #...
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.common.BrokenLinkEmailsMiddleware',
]
#...
|
The 'django.contrib.sessions.middleware.SessionMiddleware'
is responsible for generating unique SID.
Django provides an app called 'django.contrib.sessions'
whose role is to store the session data into the database. So make sure you have 'django.contrib.sessions'
in the INSTALLED_APPS
list. If for some reason you don't have 'django.contrib.sessions'
in INSTALLED_APPS
, add it right now and run python manage.py migrate
command. This command will create the necessary table to store the session data.
TGDB/django_project/django_project/settings.py
1 2 3 4 5 6 7 8 9 10 11 12 | #...
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'blog',
'cadmin',
]
#...
|
Testing Sessions #
We have already discussed that users can configure their browsers to not accept any cookie. As a result, Django provides some convenience methods to check cookies support in the browser. The request.sessions
object provides the following three methods to check cookies support in the browser.
Method | What it does? |
---|---|
set_test_cookie() |
sets the cookie in the browser |
test_cookie_worked() |
returns True if browser accepted the cookie(a browser accepted the cookie means, it has sent the cookie to the server in the next request), Otherwise False |
delete_test_cookie() |
delete the test cookie |
Let's try them!
Open blog app's views.py
file and add following two views at the end of the file.
TGDB/django_project/blog/views.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | #...
def stop_tracking(request):
#...
def test_session(request):
request.session.set_test_cookie()
return HttpResponse("Testing session cookie")
def test_delete(request):
if request.session.test_cookie_worked():
request.session.delete_test_cookie()
response = HttpResponse("Cookie test passed")
else:
response = HttpResponse("Cookie test failed")
return response
|
The test_session()
view sets the cookie and test_delete()
checks whether the browser accepted the cookie or not.
Add the following two URL patterns at the beginning of urlpatterns
list in blog's urls.py
.
TGDB/django_project/blog/urls.py
1 2 3 4 5 6 7 | #...
urlpatterns = [
url(r'^test-delete/$', views.test_delete, name='test_delete'),
url(r'^test-session/$', views.test_session, name='test_session'),
url(r'^stop-tracking/$', views.stop_tracking, name='stop_tracking'),
#...
]
|
Point your browser to http://127.0.0.1:8000/test-session/
and you should see a page like this:
Open Developer Tools hitting Ctrl+Shift+J and visit Application tab again. If your browser accepted the cookie it should look like this:
Now visit http://127.0.0.1:8000/test-delete/
, if everything went fine, you would see "Cookie test passed" response.
On the other hand, if you get "Cookie test failed" message. Check the browser settings and allow websites to save cookies.
Reading and Writing Session Data #
To read and write session data we use session
attribute of the request
object. The session
attribute acts like a dictionary.
Here is how we can save, read, and delete session data.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | # set session data
request.session['name'] = 'tom'
request.session['age'] = '18'
# read session data
request.session.get('name') # returns 'tom'
request.session.get('age') # returns '18'
## delete session data
del request.session['name']
del request.session['age']
|
Open blog app's views.py
and add the following three views at the end of the file.
TGDB/django_project/blog/views.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | #...
def test_delete(request):
#...
def save_session_data(request):
# set new data
request.session['id'] = 1
request.session['name'] = 'root'
request.session['password'] = 'rootpass'
return HttpResponse("Session Data Saved")
def access_session_data(request):
response = ""
if request.session.get('id'):
response += "Id : {0} <br>".format(request.session.get('id'))
if request.session.get('name'):
response += "Name : {0} <br>".format(request.session.get('name'))
if request.session.get('password'):
response += "Password : {0} <br>".format(request.session.get('password'))
if not response:
return HttpResponse("No session data")
else:
return HttpResponse(response)
def delete_session_data(request):
try:
del request.session['id']
del request.session['name']
del request.session['password']
except KeyError:
pass
return HttpResponse("Session Data cleared")
|
here is what happen when save_session_data()
view is called.
'django.contrib.sessions.middleware.SessionMiddleware'
middleware creates a new random session id or SID and associates the session data with it.'django.contrib.sessions.middleware.SessionMiddleware'
uses'django.contrib.sessions'
app to store the session data in the database- At last, a cookie named
sessionid
with a random value(i.eSID
) generated in step 1, is sent to the browser. - From now on, the browser would send this
sessionid
cookie with every request to the server, allowing Python code to access session data in views usingrequest.session
Before we test this code add the following URL pattern to the blog's urls.py
file:
TGDB/django_project/blog/urls.py
1 2 3 4 5 6 7 8 | #...
urlpatterns = [
url(r'^save-session-data/$', views.save_session_data, name='save_session_data'),
url(r'^access-session-data/$', views.access_session_data, name='access_session_data'),
url(r'^delete-session-data/$', views.delete_session_data, name='delete_session_data'),
url(r'^test-delete/$', views.test_delete, name='test_delete'),
#...
]
|
Visit http://127.0.0.1:8000/save-session-data/
and you should get the following output.
At this point, the browser should have a cookie named sessionid
with a random SID.
To access session data visit http://127.0.0.1:8000/access-session-data/
and you should get the following output:
To delete the session data visit http://127.0.0.1:8000/delete-session-data/
.
Visit http://127.0.0.1:8000/access-session-data/
again and you should see "No session data".
It is important to note that deleting session data from the database doesn't delete the session cookie in the browser. As a result, if you visit http://127.0.0.1:8000/save-session-data/
, Django will reuse the SID sent by the browser to associate the session data with it again.
Modifying Session Data #
Here comes one of the most important thing you need to remember while using sessions:
Django saves the session data into the database only when it is modified.
By modified, Django means following actions:
Adding new key-value pair to
request.session
dictionary.1 2 3
>>> item_list = { 'eggs': '10', 'apples': '20' , 'mango': '30' } >>> request.session['newkey'] = 'newval' >>> request.session['items_to_buy'] = item_list
Changing the value of an existing key.
>>> request.session['newkey'] = 'newval100'
Deleting the a key-value pair.
>>> del request.session['newkey']
Unfortunately, the following is not a modification.
>>> request.session['items_to_buy']['eggs'] = '100'
But why ? Here we are not modifying
request.session
dictionary instead we are modifyingrequest.session['items_to_buy']
.If you want Django to save these changes to the database, you have to set
modified
attribute ofsession
object toTrue
.request.session.modified = True
To change this default behavior permanently, set
SESSION_SAVE_EVERY_REQUEST
toTrue
insettings.py
. WhenSESSION_SAVE_EVERY_REQUEST
is set toTrue
Django will save session data to the database on every request, even if you don't modify session data at all.Another important point worth mentioning here is that Django sends the session cookie to the browser only when session data is modified, in the process also updates the cookie expiry time.
If
SESSION_SAVE_EVERY_REQUEST
is set toTrue
, Django will send the session cookie on every request.
Sessions API #
Django provides an API which allows you to access session data outside of views.
1 2 3 4 5 6 7 8 | >>>
>>> from django.contrib.sessions.models import Session
>>>
>>> s = Session.objects.get(pk='t2m5w8ng9x55fa2a78b8s680dc1ubxpl')
>>> s
>>>
<Session: t2m5w8ng9x55fa2a78b8s680dc1ubxpl>
>>>
|
The long string passed to the get()
method is SID
. You can copy it either from the browser or session_key
column from the django_session
table. By default, session data is stored in the django_session
table. It consists of three columns as follows:
Column | Explanation |
---|---|
session_key |
To store unique random session id or SID |
session_data |
Django stores the session data in the encoded format. To get the data the raw data use get_decoded() method of the session object |
expire_date |
expiration date of the session cookie |
Once you have access to session object you can use it to query for other information.
1 2 3 4 5 | >>>
>>> s.expire_date
datetime.datetime(2017, 5, 1, 8, 51, 11, 922337, tzinfo=<UTC>)
>>>
>>>
|
1 2 3 4 5 | >>>
>>> s.session_data # get the session data in encoded format
'MzM0MjViODhhNTU3Yjg5NjA2YjVmODdiNjJkOGIxYTdkNzIxNmVkYzp7InBhc3N3b3JkIjoicm9vdHB
hc3MifQ=='
>>>
|
1 2 3 4 | >>>
>>> s.get_decoded() # get decoded session data
{}
>>>
|
As you can see, get_decoded()
method returns an empty dictionary because in my case there is no data associated with the given SID. Had there been data associated with the SID the get_decoded()
would have returned a dictionary like this:
1 2 3 4 | >>>
>>> s.get_decoded()
{'password': 'rootpass', 'id': 1, 'name': 'root'}
>>>
|
Setting Expiration Time for Session Cookie #
We can control session cookies expiration time by setting the following two variables in settings.py
file:
Variable Name | Explanation |
---|---|
SESSION_COOKIE_AGE |
This variable is used to set cookie expiration time in seconds. By default, it is set to 1209600 seconds or 2 weeks. If SESSION_EXPIRE_AT_BROWSER_CLOSE is not set then Django uses this variable to set cookie expiration time. Here is how you can set session cookie expiration time to 5 days: SESSION_COOKIE_AGE = 3600*24*5 |
SESSION_EXPIRE_AT_BROWSER_CLOSE |
This variable controls whether to expire the session cookie when the user closes the browser. By default, it is set to False . If set to True , session cookie lasts until the browser is closed, irrespective of the value of SESSION_COOKIE_AGE . |
Clearing Sessions #
Session data accumulates pretty fast especially if your site relies on sessions for different functionalities. It is a good idea to search for expired sessions regularly clean them before they start cluttering up your tables. Fortunately, Django provides clearsessions
command to clear expired sessions from the tables.
(env) C:\Users\Q\TGDB\django_project>python manage.py clearsessions
Let's conclude this chapter by creating a small project - The lousy login form.
The Lousy Login #
Sessions are commonly used to create login systems. The motive of this section is to show you how login systems are implemented using sessions.
Open blog's views.py
and add lousy_login()
, lousy_secret()
and lousy_logout()
views to the end of the file.
TGDB/django_project/blog/views.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | #...
from django_project import helpers
from django.contrib import messages
#...
def delete_session_data(request):
#...
def lousy_login(request):
if request.method == 'POST':
username = request.POST.get('username')
password = request.POST.get('password')
if username == "root" and password == "pass":
request.session['logged_in'] = True
return redirect('lousy_secret')
else:
messages.error(request, 'Error wrong username/password')
return render(request, 'blog/lousy_login.html')
def lousy_secret(request):
if not request.session.get('logged_in'):
return redirect('lousy_login')
return render(request, 'blog/lousy_secret_page.html')
def lousy_logout(request):
try:
del request.session['logged_in']
except KeyError:
return redirect('lousy_login')
return render(request, 'blog/lousy_logout.html')
|
Next create three new templates lousy_login.html
, lousy_secret_page.html
and lousy_logout.html
with the following content.
TGDB/django_project/blog/templates/blog/lousy_login.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | {% extends "blog/base.html" %}
{% block title %}
Lousy Login - {{ block.super }}
{% endblock %}
{% block content %}
<div class="content">
<div class="section-inner clearfix">
<h3>Login Form</h3>
{% if messages %}
<ul class="messages">
{% for message in messages %}
<li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
<form action="" method="post">
{% csrf_token %}
<table>
<tr>
<td><label for="id_username">Enter Username</label></td>
<td><input type="text" id="id_username" name="username"></td>
</tr>
<tr>
<td><label for="id_password">Enter Password</label></td>
<td><input type="text" id="id_password" name="password"></td>
</tr>
<tr>
<td></td>
<td><input type="submit" name="submit" value="Submit"></td>
</tr>
</table>
</form>
</div>
</div>
{% endblock %}
|
TGDB/django_project/blog/templates/blog/lousy_secret_page.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | {% extends "blog/base.html" %}
{% block title %}
Lousy Secret Page - {{ block.super }}
{% endblock %}
{% block content %}
<div class="content">
<div class="section-inner clearfix">
{% if request.session.logged_in %}
<p>Welcome to the lousy secret page. You are seeing this page because you are logged in.</p>
<p>To logout click <a href="{% url 'lousy_logout' %}">here</a></p>
{% endif %}
</div>
</div>
{% endblock %}
|
TGDB/django_project/blog/templates/blog/lousy_logout.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | {% extends "blog/base.html" %}
{% block title %}
Blog - {{ block.super }}
{% endblock %}
{% block content %}
<div class="content">
<div class="section-inner clearfix">
<p>You have been logged out click <a href="{% url 'lousy_login' %}">here</a> to login again</p>
</div>
</div>
{% endblock %}
|
Finally, add the following three URL patterns at the beginning of urlpatterns
list in blog's urls.py
.
TGDB/django_project/blog/urls.py
1 2 3 4 5 6 7 8 | #...
urlpatterns = [
url(r'^lousy-login/$', views.lousy_login, name='lousy_login'),
url(r'^lousy-secret/$', views.lousy_secret, name='lousy_secret'),
url(r'^lousy-logout/$', views.lousy_logout, name='lousy_logout'),
url(r'^save-session-data/$', views.save_session_data, name='save_session_data'),
#...
]
|
Open the browser and visit http://127.0.0.1:8000/lousy-secret/
, you will be redirected to http://127.0.0.1:8000/lousy-login/
because currently we are not authorized to view this page.
Type wrong a username/password combination or just hit "Submit" without entering anything and you would get "Error wrong username/password" error above the form as follows:
Enter correct username/password (i.e root and pass) and hit submit, this time you will be redirected to http://127.0.0.1:8000/lousy-secret/
.
Click the logout link to logout from the page and this time you will be redirected to http://127.0.0.1:8000/lousy-logout/
.
Obviously, this is a lousy way of logging in users. Nonetheless, it perfectly describes the role of sessions in creating login systems. Django has an authentication framework which makes logging in user into your app pretty easily. Django authentication framework is discussed in the next chapter.
Note: To checkout this version of the repository type git checkout 28a
.
Load Comments