hyt_accounts APP注释

pull/27/head
hyt 3 months ago
parent f78fd877a8
commit 1cf5626376

@ -0,0 +1,113 @@
#hyt:
from django import forms
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.forms import UserChangeForm
from django.contrib.auth.forms import UsernameField
from django.utils.translation import gettext_lazy as _
# Register your models here.
from .models import BlogUser
class BlogUserCreationForm(forms.ModelForm):
"""
博客用户创建表单
用于管理员后台创建新用户提供密码验证和哈希处理功能
扩展了 Django 原生用户创建逻辑支持自定义字段和来源标记
"""
password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput)
password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput)
class Meta:
model = BlogUser
fields = ('email',) # 创建用户时只要求填写邮箱字段
def clean_password2(self):
"""
密码确认验证
检查两次输入的密码是否一致
确保用户在创建账户时正确确认密码
返回:
验证通过的密码
异常:
ValidationError: 当两次密码不匹配时抛出
"""
password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2")
if password1 and password2 and password1 != password2:
raise forms.ValidationError(_("passwords do not match"))
return password2
def save(self, commit=True):
"""
保存用户信息
重写保存方法对密码进行哈希处理并标记用户来源
参数:
commit: 是否立即保存到数据库
返回:
保存后的用户对象
"""
# 保存提供的密码为哈希格式
user = super().save(commit=False)
user.set_password(self.cleaned_data["password1"])
if commit:
user.source = 'adminsite' # 标记用户来源为管理员后台
user.save()
return user
class BlogUserChangeForm(UserChangeForm):
"""
博客用户信息修改表单
用于管理员后台编辑现有用户信息
继承 Django 原生用户修改表单支持所有字段编辑
"""
class Meta:
model = BlogUser
fields = '__all__' # 包含所有模型字段
field_classes = {'username': UsernameField} # 用户名字段使用特定字段类
def __init__(self, *args, **kwargs):
"""
初始化表单
调用父类初始化方法可在此处添加自定义初始化逻辑
"""
super().__init__(*args, **kwargs)
class BlogUserAdmin(UserAdmin):
"""
博客用户管理后台配置
自定义用户模型在 Django Admin 后台的显示和编辑配置
扩展了默认的用户管理功能添加了自定义字段显示
"""
form = BlogUserChangeForm # 用户编辑表单
add_form = BlogUserCreationForm # 用户创建表单
# 列表页面显示的字段
list_display = (
'id',
'nickname',
'username',
'email',
'last_login',
'date_joined',
'source'
)
list_display_links = ('id', 'username') # 可点击链接的字段
ordering = ('-id',) # 按 ID 降序排列

@ -0,0 +1,20 @@
#hyt:
from django.apps import AppConfig
class AccountsConfig(AppConfig):
"""
账户管理应用配置类
负责配置 accounts 应用在 Django 项目中的行为
包括应用名称初始化逻辑信号注册等配置项
功能特性:
- 用户认证和权限管理
- 用户资料管理
- 登录注册功能
- 会话管理
"""
# 应用路径标识 - Django 用于识别应用的完整 Python 路径
name = 'accounts'

