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

Django用户修改邮箱后的验证功能实现求助

问题分析与解决方案

你的现有代码存在以下核心逻辑问题,导致无法实现需求:

  1. 直接更新用户邮箱:UserEditView中调用super().form_valid(form)会直接将新邮箱保存到数据库,跳过了验证流程
  2. Session使用错误:创建了新的SessionStore而非使用当前用户的会话,导致后续无法读取临时存储的邮箱数据
  3. 缺少新邮箱验证逻辑:仅向旧邮箱发送通知,未实现新邮箱的验证步骤
  4. 恢复链接逻辑缺失:没有处理旧邮箱点击恢复链接后回滚邮箱的流程
  5. Token安全性不足:当前Token生成未包含邮箱信息,邮箱变更后旧Token仍可能有效

以下是调整后的完整实现,严格遵循你的需求(不使用额外模型、不依赖allauth):

1. 修正tokens.py:增强Token安全性

让Token包含旧邮箱信息,确保邮箱变更后旧Token失效:

from django.contrib.auth.tokens import PasswordResetTokenGenerator
from six import text_type

class TokenGenerator(PasswordResetTokenGenerator):
    def _make_hash_value(self, user, timestamp, old_email=None):
        hash_value = text_type(user.pk) + text_type(timestamp)
        if old_email:
            hash_value += text_type(old_email)
        return hash_value

generate_token = TokenGenerator()

2. 重构views.py:实现完整验证流程

from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth import authenticate, login, logout, update_session_auth_hash
from django.contrib.auth.forms import UserCreationForm, UserChangeForm, PasswordChangeForm
from django.contrib.auth.views import PasswordChangeView
from .forms import RegisterUserForm, EditProfileForm, EditProfileFormNormal, PasswordChangingForm
from django.views import generic
from django.urls import reverse_lazy
from django.contrib import messages
from django.http import HttpResponse
from mysite import settings
from django.core.mail import send_mail, EmailMessage
from django.contrib.auth.models import User
from .tokens import generate_token
from django.template.loader import render_to_string
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
from django.utils.encoding import force_bytes, force_str
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.contrib.sites.shortcuts import get_current_site
from django.contrib.sessions.backends.db import SessionStore
from django.views.generic.edit import FormView
from django.core.exceptions import ObjectDoesNotExist
from django.utils import timezone


class UserEditView(LoginRequiredMixin, UserPassesTestMixin, generic.UpdateView):
    model = User
    form_class = EditProfileForm
    template_name = 'authstuff/edit_profile.html'
    success_url = reverse_lazy('profile')

    def test_func(self):
        user = self.get_object()
        return self.request.user.is_authenticated and self.request.user.pk == user.pk

    def form_valid(self, form):
        user = form.save(commit=False)
        new_email = form.cleaned_data['email']
        old_email = user.email

        if old_email != new_email:
            # 存储临时数据到当前用户session
            self.request.session['email_change_data'] = {
                'old_email': old_email,
                'new_email': new_email,
                'expires_at': (timezone.now() + timezone.timedelta(hours=24)).isoformat()
            }

            current_site = get_current_site(self.request)
            uidb64 = urlsafe_base64_encode(force_bytes(user.pk))

            # 1. 向新邮箱发送验证邮件
            new_email_subject = "验证你的新邮箱 - 我的博客"
            new_email_message = render_to_string('authstuff/email_new_confirmation.html', {
                'user': user,
                'domain': current_site.domain,
                'uidb64': uidb64,
                'token': generate_token.make_token(user, timezone.now(), old_email=old_email),
            })
            new_email = EmailMessage(
                new_email_subject,
                new_email_message,
                settings.EMAIL_HOST_USER,
                [new_email],
            )
            new_email.send()

            # 2. 向旧邮箱发送恢复通知邮件
            old_email_subject = "你的邮箱即将变更 - 我的博客"
            old_email_message = render_to_string('authstuff/email_revert_confirmation.html', {
                'user': user,
                'domain': current_site.domain,
                'uidb64': uidb64,
                'token': generate_token.make_token(user, timezone.now(), old_email=old_email),
                'new_email': new_email,
            })
            old_email = EmailMessage(
                old_email_subject,
                old_email_message,
                settings.EMAIL_HOST_USER,
                [old_email],
            )
            old_email.send()

            messages.success(self.request, "邮箱变更请求已提交,请检查新邮箱完成验证,旧邮箱可在24小时内取消变更。")
            # 不直接保存用户,跳转到提示页面
            return redirect('email_change_pending')

        # 邮箱未变更,正常保存
        return super().form_valid(form)

    def get_object(self, queryset=None):
        return get_object_or_404(User, pk=self.kwargs['pk'])


