Django用户修改邮箱后的验证功能实现求助
问题分析与解决方案
你的现有代码存在以下核心逻辑问题,导致无法实现需求:
- 直接更新用户邮箱:
UserEditView中调用super().form_valid(form)会直接将新邮箱保存到数据库,跳过了验证流程 - Session使用错误:创建了新的
SessionStore而非使用当前用户的会话,导致后续无法读取临时存储的邮箱数据 - 缺少新邮箱验证逻辑:仅向旧邮箱发送通知,未实现新邮箱的验证步骤
- 恢复链接逻辑缺失:没有处理旧邮箱点击恢复链接后回滚邮箱的流程
- 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:链接无效或过期页面
核心流程说明
- 用户提交新邮箱后,系统将旧邮箱、新邮箱和过期时间存储到当前用户的Session中
- 向新邮箱发送验证链接,向旧邮箱发送恢复链接(均为24小时有效)
- 用户点击新邮箱的验证链接:系统验证Token有效性和过期时间,正式更新数据库中的邮箱并清除Session数据
- 用户点击旧邮箱的恢复链接:系统验证Token有效性和过期时间,清除Session数据,取消邮箱变更
- 若24小时内新邮箱未验证,Session自动过期,变更请求失效
内容的提问来源于stack exchange,提问作者Tanmay
相关产品推荐
相关产品推荐

