Django+PostgreSQL:如何基于动态过期条件实现OTP唯一性约束?
问题描述
我正在使用Django构建一个安全的OTP系统,对应的模型代码如下:
class OTP(models.Model): MAX_ATTEMPTS = 3 DEFAULT_EXPIRE_IN_MINUTES = 1 class Purposes(models.IntegerChoices): LOGIN = 1, "Login" REGISTER = 2, "Register" VERIFY_PHONE = 3, "Verify Phone" VERIFY_EMAIL = 4, "Verify Email" otp_hash = models.CharField(max_length=64) purpose = models.IntegerField(choices=Purposes) phone = PhoneNumberField(null=True, blank=True) email = models.EmailField(null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True) expires_at = models.DateTimeField() failed_attempts = models.PositiveSmallIntegerField(default=0) used = models.BooleanField(default=False) used_at = models.DateTimeField(null=True, blank=True)
需要强制执行以下规则:
- 若同一用途下已存在未过期(基于
expires_at字段)且未使用(used=False)的OTP,则用户无法获取新的OTP - 规则分两种情况:
- 若填写了phone,则约束
(phone, purpose) - 若填写了email,则约束
(email, purpose)
- 若填写了phone,则约束
- 逻辑在并发请求场景下仍需有效
我已尝试的方案:
- 应用层检查:创建新OTP前通过
OTP.objects.filter(...)进行检查,但无法应对竞态条件,存在安全隐患 - 新增
is_expired字段+条件唯一约束:
class Meta: constraints = [ models.UniqueConstraint( fields=["phone", "purpose"], condition=Q(is_expired=False), name="unique_active_phone_otp" ), models.UniqueConstraint( fields=["email", "purpose"], condition=Q(is_expired=False), name="unique_active_email_otp" ), ]
但该方案需要在expires_at < now()时手动或定期更新is_expired字段,增加运维复杂度且容易遗漏。
请问是否有更简洁的方法,能在数据库层面直接基于expires_at > now()这类时间动态条件强制执行该唯一性约束?
解决方案
可以利用Django的UniqueConstraint结合数据库函数直接实现动态时间条件的唯一约束,无需额外维护is_expired字段。核心思路是在约束的condition中直接对比expires_at与当前数据库时间,同时结合used=False的条件,分别针对phone和email设置约束。
实现代码
首先导入所需的工具类:
from django.db import models from django.db.models import Q from django.db.models.functions import Now
修改OTP模型的Meta类:
class OTP(models.Model): # 原有模型字段保持不变... class Meta: constraints = [ # 针对phone的动态唯一约束:未使用+未过期的phone+purpose组合唯一 models.UniqueConstraint( fields=["phone", "purpose"], condition=Q(used=False) & Q(expires_at__gt=Now()), name="unique_active_phone_otp" ), # 针对email的动态唯一约束:未使用+未过期的email+purpose组合唯一 models.UniqueConstraint( fields=["email", "purpose"], condition=Q(used=False) & Q(expires_at__gt=Now()), name="unique_active_email_otp" ), ]
关键说明
- 数据库兼容性:
- PostgreSQL完全支持这种基于函数的部分唯一索引,是推荐的数据库选择
- MySQL 8.0.16及以上版本支持检查约束中的函数调用,旧版MySQL无法生效
- 并发安全:约束直接在数据库层面生效,彻底避免应用层检查的竞态条件问题
- 零额外维护:无需手动或定时更新任何字段,数据库会自动根据当前时间判断OTP是否过期
异常处理
当用户尝试创建重复的有效OTP时,数据库会抛出IntegrityError,需在业务逻辑中捕获该异常并返回友好提示:
from django.db import IntegrityError def send_otp(request): # 业务逻辑处理... try: OTP.objects.create(...) except IntegrityError: return Response({"error": "已有未过期的OTP,请稍后再试"}, status=400)
内容的提问来源于stack exchange,提问作者Ertugrul
相关产品推荐
相关产品推荐

