Подпись Django, max_age ловушки и реализация одноразовых токенов
django.core.signing удобен. Но если вы полагаетесь только на max_age, вы можете столкнуться с серьезными проблемами безопасности.
🚫 max_age не является "одноразовым"
Это самое распространенное недоразумение. max_age в TimestampSigner лишь проверяет срок действия.
loads(token, max_age=600)Этот токен действителен в течение 10 минут. В течение этих 10 минут он может быть использован 100 или 1000 раз с успешным выполнением
loads.Модуль подписи не сохраняет состояние.
Что если ссылка на сброс пароля или проверку электронной почты будет использована несколько раз? Это ужасно.
Функцию "одноразового использования" нужно реализовать самостоятельно. Рассмотрим два наиболее распространенных метода: БД и Кэш.
1. Метод БД (самый безопасный способ)
Ключевое: статус использования токена управляется флагом в БД (used).
Настоятельно рекомендуется для таких критически важных процедур, как сброс пароля, активация аккаунта и подтверждение платежа.
1. Проектирование модели
Нужна модель для хранения состояния токена. В качестве уникального идентификатора (Nonce) используется UUID.
# 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) остается на совести разработчика. Выбирайте разумный метод в зависимости от ситуации.
Комментариев нет.