From 38dab87ffb232222ff8bbcfa45b7e9fd5fbb4ab5 Mon Sep 17 00:00:00 2001 From: jyn <3080239289@qq.com> Date: Sun, 12 Oct 2025 20:01:48 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E6=A0=87=E6=B3=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/django-master/accounts/forms.py | 63 ++++- src/django-master/accounts/models.py | 38 ++- src/django-master/accounts/tests.py | 131 +++++++--- src/django-master/accounts/urls.py | 67 ++++-- .../accounts/user_login_backend.py | 34 ++- src/django-master/accounts/utils.py | 57 +++-- src/django-master/accounts/views.py | 224 +++++++++++------- 7 files changed, 427 insertions(+), 187 deletions(-) diff --git a/src/django-master/accounts/forms.py b/src/django-master/accounts/forms.py index fce4137..713edf5 100644 --- a/src/django-master/accounts/forms.py +++ b/src/django-master/accounts/forms.py @@ -4,23 +4,38 @@ from django.contrib.auth.forms import AuthenticationForm, UserCreationForm from django.core.exceptions import ValidationError from django.forms import widgets from django.utils.translation import gettext_lazy as _ -from . import utils -from .models import BlogUser +from . import utils # 导入自定义工具模块,可能用于验证码验证等功能 +from .models import BlogUser # 导入自定义用户模型 class LoginForm(AuthenticationForm): + """ + 自定义登录表单,继承自Django内置的AuthenticationForm + 用于处理用户登录验证,主要扩展了表单字段的样式 + """ def __init__(self, *args, **kwargs): + # 调用父类构造方法初始化表单 super(LoginForm, self).__init__(*args, **kwargs) + + # 自定义用户名输入框:添加占位符和CSS类 self.fields['username'].widget = widgets.TextInput( attrs={'placeholder': "username", "class": "form-control"}) + + # 自定义密码输入框:添加占位符和CSS类 self.fields['password'].widget = widgets.PasswordInput( attrs={'placeholder': "password", "class": "form-control"}) class RegisterForm(UserCreationForm): + """ + 自定义注册表单,继承自Django内置的UserCreationForm + 用于处理用户注册逻辑,包含用户名、邮箱和密码验证 + """ def __init__(self, *args, **kwargs): + # 调用父类构造方法初始化表单 super(RegisterForm, self).__init__(*args, **kwargs) + # 自定义各字段的输入控件,添加样式和占位符 self.fields['username'].widget = widgets.TextInput( attrs={'placeholder': "username", "class": "form-control"}) self.fields['email'].widget = widgets.EmailInput( @@ -31,17 +46,27 @@ class RegisterForm(UserCreationForm): attrs={'placeholder': "repeat password", "class": "form-control"}) def clean_email(self): + """ + 邮箱验证方法:检查邮箱是否已被注册 + 表单验证机制中,以clean_为前缀的方法会自动被调用 + """ email = self.cleaned_data['email'] + # 检查该邮箱是否已存在于数据库中 if get_user_model().objects.filter(email=email).exists(): - raise ValidationError(_("email already exists")) + raise ValidationError(_("email already exists")) # 抛出验证错误 return email class Meta: - model = get_user_model() - fields = ("username", "email") + model = get_user_model() # 使用项目配置的用户模型(可能是自定义的BlogUser) + fields = ("username", "email") # 注册表单中显示的字段 class ForgetPasswordForm(forms.Form): + """ + 忘记密码表单,用于用户重置密码的流程 + 包含新密码、确认密码、邮箱和验证码字段 + """ + # 新密码字段 new_password1 = forms.CharField( label=_("New password"), widget=forms.PasswordInput( @@ -52,6 +77,7 @@ class ForgetPasswordForm(forms.Form): ), ) + # 确认新密码字段 new_password2 = forms.CharField( label="确认密码", widget=forms.PasswordInput( @@ -62,6 +88,7 @@ class ForgetPasswordForm(forms.Form): ), ) + # 邮箱字段(用于验证用户身份) email = forms.EmailField( label='邮箱', widget=forms.TextInput( @@ -72,6 +99,7 @@ class ForgetPasswordForm(forms.Form): ), ) + # 验证码字段(用于身份验证) code = forms.CharField( label=_('Code'), widget=forms.TextInput( @@ -83,35 +111,46 @@ class ForgetPasswordForm(forms.Form): ) def clean_new_password2(self): + """验证两次输入的密码是否一致,并验证密码强度""" password1 = self.data.get("new_password1") password2 = self.data.get("new_password2") + + # 检查两次密码是否一致 if password1 and password2 and password1 != password2: raise ValidationError(_("passwords do not match")) + + # 使用Django内置的密码验证器验证密码强度 password_validation.validate_password(password2) - + return password2 def clean_email(self): + """验证邮箱是否已注册""" user_email = self.cleaned_data.get("email") - if not BlogUser.objects.filter( - email=user_email - ).exists(): - # todo 这里的报错提示可以判断一个邮箱是不是注册过,如果不想暴露可以修改 + # 检查该邮箱是否存在于系统中 + if not BlogUser.objects.filter(email=user_email).exists(): + # 提示邮箱不存在(实际应用中可能需要模糊提示以避免信息泄露) raise ValidationError(_("email does not exist")) return user_email def clean_code(self): + """验证验证码是否有效""" code = self.cleaned_data.get("code") + # 调用工具函数验证邮箱和验证码是否匹配 error = utils.verify( email=self.cleaned_data.get("email"), code=code, ) if error: - raise ValidationError(error) + raise ValidationError(error) # 验证码无效时抛出错误 return code class ForgetPasswordCodeForm(forms.Form): + """ + 发送密码重置验证码的表单 + 仅包含邮箱字段,用于提交需要重置密码的邮箱 + """ email = forms.EmailField( label=_('Email'), - ) + ) \ No newline at end of file diff --git a/src/django-master/accounts/models.py b/src/django-master/accounts/models.py index 3baddbb..c1279eb 100644 --- a/src/django-master/accounts/models.py +++ b/src/django-master/accounts/models.py @@ -3,33 +3,53 @@ from django.db import models from django.urls import reverse from django.utils.timezone import now from django.utils.translation import gettext_lazy as _ -from djangoblog.utils import get_current_site +from djangoblog.utils import get_current_site # 导入获取当前站点信息的工具函数 # Create your models here. class BlogUser(AbstractUser): + """ + 自定义用户模型,继承自Django内置的AbstractUser + 扩展了默认用户模型的字段,以满足博客系统的特定需求 + """ + # 昵称字段,允许为空 nickname = models.CharField(_('nick name'), max_length=100, blank=True) + # 账号创建时间,默认为当前时间 creation_time = models.DateTimeField(_('creation time'), default=now) + # 最后修改时间,默认为当前时间(通常需要在保存时更新) last_modify_time = models.DateTimeField(_('last modify time'), default=now) + # 账号创建来源(如管理员添加、前台注册等),用于追踪用户注册渠道 source = models.CharField(_('create source'), max_length=100, blank=True) def get_absolute_url(self): + """ + 返回用户详情页的URL + Django推荐为模型定义此方法,用于获取对象的标准URL + """ return reverse( - 'blog:author_detail', kwargs={ - 'author_name': self.username}) + 'blog:author_detail', # 对应的URL名称 + kwargs={'author_name': self.username} # 传递的参数 + ) def __str__(self): + """模型的字符串表示,这里返回用户的邮箱""" return self.email def get_full_url(self): + """获取用户详情页的完整URL(包含域名)""" + # 获取当前站点的域名 site = get_current_site().domain - url = "https://{site}{path}".format(site=site, - path=self.get_absolute_url()) + # 拼接完整URL(协议+域名+路径) + url = "https://{site}{path}".format( + site=site, + path=self.get_absolute_url() # 调用get_absolute_url获取相对路径 + ) return url class Meta: - ordering = ['-id'] - verbose_name = _('user') - verbose_name_plural = verbose_name - get_latest_by = 'id' + """模型的元数据配置""" + ordering = ['-id'] # 默认排序方式:按id降序(最新创建的用户在前) + verbose_name = _('user') # 模型的单数显示名称(支持国际化) + verbose_name_plural = verbose_name # 模型的复数显示名称(与单数相同) + get_latest_by = 'id' # 指定使用id字段获取最新对象(用于Model.objects.latest()) \ No newline at end of file diff --git a/src/django-master/accounts/tests.py b/src/django-master/accounts/tests.py index 6893411..70b2776 100644 --- a/src/django-master/accounts/tests.py +++ b/src/django-master/accounts/tests.py @@ -3,182 +3,235 @@ from django.urls import reverse from django.utils import timezone from django.utils.translation import gettext_lazy as _ -from accounts.models import BlogUser -from blog.models import Article, Category -from djangoblog.utils import * -from . import utils +from accounts.models import BlogUser # 导入自定义用户模型 +from blog.models import Article, Category # 导入博客相关模型 +from djangoblog.utils import * # 导入项目工具函数 +from . import utils # 导入当前应用的工具函数 # Create your tests here. class AccountTest(TestCase): + """ + 账号相关功能的测试类,继承自Django的TestCase + 包含用户登录、注册、密码重置等功能的测试用例 + """ def setUp(self): - self.client = Client() - self.factory = RequestFactory() + """ + 测试前的初始化方法,会在每个测试方法执行前运行 + 用于创建测试所需的基础数据 + """ + self.client = Client() # 创建测试客户端,用于模拟用户请求 + self.factory = RequestFactory() # 创建请求工厂,用于构建请求对象 + # 创建一个测试用户 self.blog_user = BlogUser.objects.create_user( username="test", email="admin@admin.com", password="12345678" ) - self.new_test = "xxx123--=" + self.new_test = "xxx123--=" # 测试用的新密码 def test_validate_account(self): - site = get_current_site().domain + """测试用户账号验证相关功能,包括登录和管理员权限""" + site = get_current_site().domain # 获取当前站点域名 + + # 创建一个超级用户 user = BlogUser.objects.create_superuser( email="liangliangyy1@gmail.com", username="liangliangyy1", password="qwer!@#$ggg") + # 获取创建的用户 testuser = BlogUser.objects.get(username='liangliangyy1') + # 测试登录功能 loginresult = self.client.login( username='liangliangyy1', password='qwer!@#$ggg') - self.assertEqual(loginresult, True) + self.assertEqual(loginresult, True) # 断言登录成功 + + # 测试访问管理员页面 response = self.client.get('/admin/') - self.assertEqual(response.status_code, 200) + self.assertEqual(response.status_code, 200) # 断言页面访问成功 + # 创建测试分类 category = Category() category.name = "categoryaaa" category.creation_time = timezone.now() category.last_modify_time = timezone.now() category.save() + # 创建测试文章 article = Article() article.title = "nicetitleaaa" article.body = "nicecontentaaa" article.author = user article.category = category - article.type = 'a' - article.status = 'p' + article.type = 'a' # 假设'a'表示文章类型 + article.status = 'p' # 假设'p'表示已发布 article.save() + # 测试访问文章管理页面 response = self.client.get(article.get_admin_url()) - self.assertEqual(response.status_code, 200) + self.assertEqual(response.status_code, 200) # 断言页面访问成功 def test_validate_register(self): + """测试用户注册功能,包括注册流程、邮箱验证和权限控制""" + # 初始状态下,该邮箱应不存在 self.assertEquals( 0, len( BlogUser.objects.filter( email='user123@user.com'))) + + # 模拟用户注册提交 response = self.client.post(reverse('account:register'), { 'username': 'user1233', 'email': 'user123@user.com', 'password1': 'password123!q@wE#R$T', 'password2': 'password123!q@wE#R$T', }) + + # 注册后,该邮箱应存在 self.assertEquals( 1, len( BlogUser.objects.filter( email='user123@user.com'))) + + # 获取刚注册的用户 user = BlogUser.objects.filter(email='user123@user.com')[0] + # 生成验证链接(模拟邮箱验证流程) sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) path = reverse('accounts:result') url = '{path}?type=validation&id={id}&sign={sign}'.format( path=path, id=user.id, sign=sign) + + # 访问验证链接 response = self.client.get(url) - self.assertEqual(response.status_code, 200) + self.assertEqual(response.status_code, 200) # 断言验证页面访问成功 + # 登录新注册用户 self.client.login(username='user1233', password='password123!q@wE#R$T') + # 提升用户权限 user = BlogUser.objects.filter(email='user123@user.com')[0] user.is_superuser = True user.is_staff = True user.save() + + # 清除缓存 delete_sidebar_cache() + + # 创建测试分类 category = Category() category.name = "categoryaaa" category.creation_time = timezone.now() category.last_modify_time = timezone.now() category.save() + # 创建测试文章 article = Article() article.category = category article.title = "nicetitle333" article.body = "nicecontentttt" article.author = user - article.type = 'a' article.status = 'p' article.save() + # 测试访问文章管理页面 response = self.client.get(article.get_admin_url()) self.assertEqual(response.status_code, 200) + # 测试登出功能 response = self.client.get(reverse('account:logout')) - self.assertIn(response.status_code, [301, 302, 200]) + self.assertIn(response.status_code, [301, 302, 200]) # 登出通常是重定向 + # 登出后访问管理页面(应被拒绝或重定向) response = self.client.get(article.get_admin_url()) self.assertIn(response.status_code, [301, 302, 200]) + # 使用错误密码登录 response = self.client.post(reverse('account:login'), { 'username': 'user1233', 'password': 'password123' }) self.assertIn(response.status_code, [301, 302, 200]) + # 错误登录后访问管理页面 response = self.client.get(article.get_admin_url()) self.assertIn(response.status_code, [301, 302, 200]) def test_verify_email_code(self): + """测试邮箱验证码验证功能""" to_email = "admin@admin.com" - code = generate_code() - utils.set_code(to_email, code) - utils.send_verify_email(to_email, code) + code = generate_code() # 生成验证码 + utils.set_code(to_email, code) # 存储验证码 + utils.send_verify_email(to_email, code) # 发送验证邮件 + # 验证正确的邮箱和验证码 err = utils.verify("admin@admin.com", code) - self.assertEqual(err, None) + self.assertEqual(err, None) # 应无错误 + # 验证错误的邮箱和正确的验证码 err = utils.verify("admin@123.com", code) - self.assertEqual(type(err), str) + self.assertEqual(type(err), str) # 应返回错误信息 def test_forget_password_email_code_success(self): + """测试发送密码重置验证码成功的情况""" resp = self.client.post( path=reverse("account:forget_password_code"), - data=dict(email="admin@admin.com") + data=dict(email="admin@admin.com") # 使用已存在的邮箱 ) - self.assertEqual(resp.status_code, 200) - self.assertEqual(resp.content.decode("utf-8"), "ok") + self.assertEqual(resp.status_code, 200) # 断言请求成功 + self.assertEqual(resp.content.decode("utf-8"), "ok") # 断言返回成功信息 def test_forget_password_email_code_fail(self): + """测试发送密码重置验证码失败的情况""" + # 不提供邮箱 resp = self.client.post( path=reverse("account:forget_password_code"), data=dict() ) - self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱") + self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱") # 断言返回错误信息 + # 提供无效格式的邮箱 resp = self.client.post( path=reverse("account:forget_password_code"), data=dict(email="admin@com") ) - self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱") + self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱") # 断言返回错误信息 def test_forget_password_email_success(self): - code = generate_code() - utils.set_code(self.blog_user.email, code) + """测试密码重置成功的情况""" + code = generate_code() # 生成验证码 + utils.set_code(self.blog_user.email, code) # 存储验证码 + # 准备重置密码的数据 data = dict( new_password1=self.new_test, new_password2=self.new_test, email=self.blog_user.email, code=code, ) + # 提交密码重置请求 resp = self.client.post( path=reverse("account:forget_password"), data=data ) - self.assertEqual(resp.status_code, 302) + self.assertEqual(resp.status_code, 302) # 成功重置后通常重定向 - # 验证用户密码是否修改成功 + # 验证密码是否已更新 blog_user = BlogUser.objects.filter( email=self.blog_user.email, ).first() # type: BlogUser - self.assertNotEqual(blog_user, None) + self.assertNotEqual(blog_user, None) # 断言用户存在 + # 断言密码修改成功 self.assertEqual(blog_user.check_password(data["new_password1"]), True) def test_forget_password_email_not_user(self): + """测试使用不存在的邮箱重置密码的情况""" data = dict( new_password1=self.new_test, new_password2=self.new_test, - email="123@123.com", + email="123@123.com", # 不存在的邮箱 code="123456", ) resp = self.client.post( @@ -186,22 +239,22 @@ class AccountTest(TestCase): data=data ) - self.assertEqual(resp.status_code, 200) - + self.assertEqual(resp.status_code, 200) # 应返回页面但不重置密码 def test_forget_password_email_code_error(self): - code = generate_code() - utils.set_code(self.blog_user.email, code) + """测试使用错误的验证码重置密码的情况""" + code = generate_code() # 生成正确验证码 + utils.set_code(self.blog_user.email, code) # 存储验证码 + # 使用错误的验证码 data = dict( new_password1=self.new_test, new_password2=self.new_test, email=self.blog_user.email, - code="111111", + code="111111", # 错误的验证码 ) resp = self.client.post( path=reverse("account:forget_password"), data=data ) - self.assertEqual(resp.status_code, 200) - + self.assertEqual(resp.status_code, 200) # 应返回页面但不重置密码 \ No newline at end of file diff --git a/src/django-master/accounts/urls.py b/src/django-master/accounts/urls.py index 107a801..dcde6e9 100644 --- a/src/django-master/accounts/urls.py +++ b/src/django-master/accounts/urls.py @@ -1,28 +1,49 @@ from django.urls import path -from django.urls import re_path +from django.urls import re_path # 用于正则表达式匹配URL -from . import views -from .forms import LoginForm +from . import views # 导入当前应用的视图函数/类 +from .forms import LoginForm # 导入自定义的登录表单 +# 定义应用命名空间,避免URL名称冲突 app_name = "accounts" -urlpatterns = [re_path(r'^login/$', - views.LoginView.as_view(success_url='/'), - name='login', - kwargs={'authentication_form': LoginForm}), - re_path(r'^register/$', - views.RegisterView.as_view(success_url="/"), - name='register'), - re_path(r'^logout/$', - views.LogoutView.as_view(), - name='logout'), - path(r'account/result.html', - views.account_result, - name='result'), - re_path(r'^forget_password/$', - views.ForgetPasswordView.as_view(), - name='forget_password'), - re_path(r'^forget_password_code/$', - views.ForgetPasswordEmailCode.as_view(), - name='forget_password_code'), - ] +# URL路由配置列表,映射URL路径到对应的视图 +urlpatterns = [ + # 登录页面路由:使用正则匹配以login/结尾的路径 + re_path(r'^login/$', + # 调用LoginView类视图,指定登录成功后重定向到首页(/) + views.LoginView.as_view(success_url='/'), + name='login', # URL的名称,用于反向解析 + # 向视图传递额外参数:指定登录表单为自定义的LoginForm + kwargs={'authentication_form': LoginForm}), + + # 注册页面路由:匹配以register/结尾的路径 + re_path(r'^register/$', + # 调用RegisterView类视图,注册成功后重定向到首页 + views.RegisterView.as_view(success_url="/"), + name='register'), # URL名称,用于反向解析 + + # 登出功能路由:匹配以logout/结尾的路径 + re_path(r'^logout/$', + # 调用LogoutView类视图(Django内置或自定义) + views.LogoutView.as_view(), + name='logout'), # URL名称 + + # 账号操作结果页面路由:精确匹配account/result.html路径 + path(r'account/result.html', + # 调用函数视图account_result + views.account_result, + name='result'), # URL名称,用于展示注册/验证等结果 + + # 忘记密码页面路由:匹配以forget_password/结尾的路径 + re_path(r'^forget_password/$', + # 调用ForgetPasswordView类视图 + views.ForgetPasswordView.as_view(), + name='forget_password'), # URL名称 + + # 发送密码重置验证码页面路由:匹配以forget_password_code/结尾的路径 + re_path(r'^forget_password_code/$', + # 调用ForgetPasswordEmailCode类视图(处理发送验证码逻辑) + views.ForgetPasswordEmailCode.as_view(), + name='forget_password_code'), # URL名称 +] \ No newline at end of file diff --git a/src/django-master/accounts/user_login_backend.py b/src/django-master/accounts/user_login_backend.py index 73cdca1..1b2211c 100644 --- a/src/django-master/accounts/user_login_backend.py +++ b/src/django-master/accounts/user_login_backend.py @@ -1,26 +1,50 @@ -from django.contrib.auth import get_user_model -from django.contrib.auth.backends import ModelBackend +from django.contrib.auth import get_user_model # 获取项目配置的用户模型(支持自定义模型) +from django.contrib.auth.backends import ModelBackend # 导入Django内置的模型认证后端 class EmailOrUsernameModelBackend(ModelBackend): """ - 允许使用用户名或邮箱登录 + 自定义认证后端,继承自Django的ModelBackend + 扩展功能:允许用户使用「用户名」或「邮箱」两种方式登录,而非仅支持用户名 """ def authenticate(self, request, username=None, password=None, **kwargs): + """ + 认证核心方法:验证用户输入的凭证(用户名/邮箱 + 密码)是否有效 + 参数说明: + - request:当前请求对象 + - username:前端传入的「用户名」参数(实际可能是用户名或邮箱) + - password:前端传入的密码(明文) + 返回:验证成功返回用户对象,失败返回None + """ + # 判断输入的「username」是否包含@符号,以此区分邮箱和用户名 if '@' in username: + # 若包含@,则按邮箱字段查询用户 kwargs = {'email': username} else: + # 若不包含@,则按用户名字段查询用户 kwargs = {'username': username} + try: + # 根据上述条件从数据库查询唯一用户 user = get_user_model().objects.get(**kwargs) + # 验证密码:check_password会自动将明文密码与数据库中存储的哈希密码比对 if user.check_password(password): - return user + return user # 密码正确,返回用户对象(认证成功) except get_user_model().DoesNotExist: + # 若查询不到用户(用户名/邮箱不存在),返回None(认证失败) return None def get_user(self, username): + """ + 根据用户ID获取用户对象(Django认证系统必须实现的方法) + 作用:认证成功后,系统通过此方法获取用户完整信息 + 参数:username实际为用户ID(Django默认用ID作为唯一标识) + 返回:存在则返回用户对象,不存在返回None + """ try: + # 通过主键(ID)查询用户 return get_user_model().objects.get(pk=username) except get_user_model().DoesNotExist: - return None + # 若用户不存在,返回None + return None \ No newline at end of file diff --git a/src/django-master/accounts/utils.py b/src/django-master/accounts/utils.py index 4b94bdf..858b241 100644 --- a/src/django-master/accounts/utils.py +++ b/src/django-master/accounts/utils.py @@ -1,49 +1,70 @@ import typing from datetime import timedelta -from django.core.cache import cache -from django.utils.translation import gettext -from django.utils.translation import gettext_lazy as _ +from django.core.cache import cache # 导入Django缓存模块,用于存储验证码 +from django.utils.translation import gettext # 用于获取即时翻译文本 +from django.utils.translation import gettext_lazy as _ # 用于延迟翻译文本(支持国际化) -from djangoblog.utils import send_email +from djangoblog.utils import send_email # 导入项目自定义的发送邮件工具函数 +# 验证码有效期:5分钟(全局变量,统一控制时效) _code_ttl = timedelta(minutes=5) def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email")): - """发送重设密码验证码 + """ + 发送邮箱验证码邮件(主要用于密码重置场景) Args: - to_mail: 接受邮箱 - subject: 邮件主题 - code: 验证码 + to_mail: 接收验证码的目标邮箱地址 + code: 生成的随机验证码 + subject: 邮件主题,默认值为“Verify Email”(支持国际化) """ + # 构造邮件HTML内容,包含验证码和有效期提示,使用%(code)s占位符注入验证码 html_content = _( "You are resetting the password, the verification code is:%(code)s, valid within 5 minutes, please keep it " "properly") % {'code': code} + # 调用发送邮件函数,参数依次为:收件人列表、邮件主题、邮件内容 send_email([to_mail], subject, html_content) def verify(email: str, code: str) -> typing.Optional[str]: - """验证code是否有效 + """ + 验证用户输入的验证码是否有效 Args: - email: 请求邮箱 - code: 验证码 + email: 用户提交的邮箱(用于匹配缓存中的验证码) + code: 用户输入的验证码 Return: - 如果有错误就返回错误str - Node: - 这里的错误处理不太合理,应该采用raise抛出 - 否测调用方也需要对error进行处理 + 验证失败时返回错误提示字符串,验证成功时返回None + Note: + 代码注释中指出当前错误处理逻辑不合理:应使用raise抛出异常,而非返回错误字符串 + 若返回错误字符串,调用方需额外判断返回值是否为错误,增加了代码耦合度 """ + # 从缓存中获取该邮箱对应的验证码 cache_code = get_code(email) + # 对比用户输入的验证码与缓存中的验证码 if cache_code != code: + # 验证码不匹配时,返回国际化的错误提示 return gettext("Verification code error") def set_code(email: str, code: str): - """设置code""" + """ + 将验证码存入缓存,以邮箱为key,设置有效期 + Args: + email: 作为缓存key的邮箱地址(确保一个邮箱对应一个验证码) + code: 需要存入缓存的验证码 + """ + # 调用Django缓存的set方法:key=邮箱,value=验证码,timeout=有效期(秒) cache.set(email, code, _code_ttl.seconds) def get_code(email: str) -> typing.Optional[str]: - """获取code""" - return cache.get(email) + """ + 从缓存中获取指定邮箱对应的验证码 + Args: + email: 用于查询的邮箱地址(缓存key) + Return: + 缓存中存在该邮箱对应的验证码时返回字符串,不存在时返回None + """ + # 调用Django缓存的get方法,根据邮箱key获取验证码 + return cache.get(email) \ No newline at end of file diff --git a/src/django-master/accounts/views.py b/src/django-master/accounts/views.py index ae67aec..810e352 100644 --- a/src/django-master/accounts/views.py +++ b/src/django-master/accounts/views.py @@ -2,58 +2,72 @@ import logging from django.utils.translation import gettext_lazy as _ from django.conf import settings from django.contrib import auth -from django.contrib.auth import REDIRECT_FIELD_NAME -from django.contrib.auth import get_user_model -from django.contrib.auth import logout -from django.contrib.auth.forms import AuthenticationForm -from django.contrib.auth.hashers import make_password -from django.http import HttpResponseRedirect, HttpResponseForbidden +from django.contrib.auth import REDIRECT_FIELD_NAME # 登录后重定向字段名常量 +from django.contrib.auth import get_user_model # 获取项目配置的用户模型 +from django.contrib.auth import logout # 登出功能函数 +from django.contrib.auth.forms import AuthenticationForm # Django内置登录表单 +from django.contrib.auth.hashers import make_password # 密码哈希处理函数 +from django.http import HttpResponseRedirect, HttpResponseForbidden # HTTP响应类 from django.http.request import HttpRequest from django.http.response import HttpResponse -from django.shortcuts import get_object_or_404 -from django.shortcuts import render -from django.urls import reverse -from django.utils.decorators import method_decorator -from django.utils.http import url_has_allowed_host_and_scheme -from django.views import View -from django.views.decorators.cache import never_cache -from django.views.decorators.csrf import csrf_protect -from django.views.decorators.debug import sensitive_post_parameters -from django.views.generic import FormView, RedirectView - -from djangoblog.utils import send_email, get_sha256, get_current_site, generate_code, delete_sidebar_cache -from . import utils -from .forms import RegisterForm, LoginForm, ForgetPasswordForm, ForgetPasswordCodeForm -from .models import BlogUser - -logger = logging.getLogger(__name__) +from django.shortcuts import get_object_or_404, render # 快捷函数 +from django.urls import reverse # URL反向解析 +from django.utils.decorators import method_decorator # 类视图装饰器工具 +from django.utils.http import url_has_allowed_host_and_scheme # 验证重定向URL安全性 +from django.views import View # 基础视图类 +from django.views.decorators.cache import never_cache # 禁止缓存装饰器 +from django.views.decorators.csrf import csrf_protect # CSRF保护装饰器 +from django.views.decorators.debug import sensitive_post_parameters # 敏感参数保护装饰器 +from django.views.generic import FormView, RedirectView # 通用类视图 + +from djangoblog.utils import (send_email, get_sha256, get_current_site, + generate_code, delete_sidebar_cache) # 项目工具函数 +from . import utils # 当前应用工具函数(验证码相关) +from .forms import (RegisterForm, LoginForm, ForgetPasswordForm, + ForgetPasswordCodeForm) # 当前应用表单类 +from .models import BlogUser # 自定义用户模型 + +logger = logging.getLogger(__name__) # 初始化日志记录器 # Create your views here. class RegisterView(FormView): - form_class = RegisterForm - template_name = 'account/registration_form.html' - - @method_decorator(csrf_protect) + """ + 用户注册类视图,继承自FormView(处理表单提交的通用视图) + 负责用户注册表单展示、数据验证、发送验证邮件及注册结果跳转 + """ + form_class = RegisterForm # 指定使用的注册表单 + template_name = 'account/registration_form.html' # 注册页面模板路径 + + @method_decorator(csrf_protect) # 为视图添加CSRF保护 def dispatch(self, *args, **kwargs): + """重写分发方法,添加装饰器后调用父类逻辑""" return super(RegisterView, self).dispatch(*args, **kwargs) def form_valid(self, form): + """表单验证通过后执行的逻辑(注册核心流程)""" if form.is_valid(): + # 1. 暂存用户数据,不立即保存(is_active设为False,需邮箱验证后激活) user = form.save(False) - user.is_active = False - user.source = 'Register' - user.save(True) - site = get_current_site().domain + user.is_active = False # 初始状态:未激活(需邮箱验证) + user.source = 'Register' # 标记注册来源为“前台注册” + user.save(True) # 保存用户到数据库 + + # 2. 生成邮箱验证链接(包含签名,防止篡改) + site = get_current_site().domain # 获取当前站点域名 + # 双重SHA256加密:用SECRET_KEY+用户ID生成签名,确保链接安全性 sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) + # 开发环境下替换域名(适配本地测试) if settings.DEBUG: site = '127.0.0.1:8000' + # 反向解析结果页URL,拼接完整验证链接 path = reverse('account:result') url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format( site=site, path=path, id=user.id, sign=sign) + # 3. 构造验证邮件内容并发送 content = """
请点击下面链接验证您的邮箱
@@ -65,140 +79,188 @@ class RegisterView(FormView): {url} """.format(url=url) send_email( - emailto=[ - user.email, - ], - title='验证您的电子邮箱', - content=content) - - url = reverse('accounts:result') + \ - '?type=register&id=' + str(user.id) + emailto=[user.email], # 收件人邮箱(新注册用户的邮箱) + title='验证您的电子邮箱', # 邮件标题 + content=content # 邮件HTML内容 + ) + + # 4. 跳转到注册结果页(提示用户查收验证邮件) + url = reverse('accounts:result') + f'?type=register&id={str(user.id)}' return HttpResponseRedirect(url) else: - return self.render_to_response({ - 'form': form - }) + # 表单验证失败,重新渲染表单并显示错误 + return self.render_to_response({'form': form}) class LogoutView(RedirectView): - url = '/login/' + """ + 用户登出类视图,继承自RedirectView(处理重定向的通用视图) + 负责清除用户会话、缓存,并重定向到登录页 + """ + url = '/login/' # 登出后默认重定向地址(登录页) - @method_decorator(never_cache) + @method_decorator(never_cache) # 禁止缓存登出页面,避免浏览器缓存导致的问题 def dispatch(self, request, *args, **kwargs): + """重写分发方法,添加装饰器后调用父类逻辑""" return super(LogoutView, self).dispatch(request, *args, **kwargs) def get(self, request, *args, **kwargs): - logout(request) - delete_sidebar_cache() - return super(LogoutView, self).get(request, *args, **kwargs) + """处理GET请求(登出核心逻辑)""" + logout(request) # 清除用户会话,实现登出 + delete_sidebar_cache() # 删除侧边栏缓存(可能存储了用户相关信息) + return super(LogoutView, self).get(request, *args, **kwargs) # 执行重定向 class LoginView(FormView): - form_class = LoginForm - template_name = 'account/login.html' - success_url = '/' - redirect_field_name = REDIRECT_FIELD_NAME - login_ttl = 2626560 # 一个月的时间 - - @method_decorator(sensitive_post_parameters('password')) + """ + 用户登录类视图,继承自FormView + 负责登录表单展示、数据验证、用户认证、记住登录状态及重定向 + """ + form_class = LoginForm # 指定使用的自定义登录表单 + template_name = 'account/login.html' # 登录页面模板路径 + success_url = '/' # 登录成功默认重定向地址(首页) + redirect_field_name = REDIRECT_FIELD_NAME # 重定向字段名(默认next) + login_ttl = 2626560 # “记住登录”状态的有效期(秒),约等于1个月 + + # 为视图添加多重装饰器:敏感参数保护、CSRF保护、禁止缓存 + @method_decorator(sensitive_post_parameters('password')) # 保护密码参数,避免日志泄露 @method_decorator(csrf_protect) @method_decorator(never_cache) def dispatch(self, request, *args, **kwargs): - return super(LoginView, self).dispatch(request, *args, **kwargs) def get_context_data(self, **kwargs): + """添加额外上下文数据(重定向地址)到模板""" + # 获取URL中的重定向参数(如登录前访问的受保护页面) redirect_to = self.request.GET.get(self.redirect_field_name) if redirect_to is None: - redirect_to = '/' - kwargs['redirect_to'] = redirect_to - - return super(LoginView, self).get_context_data(**kwargs) + redirect_to = '/' # 默认重定向到首页 + kwargs['redirect_to'] = redirect_to # 将重定向地址传入模板上下文 + return super(LoginView, self).get_context_data(** kwargs) def form_valid(self, form): + """表单验证通过后执行的逻辑(登录核心流程)""" + # 用Django内置AuthenticationForm重新验证(确保认证逻辑符合默认规范) form = AuthenticationForm(data=self.request.POST, request=self.request) if form.is_valid(): - delete_sidebar_cache() - logger.info(self.redirect_field_name) + delete_sidebar_cache() # 删除侧边栏缓存(更新用户登录状态) + logger.info(self.redirect_field_name) # 日志记录重定向字段名 + # 执行登录:将用户信息存入会话 auth.login(self.request, form.get_user()) + # 处理“记住我”功能:若勾选,设置会话有效期为1个月 if self.request.POST.get("remember"): self.request.session.set_expiry(self.login_ttl) + # 调用父类form_valid,执行重定向 return super(LoginView, self).form_valid(form) - # return HttpResponseRedirect('/') else: - return self.render_to_response({ - 'form': form - }) + # 表单验证失败(如密码错误),重新渲染表单并显示错误 + return self.render_to_response({'form': form}) def get_success_url(self): - + """自定义登录成功后的重定向地址(优先使用URL中的next参数)""" + # 获取POST请求中的重定向地址(用户登录前尝试访问的页面) redirect_to = self.request.POST.get(self.redirect_field_name) + # 验证重定向地址的安全性:避免跳转到外部恶意网站 if not url_has_allowed_host_and_scheme( - url=redirect_to, allowed_hosts=[ - self.request.get_host()]): - redirect_to = self.success_url + url=redirect_to, allowed_hosts=[self.request.get_host()]): + redirect_to = self.success_url # 不安全则使用默认重定向地址 return redirect_to def account_result(request): + """ + 账号操作结果视图函数(函数视图) + 处理注册成功提示、邮箱验证逻辑,并展示结果页面 + """ + # 从URL参数中获取操作类型(register/validation)和用户ID type = request.GET.get('type') id = request.GET.get('id') + # 获取对应的用户,若不存在则返回404 user = get_object_or_404(get_user_model(), id=id) - logger.info(type) + logger.info(type) # 日志记录操作类型 + + # 若用户已激活,直接重定向到首页(避免重复验证) if user.is_active: return HttpResponseRedirect('/') + + # 处理合法的操作类型(注册成功提示/邮箱验证) if type and type in ['register', 'validation']: if type == 'register': + # 注册成功:提示用户查收验证邮件 content = ''' 恭喜您注册成功,一封验证邮件已经发送到您的邮箱,请验证您的邮箱后登录本站。 ''' title = '注册成功' else: + # 邮箱验证:验证签名是否正确,正确则激活用户 + # 重新计算签名,与URL中的签名对比(防止链接篡改) c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) sign = request.GET.get('sign') if sign != c_sign: - return HttpResponseForbidden() + return HttpResponseForbidden() # 签名不匹配,返回403禁止访问 + # 激活用户:将is_active设为True user.is_active = True user.save() + # 验证成功:提示用户可登录 content = ''' 恭喜您已经成功的完成邮箱验证,您现在可以使用您的账号来登录本站。 ''' title = '验证成功' + # 渲染结果页面,传递标题和内容 return render(request, 'account/result.html', { 'title': title, 'content': content }) else: + # 操作类型不合法,重定向到首页 return HttpResponseRedirect('/') class ForgetPasswordView(FormView): - form_class = ForgetPasswordForm - template_name = 'account/forget_password.html' + """ + 忘记密码类视图,继承自FormView + 负责密码重置表单展示、数据验证,及更新用户密码 + """ + form_class = ForgetPasswordForm # 指定使用的密码重置表单 + template_name = 'account/forget_password.html' # 密码重置页面模板路径 def form_valid(self, form): + """表单验证通过后执行的逻辑(密码重置核心流程)""" if form.is_valid(): - blog_user = BlogUser.objects.filter(email=form.cleaned_data.get("email")).get() + # 1. 获取表单中的邮箱,查询对应的用户 + blog_user = BlogUser.objects.filter( + email=form.cleaned_data.get("email") + ).get() + # 2. 对新密码进行哈希处理,并更新用户密码 blog_user.password = make_password(form.cleaned_data["new_password2"]) blog_user.save() + # 3. 密码重置成功,重定向到登录页 return HttpResponseRedirect('/login/') else: + # 表单验证失败(如验证码错误、密码不一致),重新渲染表单 return self.render_to_response({'form': form}) class ForgetPasswordEmailCode(View): - + """ + 发送密码重置验证码类视图,继承自基础View + 负责验证邮箱合法性、生成验证码、发送验证邮件,并返回结果 + """ def post(self, request: HttpRequest): + """处理POST请求(发送验证码核心逻辑)""" + # 1. 验证邮箱表单数据 form = ForgetPasswordCodeForm(request.POST) if not form.is_valid(): - return HttpResponse("错误的邮箱") - to_email = form.cleaned_data["email"] + return HttpResponse("错误的邮箱") # 邮箱格式不合法,返回错误提示 - code = generate_code() - utils.send_verify_email(to_email, code) - utils.set_code(to_email, code) + # 2. 生成验证码并发送邮件 + to_email = form.cleaned_data["email"] # 获取合法的邮箱地址 + code = generate_code() # 生成随机验证码 + utils.send_verify_email(to_email, code) # 发送验证码邮件 + utils.set_code(to_email, code) # 将验证码存入缓存(设置有效期) - return HttpResponse("ok") + # 3. 操作成功,返回“ok”提示 + return HttpResponse("ok") \ No newline at end of file