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

Django中EmailVerificationTokenGenerator.make_token()参数缺失问题求助

Django邮件验证链接生成报错:TypeError: make_token()缺少必需参数'contact_form'

问题详情

我正在搭建网站邮件服务器,实现联系表单提交后发送邮件验证链接,响应中返回重发验证链接的功能。此前功能正常,但调整后出现以下错误:

TypeError at /api/contact/
EmailVerificationTokenGenerator.make_token() missing 1 required positional argument: 'contact_form'

相关代码文件

views.py

from django.contrib.sites.shortcuts import get_current_site
from django.core.mail import EmailMessage
from django.template.loader import render_to_string
from django.utils.encoding import force_bytes, force_str
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
from rest_framework.views import APIView
from rest_framework.response import Response
from .serializers import ContactFormSerializer
from .verification import EmailVerificationTokenGenerator
from .models import ContactForm 
from email_server.permissions import CanResendVerificationLink
from rest_framework import status
from django.urls import reverse
import secrets
from django.shortcuts import get_object_or_404


# Base Mixin
class EmailVerificationMixin:
    def _send_email_verification(self, contact_form):
        subject = 'Verify Your Email'
        uidb64 = urlsafe_base64_encode(force_bytes(contact_form.pk))
        verification_token = EmailVerificationTokenGenerator().make_token(contact_form)
        verification_link = self._get_verification_link(contact_form, uidb64, verification_token)
        body = render_to_string(
            'email_verification.html',
            {
                'verification_link': verification_link,
            }
        )
        email = EmailMessage(subject=subject, body=body, to=[contact_form.email])
        email.send()

    def _get_verification_link(self, contact_form, uidb64, token):
        current_site = get_current_site(self.request)
        verification_url = reverse('activate', kwargs={'uidb64': uidb64, 'token': token})
        verification_link = f'{self.request.scheme}://{current_site.domain}{verification_url}'
        return verification_link

    def _get_resend_verification_link(self, contact_form):
        current_site = get_current_site(self.request)
        resend_token = secrets.token_urlsafe(32)  # Generate a new resend token
        resend_verification_url = reverse('resend_verification', kwargs={'pk': contact_form.pk, 'token': resend_token})
        resend_verification_link = f'{self.request.scheme}://{current_site.domain}{resend_verification_url}'
        return resend_verification_link


# API Views

class ContactFormAPIView(EmailVerificationMixin, APIView):
    def post(self, request):
        serializer = ContactFormSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        contact_form = serializer.save()

        self._send_email_verification(contact_form)

        response_data = {
            'detail': 'Email verification link sent.',
            'resend_verification_link': self._get_resend_verification_link(contact_form),
        }

        return Response(response_data, status=status.HTTP_201_CREATED)

    
class ResendVerificationLinkAPIView(EmailVerificationMixin, APIView):
    permission_classes = [CanResendVerificationLink]
    
    def post(self, request, pk, token):
        contact_form = get_object_or_404(ContactForm, pk=pk)
        
        if contact_form.is_email_verified:
            return Response({'detail': 'Email is already verified.'}, status=status.HTTP_400_BAD_REQUEST)

        if not self._is_valid_resend_token(contact_form, token):
            return Response({'detail': 'Invalid token.'}, status=status.HTTP_400_BAD_REQUEST)

        self._send_email_verification(contact_form)

        response_data = {
            'detail': 'Email verification link has been re-sent. Please check your email.',
            'resend_verification_link': self._get_resend_verification_link(contact_form),
        }

        return Response(response_data, status=status.HTTP_200_OK)
    
    def _is_valid_resend_token(self, contact_form, token):
        return token == contact_form.resend_token

verification.py

from django.contrib.auth.tokens import PasswordResetTokenGenerator
from django.utils.crypto import constant_time_compare
from django.utils import timezone
from datetime import timedelta


class EmailVerificationTokenGenerator(PasswordResetTokenGenerator):
    token_duration = timedelta(minutes=30)

    def _make_hash_value(self, contact_form, timestamp):
        return (
            str(contact_form.is_email_verified) +
            str(contact_form.pk) +
            str(timestamp)
        )

    def make_token(self, contact_form):
        timestamp = int(timezone.now().timestamp())
        hash_value = self._make_hash_value(contact_form, timestamp)
        return self._make_token_with_timestamp(hash_value, timestamp)


    def check_token(self, contact_form, token):
        if not (contact_form and token):
            return False

        try:
            timestamp = int(token.split('-')[-1])
        except (ValueError, TypeError):
            return False

        if self._is_token_expired(timestamp):
            return False

        expected_token = self._make_token_with_timestamp(self._make_hash_value(contact_form, timestamp), timestamp)
        return constant_time_compare(token, expected_token)

urls.py

from django.urls import path, include
from .views import SubmittedFormsViewSet
from email_server.views import ContactFormAPIView, ActivateAPIView, ResendVerificationLinkAPIView
from rest_framework import routers

