Django簽名,max_age的陷阱與一次性令牌的實現
django.core.signing非常方便。但如果僅依賴max_age而發放令牌,可能會存在嚴重的安全漏洞。
🚫 max_age並非"一次性"的
這是最常見的誤解。TimestampSigner的max_age僅用於驗證有效期。
loads(token, max_age=600)這個令牌是10分鐘的。在這10分鐘內,不論多少次都能成功加載100次或1000次。
因為簽名模組不會保存狀態(state)。
如果密碼重設鏈接或電子郵件認證鏈接被多次使用,那將是可怕的事情。
"一次性使用(one-time-use)"功能需要我們自己來實現。這裡介紹兩種最常見的方法:數據庫(DB)和緩存(Cache)。
1. 數據庫方式(最安全的方法)
核心:使用數據庫標誌(used)來管理令牌的使用情況。
強烈建議用於密碼重設、帳戶啟用、付款確認等安全性至關重要的情況。
1. 模型設計
需要一個存儲令牌狀態的模型。使用UUID作為唯一標識符(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. 生成令牌
首先在數據庫中創建"使用前"的記錄。然後簽名該記錄的nonce值。
from django.core.signing import TimestampSigner
from .models import OneTimeToken
def create_one_time_link(user):
# 1. 創建"使用前"的令牌記錄
token_record = OneTimeToken.objects.create(user=user)
# 2. 簽名唯一標識符(nonce)
signer = TimestampSigner()
signed_nonce = signer.dumps(str(token_record.nonce))
return f"https://example.com/verify?token={signed_nonce}"
3. 驗證令牌
這個邏輯是關鍵。
from django.core.signing import TimestampSigner, BadSignature, SignatureExpired
from .models import OneTimeToken
def verify_one_time_token(signed_nonce, max_age_seconds=1800): # 30分鐘
signer = TimestampSigner()
try:
# 1. 驗證簽名 + 驗證有效期
nonce_str = signer.loads(signed_nonce, max_age=max_age_seconds)
# 2. 在數據庫中查找Nonce
token_record = OneTimeToken.objects.get(nonce=nonce_str)
# 3. ★★★ 檢查是否已經使用 ★★★
if token_record.used:
print("已使用的令牌。")
return None
# 4. ★★★ 改變狀態為"已使用" ★★★(建議使用數據庫事務)
token_record.used = True
token_record.save()
# 5. 成功:返回用戶
return token_record.user
except SignatureExpired:
print("令牌已過期。")
return None
except (BadSignature, OneTimeToken.DoesNotExist):
print("無效的令牌。")
return None
- 優點:非常安全且明確。使用記錄保留在數據庫中,可追蹤性強。
- 缺點:會產生數據庫I/O。需要定期清理舊的令牌記錄。
2. 緩存方式(更快的方法)
核心:將已使用的令牌註冊為緩存(例如:Redis)的"黑名單"。
適合需要速度且安全風險相對較低的情況,如"一次性查看"鏈接或API重複調用的防止。
1. 生成令牌
與數據庫方式不同,生成時無需額外操作。只需簽名數據。
from django.core.signing import TimestampSigner
signer = TimestampSigner()
token = signer.dumps({"user_id": 123})
2. 驗證令牌(利用黑名單)
在loads成功後,檢查緩存中"這個令牌是否已被使用過"。
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. 驗證簽名 + 驗證有效期
data = signer.loads(token, max_age=max_age_seconds)
# 2. 創建緩存鍵(因為令牌可能較長,使用哈希)
token_hash = hashlib.sha256(token.encode()).hexdigest()
cache_key = f"used_token:{token_hash}"
# 3. ★★★ 檢查是否在緩存(黑名單)中 ★★★
if cache.get(cache_key):
print("已使用的令牌。")
return None
# 4. ★★★ 標記為"已使用"並註冊到緩存 ★★★
# 令牌的原始max_age時間內僅存儲在緩存中
cache.set(cache_key, 'used', timeout=max_age_seconds)
# 5. 成功:返回數據
return data
except SignatureExpired:
print("令牌已過期。")
return None
except BadSignature:
print("無效的令牌。")
return None
- 優點:比數據庫快得多。由於緩存的TTL,過期的令牌會自動清理。
- 缺點:若緩存(如Redis等)崩潰或數據丟失,未過期的令牌可能會被重複使用。(穩定性低於數據庫)
結論
| 方式 | 推薦情況 | 優點 | 缺點 |
|---|---|---|---|
| 數據庫 | 密碼重設、支付、帳戶驗證 | 安全, 永久記錄 | 數據庫負載、需要清理工作 |
| 緩存 | "一次性查看"鏈接、API重複防止 | 快速, 自動清理(TTL) | 緩存丟失時的重複風險 |
django.core.signing僅驗證完整性(tampering)和過期(expiration)。"使用狀態(state)"則由開發者自行管理。根據需要選擇合適的方法。
目前沒有評論。