def email_change_pending(request):
    return render(request, 'authstuff/email_change_pending.html')


def confirm_new_email(request, uidb64, token):
    try:
        uid = force_str(urlsafe_base64_decode(uidb64))
        user = get_object_or_404(User, pk=uid)
    except (TypeError, ValueError, OverflowError, User.DoesNotExist):
        user = None

    if not user:
        return render(request, 'authstuff/email_change_invalid.html')

    # 读取session中的临时数据
    email_change_data = request.session.get('email_change_data')
    if not email_change_data:
        messages.error(request, "变更请求已过期或不存在")
        return redirect('profile')

    # 检查过期时间
    expires_at = timezone.datetime.fromisoformat(email_change_data['expires_at'])
    if timezone.now() > expires_at:
        del request.session['email_change_data']
        messages.error(request, "变更请求已过期")
        return redirect('profile')

    old_email = email_change_data['old_email']
    new_email = email_change_data['new_email']

    # 验证Token(包含旧邮箱信息)
    if generate_token.check_token(user, token, old_email=old_email):
        # 正式更新邮箱
        user.email = new_email
        user.save()
        # 清除session数据
        del request.session['email_change_data']
        messages.success(request, "邮箱已成功变更并验证")
        return render(request, 'authstuff/email_change_success.html')
    else:
        return render(request, 'authstuff/email_change_invalid.html')


def revert_email_change(request, uidb64, token):
    try:
        uid = force_str(urlsafe_base64_decode(uidb64))
        user = get_object_or_404(User, pk=uid)
    except (TypeError, ValueError, OverflowError, User.DoesNotExist):
        user = None

    if not user:
        return render(request, 'authstuff/email_change_invalid.html')

    email_change_data = request.session.get('email_change_data')
    if not email_change_data:
        messages.error(request, "无待恢复的变更请求")
        return redirect('profile')

    expires_at = timezone.datetime.fromisoformat(email_change_data['expires_at'])
    if timezone.now() > expires_at:
        del request.session['email_change_data']
        messages.error(request, "恢复请求已过期")
        return redirect('profile')

    old_email = email_change_data['old_email']
    if generate_token.check_token(user, token, old_email=old_email):
        # 清除session数据,取消变更
        del request.session['email_change_data']
        messages.success(request, "邮箱变更已取消,你的邮箱保持不变")
        return redirect('profile')
    else:
        return render(request, 'authstuff/email_change_invalid.html')

3. 修正urls.py:添加新的路由

from django.urls import path
from . import views
from .views import UserEditView

urlpatterns = [
    path('edit_user_profile/<int:pk>/', UserEditView.as_view(), name="edit-profile"),
    path('email-change-pending/', views.email_change_pending, name="email_change_pending"),
    path('confirm-new-email/<uidb64>/<token>/', views.confirm_new_email, name="confirm-new-email"),
    path('revert-email-change/<uidb64>/<token>/', views.revert_email_change, name="revert-email-change"),
]

4. 添加新的邮件模板

新邮箱验证模板 email_new_confirmation.html

{% autoescape off %}  
Hi {{ user.username }},  

请点击下方链接验证你的新邮箱,完成邮箱变更:

http://{{ domain }}{% url 'confirm-new-email' uidb64=uidb64 token=token %}

该链接24小时内有效,过期后需重新提交变更请求。

{% endautoescape %}

旧邮箱恢复模板 email_revert_confirmation.html

{% autoescape off %}  
Hi {{ user.username }},  

我们检测到你的账户邮箱即将变更为 {{ new_email }}。如果这不是你操作的,请点击下方链接取消变更:

http://{{ domain }}{% url 'revert-email-change' uidb64=uidb64 token=token %}

该链接24小时内有效,过期后变更将自动失效。

{% endautoescape %}

5. 补充必要的HTML模板

  • email_change_pending.html:显示变更待验证的提示页面
  • email_change_success.html:邮箱变更成功页面
  • email_change_invalid.html:链接无效或过期页面

核心流程说明

  1. 用户提交新邮箱后,系统将旧邮箱、新邮箱和过期时间存储到当前用户的Session中
  2. 向新邮箱发送验证链接,向旧邮箱发送恢复链接(均为24小时有效)
  3. 用户点击新邮箱的验证链接:系统验证Token有效性和过期时间,正式更新数据库中的邮箱并清除Session数据
  4. 用户点击旧邮箱的恢复链接:系统验证Token有效性和过期时间,清除Session数据,取消邮箱变更
  5. 若24小时内新邮箱未验证,Session自动过期,变更请求失效

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.21 01:18:09