Django簽名,max_age的陷阱與一次性令牌的實現



django.core.signing非常方便。但如果僅依賴max_age而發放令牌,可能會存在嚴重的安全漏洞。

🚫 max_age並非"一次性"的

這是最常見的誤解。TimestampSignermax_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)"則由開發者自行管理。根據需要選擇合適的方法。