如何在Django中通过发送找回验证码而非重置链接实现密码重置
Django基于找回验证码(Recovery Codes)实现无链接密码重置方案
1. 前置准备:扩展数据模型
首先需要新增存储重置验证码的模型,不存储明文验证码,避免数据库泄露风险,同时设置验证码有效期限制:
from django.db import models from django.contrib.auth.models import User from django.utils import timezone import hashlib import random class PasswordResetCode(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="reset_code") code_hash = models.CharField(max_length=64) expires_at = models.DateTimeField() is_used = models.BooleanField(default=False) # 生成6位数字验证码,可根据需求调整为字母数字组合 @classmethod def generate_code(cls, user, valid_minutes=15): # 清理该用户之前未使用的旧验证码 cls.objects.filter(user=user, is_used=False).update(is_used=True) raw_code = f"{random.randint(100000, 999999)}" # 哈希存储验证码,可自定义加盐提高安全性 code_hash = hashlib.sha256(raw_code.encode()).hexdigest() expires_at = timezone.now() + timezone.timedelta(minutes=valid_minutes) cls.objects.create(user=user, code_hash=code_hash, expires_at=expires_at) # 返回明文验证码用于发送给用户 return raw_code
2. 验证码申请接口实现
用户提交绑定的邮箱/手机号后,后台校验账号存在性,生成验证码直接发送,不生成任何重置链接:
from django.http import JsonResponse from django.core.mail import send_mail from .models import PasswordResetCode def request_reset_code(request): if request.method != "POST": return JsonResponse({"error": "仅支持POST请求"}, status=405) email = request.POST.get("email") try: user = User.objects.get(email=email) except User.DoesNotExist: # 返回统一提示避免账号枚举漏洞 return JsonResponse({"status": "success", "msg": "如果账号存在,验证码已发送到你的邮箱"}) raw_code = PasswordResetCode.generate_code(user) # 发送仅包含验证码的通知,无跳转链接 send_mail( subject="你的账号密码重置验证码", message=f"你的密码重置验证码是:{raw_code},15分钟内有效,请勿泄露给他人。", from_email="系统配置的发件邮箱", recipient_list=[email], fail_silently=False, ) return JsonResponse({"status": "success", "msg": "如果账号存在,验证码已发送到你的邮箱"})
3. 验证码校验+密码重置接口实现
用户在当前页面输入验证码和新密码,后台一次性完成校验和密码更新,全程无页面跳转:
from django.contrib.auth.hashers import make_password from .models import PasswordResetCode import hashlib import timezone def reset_password_with_code(request): if request.method != "POST": return JsonResponse({"error": "仅支持POST请求"}, status=405) email = request.POST.get("email") raw_code = request.POST.get("code") new_password = request.POST.get("new_password") # 校验参数完整性 if not all([email, raw_code, new_password]): return JsonResponse({"error": "缺少必填参数"}, status=400) try: user = User.objects.get(email=email) except User.DoesNotExist: return JsonResponse({"error": "账号不存在或验证码无效"}, status=400) # 校验验证码有效性 code_hash = hashlib.sha256(raw_code.encode()).hexdigest() reset_code = PasswordResetCode.objects.filter( user=user, code_hash=code_hash, is_used=False, expires_at__gt=timezone.now() ).first() if not reset_code: return JsonResponse({"error": "验证码无效或已过期"}, status=400) # 标记验证码已使用,防止重复利用 reset_code.is_used = True reset_code.save() # 更新用户密码 user.password = make_password(new_password) user.save() # 可选操作:清空该用户所有已登录会话,避免账号风险 user.auth_token_set.all().delete() return JsonResponse({"status": "success", "msg": "密码重置成功,请使用新密码登录"})
4. 安全性优化建议
- 限制同一账号验证码发送频率,比如1分钟内仅能发送1次,1小时内最多发送5次,防止接口被恶意刷取
- 重置环节保留密码复杂度校验,避免用户设置弱密码
- 验证码长度不低于6位,有效期不超过30分钟,降低暴力破解风险
- 增加验证码错误次数限制,同一账号连续输错5次验证码后锁定重置请求1小时
内容的提问来源于stack exchange,提问作者pear rose
相关产品推荐
相关产品推荐

