You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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)
  • 逻辑在并发请求场景下仍需有效

我已尝试的方案:

  1. 应用层检查:创建新OTP前通过OTP.objects.filter(...)进行检查,但无法应对竞态条件,存在安全隐患
  2. 新增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"
            ),
        ]

关键说明

  1. 数据库兼容性:
    • PostgreSQL完全支持这种基于函数的部分唯一索引,是推荐的数据库选择
    • MySQL 8.0.16及以上版本支持检查约束中的函数调用,旧版MySQL无法生效
  2. 并发安全:约束直接在数据库层面生效,彻底避免应用层检查的竞态条件问题
  3. 零额外维护:无需手动或定时更新任何字段,数据库会自动根据当前时间判断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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.12 20:35:22