@ -0,0 +1,210 @@
#hyt:
from django import forms
from django.contrib.auth import get_user_model, password_validation
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
class LoginForm(AuthenticationForm):
"""
用户登录表单
继承 Django 原生认证表单自定义界面样式和占位符文本
提供用户名和密码的登录验证功能
"""
def __init__(self, *args, **kwargs):
"""
初始化表单控件
设置表单字段的样式类和占位符文本优化用户体验
"""
super(LoginForm, self).__init__(*args, **kwargs)
# 设置用户名字段的输入框样式
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"})
# 设置密码字段的输入框样式
self.fields['password'].widget = widgets.PasswordInput(
attrs={'placeholder': "password", "class": "form-control"})
class RegisterForm(UserCreationForm):
"""
用户注册表单
处理新用户注册流程包括用户名邮箱验证和密码确认
扩展了 Django 原生用户创建表单添加邮箱字段和样式定制
"""
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(
attrs={'placeholder': "email", "class": "form-control"})
self.fields['password1'].widget = widgets.PasswordInput(
attrs={'placeholder': "password", "class": "form-control"})
self.fields['password2'].widget = widgets.PasswordInput(
attrs={'placeholder': "repeat password", "class": "form-control"})
def clean_email(self):
"""
邮箱唯一性验证
检查邮箱是否已被其他用户注册确保邮箱地址的唯一性
返回:
验证通过的邮箱地址
异常:
ValidationError: 当邮箱已存在时抛出
"""
email = self.cleaned_data['email']
if get_user_model().objects.filter(email=email).exists():
raise ValidationError(_("email already exists"))
return email
class Meta:
"""表单元数据配置"""
model = get_user_model() # 使用当前激活的用户模型
fields = ("username", "email") # 注册时需要填写的字段
class ForgetPasswordForm(forms.Form):
"""
密码重置表单
处理用户忘记密码时的重置流程包含邮箱验证验证码校验和新密码设置
通过多步骤验证确保账户安全
"""
# 新密码字段 - 第一次输入
new_password1 = forms.CharField(
label=_("New password"),
widget=forms.PasswordInput(
attrs={
"class": "form-control",
'placeholder': _("New password")
}
),
)
# 新密码字段 - 确认输入
new_password2 = forms.CharField(
label="确认密码",
widget=forms.PasswordInput(
attrs={
"class": "form-control",
'placeholder': _("Confirm password")
}
),
)
# 邮箱字段 - 用于身份验证
email = forms.EmailField(
label='邮箱',
widget=forms.TextInput(
attrs={
'class': 'form-control',
'placeholder': _("Email")
}
),
)
# 验证码字段 - 安全验证
code = forms.CharField(
label=_('Code'),
widget=forms.TextInput(
attrs={
'class': 'form-control',
'placeholder': _("Code")
}
),
)
def clean_new_password2(self):
"""
新密码确认验证
检查两次输入的新密码是否一致并验证密码强度
返回:
验证通过的密码
异常:
ValidationError: 当密码不匹配或强度不足时抛出
"""
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"))
# 验证密码强度
password_validation.validate_password(password2)
return password2
def clean_email(self):
"""
邮箱存在性验证
验证输入的邮箱是否在系统中已注册
返回:
验证通过的邮箱地址
异常:
ValidationError: 当邮箱不存在时抛出
"""
user_email = self.cleaned_data.get("email")
if not BlogUser.objects.filter(email=user_email).exists():
# TODO: 这里的报错提示可以判断一个邮箱是不是注册过,如果不想暴露可以修改
raise ValidationError(_("email does not exist"))
return user_email
def clean_code(self):
"""
验证码校验
验证邮箱和验证码的匹配关系确保重置请求的合法性
返回:
验证通过的验证码
异常:
ValidationError: 当验证码无效时抛出
"""
code = self.cleaned_data.get("code")
error = utils.verify(
email=self.cleaned_data.get("email"),
code=code,
)
if error:
raise ValidationError(error)
return code
class ForgetPasswordCodeForm(forms.Form):
"""
密码重置验证码请求表单
用于请求发送密码重置验证码只需提供邮箱地址
是密码重置流程的第一步
"""
# 邮箱字段 - 用于发送验证码
email = forms.EmailField(
label=_('Email'),
)

@ -0,0 +1,104 @@
#hyt:
# Generated by Django 4.1.7 on 2023-03-02 07:14
import django.contrib.auth.models
import django.contrib.auth.validators
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
"""
BlogUser 模型的初始迁移文件
创建自定义用户模型 BlogUser扩展 Django 内置 User 模型
添加了昵称时间戳和来源字段支持中文显示和自定义排序
"""
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name='BlogUser',
fields=[
# 主键字段 - 使用 BigAutoField 作为自增主键
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
# 认证相关字段 - Django 内置用户认证系统必需字段
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
# 权限相关字段 - 用户权限和状态管理
('is_superuser', models.BooleanField(
default=False,
help_text='Designates that this user has all permissions without explicitly assigning them.',
verbose_name='superuser status'
)),
# 用户基本信息字段 - 用户名、姓名、邮箱等
('username', models.CharField(
error_messages={'unique': 'A user with that username already exists.'},
help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.',
max_length=150,
unique=True,
validators=[django.contrib.auth.validators.UnicodeUsernameValidator()],
verbose_name='username'
)),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
# 状态字段 - 用户状态标识
('is_staff', models.BooleanField(
default=False,
help_text='Designates whether the user can log into this admin site.',
verbose_name='staff status'
)),
('is_active', models.BooleanField(
default=True,
help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.',
verbose_name='active'
)),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
# 自定义扩展字段 - 博客用户特有字段
('nickname', models.CharField(blank=True, max_length=100, verbose_name='昵称')),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
('source', models.CharField(blank=True, max_length=100, verbose_name='创建来源')),
# 权限关联字段 - 用户组和权限的多对多关系
('groups', models.ManyToManyField(
blank=True,
help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.',
related_name='user_set',
related_query_name='user',
to='auth.group',
verbose_name='groups'
)),
('user_permissions', models.ManyToManyField(
blank=True,
help_text='Specific permissions for this user.',
related_name='user_set',
related_query_name='user',
to='auth.permission',
verbose_name='user permissions'
)),
],
options={
# 模型元选项 - 定义模型在 admin 中的显示和排序
'verbose_name': '用户', # 单数显示名称
'verbose_name_plural': '用户', # 复数显示名称
'ordering': ['-id'], # 按 ID 降序排列
'get_latest_by': 'id', # 获取最新记录的依据字段
},
managers=[
# 模型管理器 - 使用 Django 默认的用户管理器
('objects', django.contrib.auth.models.UserManager()),
],
),
]