router = routers.DefaultRouter()
router.register(r'list', SubmittedFormsViewSet, basename='contact_form')

urlpatterns = [
    path('', include(router.urls)),
    path('contact/', ContactFormAPIView.as_view(), name='contact_form'),
    path('resend-verification/<slug:token>/', ResendVerificationLinkAPIView.as_view(), name='resend_verification'),
    path('activate/<str:uidb64>/<slug:token>/', ActivateAPIView.as_view(), name='activate'),
]

models.py

from django.db import models
from django.utils import timezone
from .verification import EmailVerificationTokenGenerator
import secrets

class ContactForm(models.Model):
    name = models.CharField(max_length=100)
    email = models.EmailField(max_length=100)
    message = models.TextField(max_length=500, blank=True, null=True)
    submitted_at = models.DateTimeField(default=timezone.now)
    is_validated = models.BooleanField(default=False)
    is_email_verified = models.BooleanField(default=False)
    verification_token = models.UUIDField(default=EmailVerificationTokenGenerator().make_token, editable=False)
    resend_token = models.UUIDField(default=secrets.token_urlsafe(32), editable=False)

    def __str__(self):
        return self.email

    class Meta:
        verbose_name = 'Contact Form'
        verbose_name_plural = 'Contact Forms'
        indexes = [
            models.Index(fields=['email']),
            models.Index(fields=['submitted_at']),
            models.Index(fields=['is_email_verified']),
        ]

serializers.py

from rest_framework import serializers
from .models import ContactForm


class ContactFormSerializer(serializers.ModelSerializer):
    class Meta:
        model = ContactForm
        fields = '__all__'
        read_only_fields = ['submitted_at', 'is_validated', 'is_email_verified']

    def validate(self, attrs):
        email = attrs.get('email')
        name = attrs.get('name')

        if not email:   
            raise serializers.ValidationError("Email is required")

        if not name:
            raise serializers.ValidationError("Name is required")
        
        if ContactForm.objects.filter(email=email).exists():
            raise serializers.ValidationError("Email already exists")
        
        attrs['is_validated'] = True
        
        return attrs

    def save(self, **kwargs):
        return ContactForm.objects.create(**self.validated_data)

错误原因及修复方案

1. 核心错误:模型字段默认值设置错误

在models.py中,verification_token字段的default被设置为EmailVerificationTokenGenerator().make_token,这是把方法本身作为默认值,而非方法调用结果。而make_token方法必须接收contact_form参数才能生成令牌,创建模型实例时无法自动传入该参数,因此触发TypeError。

修复:删除verification_token字段
EmailVerificationTokenGenerator生成的令牌是基于用户实例(contact_form)和时间戳动态计算的,不需要持久化到数据库。直接删除models.py中的以下行:

verification_token = models.UUIDField(default=EmailVerificationTokenGenerator().make_token, editable=False)

2. 重发令牌未持久化

当前_get_resend_verification_link方法生成新的resend_token后,没有保存到数据库,导致下次验证时token不匹配。修改该方法:

def _get_resend_verification_link(self, contact_form):
    current_site = get_current_site(self.request)
    resend_token = secrets.token_urlsafe(32)
    # 将新令牌保存到数据库
    contact_form.resend_token = resend_token
    contact_form.save(update_fields=['resend_token'])
    resend_verification_url = reverse('resend_verification', kwargs={'pk': contact_form.pk, 'token': resend_token})
    resend_verification_link = f'{self.request.scheme}://{current_site.domain}{resend_verification_url}'
    return resend_verification_link

3. 重发验证URL缺少PK参数

urls.py中resend-verification的路径定义缺少pk参数,与视图要求不符,修改如下:

path('resend-verification/<int:pk>/<slug:token>/', ResendVerificationLinkAPIView.as_view(), name='resend_verification'),

额外优化:确保令牌过期后旧链接失效

你的EmailVerificationTokenGenerator已经实现了过期时间验证,只需确保验证视图(ActivateAPIView)中调用check_token方法即可,示例如下:

class ActivateAPIView(APIView):
    def get(self, request, uidb64, token):
        try:
            uid = force_str(urlsafe_base64_decode(uidb64))
            contact_form = ContactForm.objects.get(pk=uid)
        except (TypeError, ValueError, OverflowError, ContactForm.DoesNotExist):
            contact_form = None

        if contact_form is not None and EmailVerificationTokenGenerator().check_token(contact_form, token):
            contact_form.is_email_verified = True
            contact_form.save()
            return Response({'detail': 'Email verified successfully.'}, status=status.HTTP_200_OK)
        else:
            return Response({'detail': 'Invalid or expired verification link.'}, status=status.HTTP_400_BAD_REQUEST)

内容的提问来源于stack exchange,提问作者he1dj

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.17 23:26:59