Django中EmailVerificationTokenGenerator.make_token()参数缺失问题求助
问题详情
我正在搭建网站邮件服务器,实现联系表单提交后发送邮件验证链接,响应中返回重发验证链接的功能。此前功能正常,但调整后出现以下错误:
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