@ -0,0 +1,81 @@
#hyt:
# Generated by Django 4.2.5 on 2023-09-06 13:13
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
"""
BlogUser 模型结构调整迁移文件
accounts 应用的 BlogUser 模型进行字段优化和国际化改进
1. 调整时间字段命名统一使用英文命名规范
2. 更新模型选项改进 Admin 后台显示
3. 字段标签国际化为多语言支持做准备
"""
dependencies = [
('accounts', '0001_initial'), # 依赖于初始迁移文件
]
operations = [
# 模型选项调整 - 更新 Admin 后台显示配置
migrations.AlterModelOptions(
name='bloguser',
options={
'get_latest_by': 'id', # 按 ID 获取最新记录
'ordering': ['-id'], # 按 ID 降序排列
'verbose_name': 'user', # 单数显示名称(英文)
'verbose_name_plural': 'user', # 复数显示名称(英文)
},
),
# 字段清理 - 移除旧的时间字段
migrations.RemoveField(
model_name='bloguser',
name='created_time', # 移除旧的创建时间字段
),
migrations.RemoveField(
model_name='bloguser',
name='last_mod_time', # 移除旧的修改时间字段
),
# 字段添加 - 新增标准化时间字段
migrations.AddField(
model_name='bloguser',
name='creation_time',
field=models.DateTimeField(
default=django.utils.timezone.now, # 默认值为当前时间
verbose_name='creation time' # 字段显示名称(英文)
),
),
migrations.AddField(
model_name='bloguser',
name='last_modify_time',
field=models.DateTimeField(
default=django.utils.timezone.now, # 默认值为当前时间
verbose_name='last modify time' # 字段显示名称(英文)
),
),
# 字段调整 - 更新字段标签为英文
migrations.AlterField(
model_name='bloguser',
name='nickname',
field=models.CharField(
blank=True, # 允许为空
max_length=100, # 最大长度100字符
verbose_name='nick name' # 字段显示名称(英文)
),
),
migrations.AlterField(
model_name='bloguser',
name='source',
field=models.CharField(
blank=True, # 允许为空
max_length=100, # 最大长度100字符
verbose_name='create source' # 字段显示名称(英文)
),
),
]

