When developing with Django, there are times when you need to send data to the client via URL parameters, hidden fields in forms, cookies, etc., and then retrieve it again. At this point, you might wonder, "Has this data been altered by the user in the meantime?" django.core.signing is a powerful tool designed specifically to address this issue.
This module provides Cryptographic Signing, not Encryption.
-
Encryption (X): Hides the contents of the data.
-
Signing (O): The contents of the data may be exposed, but it guarantees that the data has not been tampered with.
1. Core Usage: dumps() and loads()
The core of the signing module is dumps() and loads(). It converts Python objects to signed strings or verifies and restores signed strings back to objects.
dumps(): Convert an object to a signed string
dumps() takes JSON-serializable objects like dictionaries or lists and returns a URL-safe signed string.
from django.core.signing import dumps
# Data to be signed
user_data = {'user_id': 123, 'role': 'user'}
# Sign the data to create a string.
signed_data = dumps(user_data)
print(signed_data)
# Example Output: eyJ1c2VyX2lkIjoxMjMsInJvbGUiOiJ1c2VyIn0:1q51oH:F... (data:signature)
The output is in the form of [encoded data]:[signature]. The front part is the original data Base64 encoded, which means anyone can decode it to see the original. The back part is the signature (hash) created using Django's SECRET_KEY.
loads(): Restore a signed string to an object (verification)
loads() takes the string generated by dumps() and verifies the signature. If the signature is valid, it returns the original object; if not, it raises a BadSignature exception.
from django.core.signing import loads, BadSignature
try:
# Restore the signed data back to the original object (verification)
unsigned_data = loads(signed_data)
print(unsigned_data)
# Output: {'user_id': 123, 'role': 'user'}
except BadSignature:
print("The data has been tampered with or the signature is not valid.")
# What if even a single character in the data is changed?
tampered_data = signed_data.replace('user', 'admin')
try:
loads(tampered_data)
except BadSignature:
print("Tampered data raises BadSignature exception.")
Always wrap this in a try...except BadSignature block.
2. Core Feature: Where is the data stored?
The biggest feature of django.core.signing is its "Stateless" nature.
Strings generated by dumps() are not stored anywhere on the server's database or cache. All information (data and signature) is included within the signed string itself.
The server simply sends this string to the client (via URL, cookies, form fields, etc.), and later when the client submits this value again, it uses the SECRET_KEY to validate it on the spot. This ensures that no server storage space is used, making it very lightweight and efficient.
3. Setting Validity Duration: The Secret of max_age
"If the data is not stored on the server, how can I set a validity duration?"
The max_age option includes the timestamp (time information) as part of the data signature when calling dumps().
When calling loads(), if you pass the max_age argument (in seconds), it will check the built-in timestamp after verifying the signature.
-
Calculate the difference between the current time and the timestamp.
-
If this difference exceeds
max_age, it raises aSignatureExpiredexception even if the signature has not been tampered with.
from django.core.signing import dumps, loads, SignatureExpired, BadSignature
import time
# 1. Create a signature (the time at this moment is recorded)
signed_data = dumps({'user_id': 456})
# 2. Verify with a 10-second validity -> success
try:
data = loads(signed_data, max_age=10)
print(f"Verification successful: {data}")
except SignatureExpired:
print("The signature has expired.")
except BadSignature:
print("The signature is not valid.")
# 3. Wait for 5 seconds
time.sleep(5)
# 4. Verify with a 3-second validity (5 seconds have already passed) -> failure
try:
data = loads(signed_data, max_age=3)
print(f"Verification successful: {data}")
except SignatureExpired:
print("The signature has expired. (max_age=3)")
except BadSignature:
print("The signature is not valid.")
This also does not involve storing the expiration time on the server, but utilizes the timestamp included in the signature, which is a stateless method.
4. Important! Precautions and Tips
-
SECRET_KEY is crucial.
All of these signing mechanisms operate based on the SECRET_KEY in settings.py. If this key is leaked, anyone can create a valid signature, so it must never be exposed externally. (Do not upload it to Git!)
-
Keep in mind that this is not encryption.
The front part of the signed data (Base64) can be easily decoded by anyone to see the original content. Never directly put sensitive data such as passwords or personal information into dumps(). (E.g., user_id is fine, but user_password is not.)
-
Use salt to separate signatures.
When using signatures for different purposes, use the salt parameter. If the salt is different, the signature result will be completely different even for the same data.
# Use different salt based on purpose
pw_reset_token = dumps(user.pk, salt='password-reset')
unsubscribe_link = dumps(user.pk, salt='unsubscribe')
By doing this, you can prevent attacks that try to reuse a 'unsubscribe link' signature for 'password reset'.
5. Key Use Cases
-
Password Reset URL: Sign the user's ID and timestamp and send it via email (no need to store temporary tokens in the DB)
-
Email Verification Link: Email verification link when new users sign up
-
Secure
nextURL: Prevent malicious site alteration of the?next=/private/URL that will redirect after login -
Temporary Download Link: Generate a file access URL valid only for a specific time (
max_age) -
Multi-step Form (Form Wizard): Prevent data tampering when passing form data from the previous step to the next step
There are no comments.