Django Signing, max_age die Fallen und die Implementierung von Einmal-Token
django.core.signing ist praktisch. Wenn Sie jedoch nur auf max_age vertrauen und Token ausstellen, könnten Sie ernsthafte Sicherheitslücken haben.
🚫 max_age ist nicht "einmalig"
Das häufigste Missverständnis. TimestampSigner prüft nur die Gültigkeitsdauer.
loads(token, max_age=600)Dieses Token hat eine Gültigkeitsdauer von 10 Minuten. In diesen 10 Minuten kann
loads100 oder 1000 Mal erfolgreich abgeschlossen werden.Das Signing-Modul speichert keinen Zustand.
Was, wenn der Passwort-Zurücksetzungslink oder der E-Mail-Bestätigungslink mehrmals verwendet wird? Das ist schrecklich.
Die Funktion "einmalig verwendbar" müssen wir selbst implementieren. Hier stelle ich die beiden gängigsten Methoden vor: DB und Cache.
1. DB-Methode (sicherste Methode)
Wesentlich: Verwenden Sie ein DB-Flag (used), um den Status des Tokens zu verwalten.
Dies wird für sicherheitsrelevante Aktionen wie Passwortzurücksetzung, Kontoaktivierung und Zahlungsfreigabe dringend empfohlen.
1. Modellentwurf
Sie benötigen ein Modell, um den Status des Tokens zu speichern. Verwenden Sie UUID als eindeutigen Identifikator (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. Tokenerstellung
Erstellen Sie zuerst einen "vor der Verwendung"-Datensatz in der DB. Und signieren Sie dann den nonce-Wert dieses Datensatzes.
from django.core.signing import TimestampSigner
from .models import OneTimeToken
def create_one_time_link(user):
# 1. "Vor der Verwendung" Token-Datensatz erstellen
token_record = OneTimeToken.objects.create(user=user)
# 2. Eindeutigen Identifikator (nonce) signieren
signer = TimestampSigner()
signed_nonce = signer.dumps(str(token_record.nonce))
return f"https://example.com/verify?token={signed_nonce}"
3. Tokenverifizierung
Dieser Logik ist entscheidend.
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. Signatur + Ablaufzeit überprüfen
nonce_str = signer.loads(signed_nonce, max_age=max_age_seconds)
# 2. Nonce in der DB suchen
token_record = OneTimeToken.objects.get(nonce=nonce_str)
# 3. ★★★ Überprüfen, ob es bereits verwendet wurde ★★★
if token_record.used:
print("Das Token wurde bereits verwendet.")
return None
# 4. ★★★ Status auf "verwendet" ändern ★★★ (DB-Transaktionen empfohlen)
token_record.used = True
token_record.save()
# 5. Erfolg: Benutzer zurückgeben
return token_record.user
except SignatureExpired:
print("Das Token ist abgelaufen.")
return None
except (BadSignature, OneTimeToken.DoesNotExist):
print("Ungültiges Token.")
return None
- Vorteil: Sehr sicher und klar. Der Einsatz wird in der DB protokolliert und kann nachverfolgt werden.
- Nachteil: DB-I/O ist erforderlich. Alte Token-Datensätze müssen regelmäßig bereinigt werden.
2. Cache-Methode (schneller)
Wesentlich: Registrieren Sie verwendete Token als "Blacklist" im Cache (z. B. Redis).
Geeignet für "einmalige" Links oder zur Vermeidung von API-Duplikaten, wo Geschwindigkeit wichtig ist und das Sicherheitsrisiko relativ gering ist.
1. Tokenerstellung
Im Gegensatz zur DB-Methode gibt es keine separaten Schritte während der Erstellung. Sie signieren einfach die Daten.
from django.core.signing import TimestampSigner
signer = TimestampSigner()
token = signer.dumps({"user_id": 123})
2. Tokenverifizierung (Verwendung von Blacklist)
Überprüfen Sie nach einem erfolgreichen loads, ob "dieses Token jemals verwendet wurde" im Cache.
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. Signatur + Ablaufzeit überprüfen
data = signer.loads(token, max_age=max_age_seconds)
# 2. Cache-Schlüssel erstellen (Token kann lang sein, daher Hash)
token_hash = hashlib.sha256(token.encode()).hexdigest()
cache_key = f"used_token:{token_hash}"
# 3. ★★★ Überprüfen, ob es im Cache (Blacklist) ist ★★★
if cache.get(cache_key):
print("Das Token wurde bereits verwendet.")
return None
# 4. ★★★ Registrar in den Cache als "verwendet" ★★★
# Nur für die ursprüngliche max_age im Cache speichern
cache.set(cache_key, 'used', timeout=max_age_seconds)
# 5. Erfolg: Daten zurückgeben
return data
except SignatureExpired:
print("Das Token ist abgelaufen.")
return None
except BadSignature:
print("Ungültiges Token.")
return None
- Vorteil: Ist viel schneller als die DB. Dank der TTL des Caches werden abgelaufene Token automatisch bereinigt.
- Nachteil: Wenn der Cache (z. B. Redis) ausfällt oder Daten verloren gehen, können abgelaufene Token wiederverwendet werden. (Weniger stabil als DB)
Fazit
| Methode | Empfohlene Situationen | Vorteil | Nachteil |
|---|---|---|---|
| DB | Passwortzurücksetzung, Zahlungen, Kontoauthentifizierung | Sicherheit, permanente Aufzeichnungen | DB-Belastung, Bereinigungsaufwand erforderlich |
| Cache | "einmalige" Links, Vermeidung von API-Duplikaten | Schnelligkeit, automatische Bereinigung (TTL) | Wiederverwendungsrisiko bei Cache-Verlust |
django.core.signing prüft nur die Integrität und Ablauf. Die "Nutzung" ist die Verantwortung des Entwicklers. Wählen Sie die passende Methode für Ihre Situation.
Es sind keine Kommentare vorhanden.