@ -0,0 +1,81 @@
#hyt:
from django.contrib.auth.models import AbstractUser
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
# 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
用于生成用户个人主页的链接支持反向解析
返回:
用户详情页的URL路径
"""
return reverse(
'blog:author_detail', kwargs={
'author_name': self.username})
def __str__(self):
"""
对象字符串表示
在Admin后台和Shell中显示用户的邮箱地址
返回:
用户的邮箱地址
"""
return self.email
def get_full_url(self):
"""
获取用户的完整URL包含域名
生成包含协议和域名的完整用户主页链接
返回:
用户的完整主页URL
"""
site = get_current_site().domain
url = "https://{site}{path}".format(site=site,
path=self.get_absolute_url())
return url
class Meta:
"""
模型元数据配置
定义模型在数据库和Admin后台中的行为
"""
ordering = ['-id'] # 默认按ID降序排列
verbose_name = _('user') # 单数显示名称
verbose_name_plural = verbose_name # 复数显示名称(与单数相同)
get_latest_by = 'id' # 获取最新记录的依据字段

@ -0,0 +1,296 @@
#hyt:
from django.test import Client, RequestFactory, TestCase
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
# Create your tests here.
class AccountTest(TestCase):
"""
账户功能测试类
测试用户认证注册密码重置等核心账户功能
包含完整的用户生命周期测试用例
"""
def setUp(self):
"""
测试初始化方法
在每个测试方法执行前运行创建测试所需的客户端和用户数据
"""
self.client = Client() # Django 测试客户端
self.factory = RequestFactory() # 请求工厂,用于模拟请求
# 创建测试用户
self.blog_user = BlogUser.objects.create_user(
username="test",
email="admin@admin.com",
password="12345678"
)
self.new_test = "xxx123--=" # 测试用的新密码
def test_validate_account(self):
"""
测试账户验证功能
验证超级用户创建登录文章管理权限等核心功能
包括管理员权限验证和文章操作测试
"""
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)
# 测试管理员后台访问权限
response = self.client.get('/admin/')
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.save()
# 测试文章管理页面访问
response = self.client.get(article.get_admin_url())
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.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])
# 验证退出后权限失效
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)
# 测试正确验证码验证
err = utils.verify("admin@admin.com", code)
self.assertEqual(err, None)
# 测试错误邮箱验证
err = utils.verify("admin@123.com", code)
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")
)
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"), "错误的邮箱")
# 测试错误邮箱格式
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict(email="admin@com")
)
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)
# 准备密码重置数据
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) # 重定向表示成功
# 验证用户密码是否修改成功
blog_user = BlogUser.objects.filter(
email=self.blog_user.email,
).first() # type: BlogUser
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", # 不存在的邮箱
code="123456",
)
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
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)
# 使用错误的验证码
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email=self.blog_user.email,
code="111111", # 错误的验证码
)
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
self.assertEqual(resp.status_code, 200) # 应返回错误页面

@ -0,0 +1,50 @@
#hyt:
from django.urls import path
from django.urls import re_path
from . import views
from .forms import LoginForm
# 应用命名空间 - 用于URL反向解析时区分不同应用的URL
app_name = "accounts"
# URL模式配置 - 定义账户管理相关的所有路由规则
urlpatterns = [
# 用户登录路由
# 使用正则表达式匹配 /login/ 路径
re_path(r'^login/$',
views.LoginView.as_view(success_url='/'), # 登录成功后跳转到首页
name='login', # URL名称用于反向解析
kwargs={'authentication_form': LoginForm}), # 传入自定义登录表单
# 用户注册路由
# 使用正则表达式匹配 /register/ 路径
re_path(r'^register/$',
views.RegisterView.as_view(success_url="/"), # 注册成功后跳转到首页
name='register'), # URL名称用于反向解析
# 用户退出登录路由
# 使用正则表达式匹配 /logout/ 路径
re_path(r'^logout/$',
views.LogoutView.as_view(), # 使用Django内置的退出视图
name='logout'), # URL名称用于反向解析
# 账户操作结果页面路由
# 使用path匹配固定路径 /account/result.html
path(r'account/result.html',
views.account_result, # 函数视图,显示账户操作结果
name='result'), # URL名称用于反向解析
# 忘记密码页面路由
# 使用正则表达式匹配 /forget_password/ 路径
re_path(r'^forget_password/$',
views.ForgetPasswordView.as_view(), # 类视图,处理密码重置
name='forget_password'), # URL名称用于反向解析
# 忘记密码验证码请求路由
# 使用正则表达式匹配 /forget_password_code/ 路径
re_path(r'^forget_password_code/$',
views.ForgetPasswordEmailCode.as_view(), # 类视图,发送验证码
name='forget_password_code'), # URL名称用于反向解析
]

@ -0,0 +1,67 @@
#hyt:
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
class EmailOrUsernameModelBackend(ModelBackend):
"""
自定义认证后端 - 支持邮箱或用户名登录
扩展 Django 原生认证系统允许用户使用用户名或邮箱地址进行登录
提供更灵活的用户认证方式提升用户体验
"""
def authenticate(self, request, username=None, password=None, **kwargs):
"""
用户认证方法
根据输入的用户名判断是邮箱还是用户名并进行相应的认证处理
支持两种登录方式
- 用户名登录username = "admin"
- 邮箱登录username = "admin@example.com"
参数:
request: HttpRequest 对象
username: 用户输入的用户名或邮箱地址
password: 用户输入的密码
**kwargs: 其他关键字参数
返回:
User 对象: 认证成功时返回用户对象
None: 认证失败时返回 None
"""
# 根据输入内容判断是邮箱还是用户名
if '@' in username:
kwargs = {'email': username} # 按邮箱查询
else:
kwargs = {'username': username} # 按用户名查询
try:
# 根据查询条件获取用户对象
user = get_user_model().objects.get(**kwargs)
# 验证密码是否正确
if user.check_password(password):
return user
except get_user_model().DoesNotExist:
# 用户不存在时返回 None
return None
def get_user(self, user_id):
"""
根据用户ID获取用户对象
用于会话认证期间从用户ID获取用户对象
保持与 Django 原生认证后端的兼容性
参数:
user_id: 用户的主键ID
返回:
User 对象: 用户存在时返回用户对象
None: 用户不存在时返回 None
"""
try:
return get_user_model().objects.get(pk=user_id)
except get_user_model().DoesNotExist:
return None

@ -0,0 +1,85 @@
#hyt:
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 djangoblog.utils import send_email
# 验证码有效期配置 - 5分钟过期时间
_code_ttl = timedelta(minutes=5)
def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email")):
"""
发送邮箱验证邮件
用于密码重置邮箱验证等场景向指定邮箱发送包含验证码的邮件
邮件内容支持国际化验证码有效期为5分钟
参数:
to_mail: 接收邮件的邮箱地址
code: 需要发送的验证码
subject: 邮件主题默认为"Verify Email"
"""
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]:
"""
验证验证码是否有效
检查用户输入的验证码与缓存中的验证码是否匹配
用于验证邮箱验证码的正确性
参数:
email: 请求验证的邮箱地址
code: 用户输入的验证码
返回:
str: 验证失败时返回错误信息
None: 验证成功时返回None
注意:
这里的错误处理不太合理应该采用raise抛出异常
否则调用方也需要对error进行处理增加了调用复杂度
"""
cache_code = get_code(email)
if cache_code != code:
return gettext("Verification code error")
def set_code(email: str, code: str):
"""
设置验证码到缓存
将验证码与邮箱关联并存储到缓存中设置5分钟的有效期
参数:
email: 邮箱地址作为缓存的键
code: 验证码作为缓存的值
"""
cache.set(email, code, _code_ttl.seconds)
def get_code(email: str) -> typing.Optional[str]:
"""
从缓存获取验证码
根据邮箱地址从缓存中获取对应的验证码
如果验证码不存在或已过期返回None
参数:
email: 邮箱地址作为缓存的键
返回:
str: 找到的验证码
None: 验证码不存在或已过期
"""
return cache.get(email)

@ -0,0 +1,316 @@
#hyt:
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.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__)
# Create your views here.
class RegisterView(FormView):
"""
用户注册视图
处理新用户注册流程包括表单验证用户创建和邮箱验证邮件发送
注册后用户处于未激活状态需要邮箱验证后才能登录
"""
form_class = RegisterForm # 注册表单类
template_name = 'account/registration_form.html' # 注册页面模板
@method_decorator(csrf_protect)
def dispatch(self, *args, **kwargs):
"""
请求分发方法
添加CSRF保护装饰器防止跨站请求伪造攻击
"""
return super(RegisterView, self).dispatch(*args, **kwargs)
def form_valid(self, form):
"""
表单验证通过处理
保存用户信息发送邮箱验证邮件跳转到结果页面
"""
if form.is_valid():
# 保存用户信息但不立即提交到数据库
user = form.save(False)
user.is_active = False # 设置用户为未激活状态
user.source = 'Register' # 标记用户来源为注册
user.save(True) # 保存到数据库
# 生成邮箱验证链接
site = get_current_site().domain
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
# 开发环境下使用本地地址
if settings.DEBUG:
site = '127.0.0.1:8000'
path = reverse('account:result')
url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format(
site=site, path=path, id=user.id, sign=sign)
# 构建邮件内容
content = """
<p>请点击下面链接验证您的邮箱</p>
<a href="{url}" rel="bookmark">{url}</a>
再次感谢您
<br />
如果上面链接无法打开请将此链接复制至浏览器
{url}
""".format(url=url)
# 发送验证邮件
send_email(
emailto=[
user.email,
],
title='验证您的电子邮箱',
content=content)
# 跳转到注册结果页面
url = reverse('accounts:result') + \
'?type=register&id=' + str(user.id)
return HttpResponseRedirect(url)
else:
# 表单验证失败,重新渲染表单页面
return self.render_to_response({
'form': form
})
class LogoutView(RedirectView):
"""
用户退出登录视图
处理用户退出登录流程清理会话和缓存重定向到登录页面
"""
url = '/login/' # 退出后重定向的URL
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
"""
请求分发方法
添加不缓存装饰器确保退出后页面不被缓存
"""
return super(LogoutView, self).dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
"""
GET请求处理
执行退出登录操作清理用户会话和侧边栏缓存
"""
logout(request) # Django内置退出登录方法
delete_sidebar_cache() # 清理侧边栏缓存
return super(LogoutView, self).get(request, *args, **kwargs)
class LoginView(FormView):
"""
用户登录视图
处理用户登录认证支持记住登录状态和重定向功能
"""
form_class = LoginForm # 登录表单类
template_name = 'account/login.html' # 登录页面模板
success_url = '/' # 登录成功默认跳转URL
redirect_field_name = REDIRECT_FIELD_NAME # 重定向字段名
login_ttl = 2626560 # 记住登录状态的会话有效期(一个月)
@method_decorator(sensitive_post_parameters('password')) # 保护密码参数
@method_decorator(csrf_protect) # CSRF保护
@method_decorator(never_cache) # 禁止缓存
def dispatch(self, request, *args, **kwargs):
"""
请求分发方法
添加安全相关的装饰器保护登录过程的安全性
"""
return super(LoginView, self).dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
"""
获取模板上下文数据
处理重定向参数确保登录后能正确跳转
"""
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)
def form_valid(self, form):
"""
表单验证通过处理
执行用户登录认证处理记住登录状态选项
"""
form = AuthenticationForm(data=self.request.POST, request=self.request)
if form.is_valid():
# 登录成功,清理缓存并记录日志
delete_sidebar_cache()
logger.info(self.redirect_field_name)
# 执行登录操作
auth.login(self.request, form.get_user())
# 处理"记住我"选项
if self.request.POST.get("remember"):
self.request.session.set_expiry(self.login_ttl) # 设置会话有效期
return super(LoginView, self).form_valid(form)
else:
# 登录失败,重新显示表单
return self.render_to_response({
'form': form
})
def get_success_url(self):
"""
获取登录成功后的跳转URL
验证重定向URL的安全性防止开放重定向攻击
"""
redirect_to = self.request.POST.get(self.redirect_field_name)
# 验证重定向URL是否安全
if not url_has_allowed_host_and_scheme(
url=redirect_to, allowed_hosts=[
self.request.get_host()]):
redirect_to = self.success_url # 不安全则使用默认URL
return redirect_to
def account_result(request):
"""
账户操作结果页面视图
显示注册成功或邮箱验证成功的结果信息
处理邮箱验证链接的验证逻辑
"""
type = request.GET.get('type') # 操作类型register或validation
id = request.GET.get('id') # 用户ID
user = get_object_or_404(get_user_model(), id=id)
logger.info(type)
# 如果用户已激活,直接跳转到首页
if user.is_active:
return HttpResponseRedirect('/')
# 处理注册结果或邮箱验证
if type and type in ['register', 'validation']:
if type == 'register':
# 注册成功页面
content = '''
恭喜您注册成功一封验证邮件已经发送到您的邮箱请验证您的邮箱后登录本站
'''
title = '注册成功'
else:
# 邮箱验证处理
c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
sign = request.GET.get('sign')
# 验证签名防止篡改
if sign != c_sign:
return HttpResponseForbidden()
# 激活用户账户
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' # 模板路径
def form_valid(self, form):
"""
表单验证通过处理
重置用户密码并重定向到登录页面
"""
if form.is_valid():
# 获取用户并更新密码
blog_user = BlogUser.objects.filter(email=form.cleaned_data.get("email")).get()
blog_user.password = make_password(form.cleaned_data["new_password2"]) # 哈希密码
blog_user.save()
return HttpResponseRedirect('/login/') # 重定向到登录页面
else:
# 表单验证失败,重新显示表单
return self.render_to_response({'form': form})
class ForgetPasswordEmailCode(View):
"""
忘记密码验证码发送视图
处理忘记密码流程中的验证码发送请求
"""
def post(self, request: HttpRequest):
"""
POST请求处理
验证邮箱地址并发送密码重置验证码
"""
form = ForgetPasswordCodeForm(request.POST)
if not form.is_valid():
return HttpResponse("错误的邮箱") # 邮箱格式错误
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") # 发送成功
Loading…
Cancel
Save