Django邮箱验证OTP无效问题排查及代码优化需求
问题排查(OTP无效错误)
核心原因:类型不匹配
你打印的verify.otp是整数,但serializer.data["otp"]大概率是字符串(前端传参或序列化器未做类型转换),直接用!=比较会返回True,触发无效提示。比如数据库存的是123456,而你拿到的otp是"123456",两者类型不同导致不相等。
其他潜在问题
- 时区不一致:邮件函数里用
datetime.now()生成时间,视图里用timezone.now(),如果项目开启了时区支持,两者会存在偏差,可能导致OTP被误判为过期或有效时间计算错误。 - 重复请求OTP报错:Verify模型的email字段设为
unique=True,用户重复请求验证邮件时,Verify.objects.create()会抛出唯一约束异常,导致新OTP无法存储,用户只能用旧的OTP。 - 变量名冲突:邮件函数里
otp先存随机数,后又赋值为Verify对象,后续代码如果用到这个变量会出问题,逻辑混淆。
代码优化方案
1. 修复OTP类型不匹配问题
修改VerifySerializerBase
确保序列化器将otp字段转为整数:
class VerifySerializerBase(serializers.Serializer): email = serializers.EmailField() otp = serializers.IntegerField(min_value=100000, max_value=999999)
视图中强制类型转换(备选)
如果序列化器无法修改,在视图里手动转换:
otp = int(serializer.data["otp"])
2. 优化Verify模型与邮件发送函数
模型调整
- 去掉
created_at的手动赋值,因为已经设置auto_now_add=True - 可选:如果需要允许用户重复请求OTP并覆盖旧记录,保留
unique_together约束;如果要保留历史验证记录,去掉该约束
class Verify(models.Model): email = models.EmailField(_("Email"), max_length=254) otp = IntegerRangeField(min_value=100000, max_value=999999) created_at = models.DateTimeField(_("created at"), auto_now_add=True) expires_at = models.DateTimeField() def __str__(self) -> str: return self.email class Meta: unique_together = ('email',)
邮件函数优化
- 修复变量名冲突
- 处理重复请求OTP的情况(用get_or_create更新)
- 统一使用时区时间
from django.utils import timezone def send_opt_email(email, firstname, lastname): otp_code = random.randint(100000, 999999) subject = "Email verification" message = "Email verification" html_message = loader.render_to_string( "email_verify.html", { "firstname": firstname, "lastname": lastname, "otp": otp_code, }, ) email_from = settings.EMAIL_HOST_USER send_mail( subject, message, email_from, [email], fail_silently=True, html_message=html_message, ) # 处理重复请求:更新现有记录或创建新记录 verify_obj, created = Verify.objects.get_or_create( email=email, defaults={ 'otp': otp_code, 'expires_at': timezone.now() + timedelta(minutes=30) } ) if not created: # 如果记录已存在,更新OTP和过期时间 verify_obj.otp = otp_code verify_obj.expires_at = timezone.now() + timedelta(minutes=30) verify_obj.save()
3. 重构Verify_Email视图(简化逻辑+异常处理)
from django.utils import timezone from rest_framework import status from rest_framework.response import Response from rest_framework.views import APIView class Verify_Email(APIView): """Verify registered emails""" def post(self, request): serializer = VerifySerializerBase(data=request.data) if not serializer.is_valid(): return Response( {"message": "Invalid input data", "errors": serializer.errors}, status=status.HTTP_400_BAD_REQUEST ) email = serializer.validated_data["email"] otp = serializer.validated_data["otp"] # 检查用户是否存在 try: user = User.objects.get(email=email) except User.DoesNotExist: return Response( {"message": "User was not found"}, status=status.HTTP_404_NOT_FOUND ) # 检查用户是否已激活 if user.is_active: return Response( {"message": "This email has already been verified"}, status=status.HTTP_400_BAD_REQUEST ) # 检查验证记录是否存在 try: verify = Verify.objects.get(email=email) except Verify.DoesNotExist: return Response( {"message": "No verification record found, please request OTP again"}, status=status.HTTP_404_NOT_FOUND ) # 验证OTP和过期时间 now = timezone.now() if verify.otp != otp: return Response( {"message": "This OTP is invalid"}, status=status.HTTP_400_BAD_REQUEST ) if verify.expires_at < now: verify.delete() return Response( {"message": "This OTP has expired, please request another one"}, status=status.HTTP_400_BAD_REQUEST ) # 验证通过,激活用户并删除验证记录 verify.delete() user.is_active = True user.save() return Response( {"message": "Email has been verified"}, status=status.HTTP_200_OK )
视图优化点
- 简化嵌套try-except结构,按逻辑顺序依次检查,代码更易读
- 直接使用
serializer.validated_data替代serializer.data,数据更安全 - 单独处理
User.DoesNotExist和Verify.DoesNotExist,返回明确提示 - 验证过期后自动删除无效记录
- 返回序列化错误信息,方便调试
内容的提问来源于stack exchange,提问作者rediet yosef
相关产品推荐
相关产品推荐

