Skip to content

下面给出两个最常用、最安全的实现示例(Django ≥ 3.2,SMTP 用 QQ/163/Gmail 均可)。
目录结构约定:

myproject/
├── myproject/
│   ├── settings.py
│   └── urls.py
├── users/               # 新建 app
│   ├── models.py
│   ├── serializers.py
│   ├── views.py
│   ├── urls.py
│   └── utils.py
└── manage.py

一、settings.py 里先把邮箱后端配好

python
# myproject/settings.py
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.qq.com'  # 或 smtp.163.com / smtp.gmail.com
EMAIL_PORT = 465  # SSL 端口
EMAIL_USE_SSL = True  # 465 用 SSL;587 用 TLS
EMAIL_HOST_USER = 'your_qq@qq.com'
EMAIL_HOST_PASSWORD = '授权码'  # 不是登录密码!
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER

# 验证码有效期(秒)
CODE_EXPIRE = 300

二、users/models.py —— 只存验证码(不存密码)

python
from django.db import models
from django.contrib.auth import get_user_model

User = get_user_model()


class EmailVerifyCode(models.Model):
    email = models.EmailField()
    code = models.CharField(max_length=6)
    created = models.DateTimeField(auto_now_add=True)

    class Meta:
        indexes = [models.Index(fields=['email', '-created'])]

三、users/utils.py —— 生成 6 位随机码 + 发送函数

python
import random
from django.core.mail import send_mail
from django.conf import settings
from django.utils import timezone
from datetime import timedelta
from .models import EmailVerifyCode


def generate_code():
    return ''.join(random.choices('0123456789', k=6))


def send_mail_code(email):
    code = generate_code()
    EmailVerifyCode.objects.create(email=email, code=code)
    subject = '登录验证码'
    message = f'您的验证码是:{code}{settings.CODE_EXPIRE // 60} 分钟内有效。'
    send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [email])

四、users/serializers.py

python
from rest_framework import serializers
from django.contrib.auth import get_user_model
from .models import EmailVerifyCode
from .utils import send_mail_code
from django.utils import timezone
from datetime import timedelta

User = get_user_model()


class SendCodeSerializer(serializers.Serializer):
    email = serializers.EmailField()

    def validate_email(self, value):
        # 可在此处限制注册或未注册邮箱
        return value

    def save(self):
        send_mail_code(self.validated_data['email'])


class LoginByCodeSerializer(serializers.Serializer):
    email = serializers.EmailField()
    code = serializers.CharField(min_length=6, max_length=6)

    def validate(self, attrs):
        email = attrs['email']
        code = attrs['code']
        qs = EmailVerifyCode.objects.filter(
            email=email, code=code,
            created__gte=timezone.now() - timedelta(seconds=settings.CODE_EXPIRE)
        )
        if not qs.exists():
            raise serializers.ValidationError('验证码错误或已过期')
        return attrs

    def save(self):
        email = self.validated_data['email']
        user, _ = User.objects.get_or_create(username=email, defaults={'email': email})
        # 清理用过的验证码
        EmailVerifyCode.objects.filter(email=email).delete()
        from rest_framework_simplejwt.tokens import RefreshToken
        refresh = RefreshToken.for_user(user)
        return {
            'refresh': str(refresh),
            'access': str(refresh.access_token),
        }


class CustomMailSerializer(serializers.Serializer):
    to = serializers.EmailField()
    subject = serializers.CharField(max_length=100)
    body = serializers.CharField()

五、users/views.py —— 两个 CBV

python
from rest_framework import generics, status
from rest_framework.response import Response
from .serializers import (
    SendCodeSerializer,
    LoginByCodeSerializer,
    CustomMailSerializer,
)
from django.core.mail import send_mail
from django.conf import settings


class SendCodeView(generics.GenericAPIView):
    serializer_class = SendCodeSerializer

    def post(self, request, *args, **kwargs):
        ser = self.get_serializer(data=request.data)
        ser.is_valid(raise_exception=True)
        ser.save()
        return Response({'detail': '验证码已发送,请查收'})


class LoginByCodeView(generics.GenericAPIView):
    serializer_class = LoginByCodeSerializer

    def post(self, request, *args, **kwargs):
        ser = self.get_serializer(data=request.data)
        ser.is_valid(raise_exception=True)
        tokens = ser.save()
        return Response(tokens)


class SendCustomMailView(generics.GenericAPIView):
    serializer_class = CustomMailSerializer

    def post(self, request, *args, **kwargs):
        ser = self.get_serializer(data=request.data)
        ser.is_valid(raise_exception=True)
        data = ser.validated_data
        send_mail(
            subject=data['subject'],
            message=data['body'],
            from_email=settings.DEFAULT_FROM_EMAIL,
            recipient_list=[data['to']],
            fail_silently=False,
        )
        return Response({'detail': '邮件已发送'})

六、users/urls.py

python
from django.urls import path
from .views import SendCodeView, LoginByCodeView, SendCustomMailView

urlpatterns = [
    path('send_code/', SendCodeView.as_view(), name='send_code'),
    path('login_by_code/', LoginByCodeView.as_view(), name='login_by_code'),
    path('send_mail/', SendCustomMailView.as_view(), name='send_custom_mail'),
]

七、myproject/urls.py 里 include

python
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('modules.users.urls')),
]

八、requirements.txt(最小集)

Django>=4.2
djangorestframework
djangorestframework-simplejwt

九、迁移 & 运行

bash
python manage.py makemigrations
python manage.py migrate
python manage.py runserver

十、接口测试(curl / Postman)

  1. 获取验证码
shell
curl -X POST http://127.0.0.1:8000/api/send_code/ \
     -H "Content-Type: application/json" \
     -d '{"email":"test@qq.com"}'
  1. 验证码登录
shell
curl -X POST http://127.0.0.1:8000/api/login_by_code/ \
     -H "Content-Type: application/json" \
     -d '{"email":"test@qq.com","code":"123456"}'

返回:{"refresh":"xxx","access":"yyy"}

  1. 任意自定义邮件
shell
curl -X POST http://127.0.0.1:8000/api/send_mail/ \
     -H "Content-Type: application/json" \
     -d '{
           "to": "friend@example.com",
           "subject": "生日快乐",
           "body": "祝你生日快乐!愿每天都开心~"
         }'

至此,两个需求全部完成,可直接并入正式项目。祝编码愉快!