Django signing, max_age de valkuil en het implementeren van eenmalige tokens
django.core.signing is handig. Maar als je uitsluitend vertrouwt op max_age bij het uitgeven van tokens, kan dat ernstige beveiligingsproblemen opleveren.
🚫 max_age is geen "eenmalig"
Een van de meest voorkomende misverstanden. TimestampSigner's max_age controleert alleen de geldigheidstijd.
loads(token, max_age=600)Dit token is 10 minuten geldig. Gedurende deze 10 minuten kan het 100 of 1000 keer succesvol worden geladen.
Dat komt omdat het signing-moduul geen status (state) opslaat.
Wat als de link voor het resetten van wachtwoorden of de e-mailbevestigingslink meerdere keren wordt gebruikt? Dat is verschrikkelijk.
De functie "eenmalig gebruik" moet door ons zelf worden geïmplementeerd. We presenteren de twee meest voorkomende methoden, DB en Cache.
1. DB-methode (de veiligste methode)
Kern: Beheer het gebruik van de token met een DB-vlag (used).
Sterk aanbevolen voor beveiligingsgevoelige acties zoals het resetten van wachtwoorden, het activeren van accounts en het goedkeuren van betalingen.
1. Modelontwerp
We hebben een model nodig om de status van de token op te slaan. We gebruiken een UUID als unieke identificator (Nonce).
# models.py
import uuid
from django.db import models
from django.conf import settings
class OneTimeToken(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
nonce = models.UUIDField(default=uuid.uuid4, unique=True, editable=False)
created_at = models.DateTimeField(auto_now_add=True)
used = models.BooleanField(default=False)
2. Token genereren
Maak eerst een "voor gebruik" record in de DB. Onderteken vervolgens de nonce waarde van dat record.
from django.core.signing import TimestampSigner
from .models import OneTimeToken
def create_one_time_link(user):
# 1. Genereer record voor "voor gebruik" token
token_record = OneTimeToken.objects.create(user=user)
# 2. Onderteken de unieke identificator (nonce)
signer = TimestampSigner()
signed_nonce = signer.dumps(str(token_record.nonce))
return f"https://example.com/verify?token={signed_nonce}"
3. Token validatie
Deze logica is cruciaal.
from django.core.signing import TimestampSigner, BadSignature, SignatureExpired
from .models import OneTimeToken
def verify_one_time_token(signed_nonce, max_age_seconds=1800): # 30 minuten
signer = TimestampSigner()
try:
# 1. Ondertekening + geldigheidstijd controleren
nonce_str = signer.loads(signed_nonce, max_age=max_age_seconds)
# 2. Zoek Nonce in de DB
token_record = OneTimeToken.objects.get(nonce=nonce_str)
# 3. ★★★ Controleer of het al is gebruikt ★★★
if token_record.used:
print("Dit token is al gebruikt.")
return None
# 4. ★★★ Wijzig de status naar "gebruikt" ★★★ (DB-transactie aanbevolen)
token_record.used = True
token_record.save()
# 5. Succes: retourneer gebruiker
return token_record.user
except SignatureExpired:
print("Token is verlopen.")
return None
except (BadSignature, OneTimeToken.DoesNotExist):
print("Ongeldig token.")
return None
- Voordelen: Zeer veilig en transparant. Het gebruiksgeschiedenis wordt in de DB opgeslagen, wat tracking mogelijk maakt.
- Nadelen: DB I/O vindt plaats. Verouderde tokenrecords moeten regelmatig worden opgeschoond.
2. Cache-methode (snellere methode)
Kern: Registreer gebruikte tokens in de cache (bijv. Redis) als "blacklist".
Geschikt voor "eenmalig bekijken" links of het voorkomen van dubbele API-aanroepen, waar snelheid belangrijk is en de beveiligingsrisico's relatief laag zijn.
1. Token genereren
In tegenstelling tot de DB-methode is er bij de creatie geen aparte handeling vereist. Gewoon de data ondertekenen.
from django.core.signing import TimestampSigner
signer = TimestampSigner()
token = signer.dumps({"user_id": 123})
2. Token validatie (gebruik van blacklist)
Na succesvolle loads, controleer in de cache of "dit token ooit is gebruikt".
from django.core.cache import cache
from django.core.signing import TimestampSigner, BadSignature, SignatureExpired
import hashlib
def verify_one_time_token_with_cache(token, max_age_seconds=1800):
signer = TimestampSigner()
try:
# 1. Ondertekening + geldigheidstijd controleren
data = signer.loads(token, max_age=max_age_seconds)
# 2. Maak cache sleutel aan (omdat de token lang kan zijn, hash)
token_hash = hashlib.sha256(token.encode()).hexdigest()
cache_key = f"used_token:{token_hash}"
# 3. ★★★ Controleer of het in de cache (blacklist) zit ★★★
if cache.get(cache_key):
print("Dit token is al gebruikt.")
return None
# 4. ★★★ Registreer in de cache als "gebruikt" ★★★
# Alleen voor de oorspronkelijke max_age van de token in de cache bewaren
cache.set(cache_key, 'used', timeout=max_age_seconds)
# 5. Succes: retourneer data
return data
except SignatureExpired:
print("Token is verlopen.")
return None
except BadSignature:
print("Ongeldig token.")
return None
- Voordelen: Veel sneller dan de DB. Door de TTL van de cache worden verlopen tokens automatisch opgeruimd.
- Nadelen: Als de cache (zoals Redis) uitvalt of gegevens verliest, kan een niet-verlopen token hergebruikt worden. (minder stabiliteit dan de DB)
Conclusie
| Methode | Aangeraden situatie | Voordelen | Nadelen |
|---|---|---|---|
| DB | Wachtwoord resetten, betalingen, account validatie | Veilig, permanente registratie | DB-belasting, schoonmaak nodig |
| Cache | "Eenmalig bekijken" links, voorkomen van dubbele API-aanroepen | Snel, automatische opruiming (TTL) | Risico op hergebruik bij cache verlies |
django.core.signing controleert alleen integriteit en vervaldatum. "Gebruik status" is aan de ontwikkelaar. Kies een verstandige methode die past bij de situatie.
댓글이 없습니다.