diff --git a/src/DjangoBlog/accounts/admin.py b/src/DjangoBlog/accounts/admin.py
index 29d162a..fef734b 100644
--- a/src/DjangoBlog/accounts/admin.py
+++ b/src/DjangoBlog/accounts/admin.py
@@ -1,60 +1,152 @@
+"""
+Django管理后台用户模型配置模块
+
+本模块配置BlogUser模型在Django管理后台的显示和编辑行为,
+包括自定义表单验证、列表显示字段、搜索过滤等功能。
+
+主要组件:
+- BlogUserCreationForm: 用户创建表单,处理密码验证和设置
+- BlogUserChangeForm: 用户信息修改表单
+- BlogUserAdmin: 用户模型管理后台配置类
+"""
+
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):
+ """
+ 博客用户创建表单
+
+ 扩展自ModelForm,专门用于在Django管理后台创建新用户。
+ 提供密码确认验证和密码哈希处理功能。
+ """
+
+ # 密码输入字段1 - 使用PasswordInput控件隐藏输入
password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput)
+ # 密码输入字段2 - 用于密码确认
password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput)
class Meta:
+ """表单元数据配置"""
+ # 指定关联的模型
model = BlogUser
+ # 指定表单中包含的字段 - 仅包含email字段
fields = ('email',)
def clean_password2(self):
- # Check that the two password entries match
+ """
+ 密码确认字段验证方法
+
+ 验证两次输入的密码是否一致,确保用户输入正确的密码。
+
+ Returns:
+ str: 验证通过的密码
+
+ Raises:
+ forms.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):
- # Save the provided password in hashed format
+ """
+ 表单保存方法
+
+ 重写保存逻辑,处理密码哈希化和设置用户来源。
+
+ Args:
+ commit (bool): 是否立即保存到数据库,默认为True
+
+ Returns:
+ BlogUser: 保存后的用户实例
+ """
+ # 调用父类保存方法,但不立即提交到数据库
user = super().save(commit=False)
+ # 使用Django的密码哈希方法设置密码
user.set_password(self.cleaned_data["password1"])
+
+ # 如果设置为立即提交,则保存用户并设置来源
if commit:
+ # 标记用户创建来源为管理后台
user.source = 'adminsite'
+ # 保存用户到数据库
user.save()
+
return user
class BlogUserChangeForm(UserChangeForm):
+ """
+ 博客用户信息修改表单
+
+ 继承自Django的UserChangeForm,用于在管理后台编辑现有用户信息。
+ 保持与Django原生用户管理表单的兼容性。
+ """
+
class Meta:
+ """表单元数据配置"""
+ # 指定关联的模型
model = BlogUser
+ # 包含所有字段
fields = '__all__'
+ # 指定用户名字段使用Django的UsernameField类型
field_classes = {'username': UsernameField}
def __init__(self, *args, **kwargs):
+ """
+ 表单初始化方法
+
+ 可以在此处添加自定义的表单初始化逻辑。
+ """
+ # 调用父类初始化方法
super().__init__(*args, **kwargs)
class BlogUserAdmin(UserAdmin):
+ """
+ 博客用户管理后台配置类
+
+ 继承自Django的UserAdmin,自定义BlogUser模型在管理后台的显示和行为。
+ 配置列表显示、搜索、排序等管理界面功能。
+ """
+
+ # 指定用户编辑表单
form = BlogUserChangeForm
+ # 指定用户创建表单
add_form = BlogUserCreationForm
+
+ # 配置列表页面显示的字段
list_display = (
- 'id',
- 'nickname',
- 'username',
- 'email',
- 'last_login',
- 'date_joined',
- 'source')
+ 'id', # 用户ID
+ 'nickname', # 用户昵称
+ 'username', # 用户名
+ 'email', # 邮箱地址
+ 'last_login', # 最后登录时间
+ 'date_joined', # 注册时间
+ 'source' # 用户来源
+ )
+
+ # 配置列表中可点击跳转到编辑页面的字段
list_display_links = ('id', 'username')
+
+ # 配置默认排序规则 - 按ID倒序排列(最新的在前)
ordering = ('-id',)
- search_fields = ('username', 'nickname', 'email')
+
+ # 配置搜索框可搜索的字段
+ search_fields = ('username', 'nickname', 'email')
\ No newline at end of file
diff --git a/src/DjangoBlog/accounts/apps.py b/src/DjangoBlog/accounts/apps.py
index 9b3fc5a..9e1f7e0 100644
--- a/src/DjangoBlog/accounts/apps.py
+++ b/src/DjangoBlog/accounts/apps.py
@@ -1,5 +1,37 @@
+"""
+Django应用配置模块
+
+本模块定义accounts应用的配置类,用于配置应用级别的设置和元数据。
+"""
+
from django.apps import AppConfig
class AccountsConfig(AppConfig):
+ """
+ 用户账户应用配置类
+
+ 继承自Django的AppConfig类,用于配置accounts应用的各项设置。
+ 包括应用名称、显示名称、初始化逻辑等。
+
+ 属性:
+ name (str): 应用的Python路径标识符,Django使用此名称来识别应用
+ """
+
+ # 应用名称 - 使用Python路径格式,Django通过此名称识别应用
+ # 此名称必须与应用的目录名和INSTALLED_APPS中的配置一致
name = 'accounts'
+
+ # 可选:应用的可读名称(用于Django管理后台显示)
+ # verbose_name = '用户账户'
+
+ # 可选:应用初始化方法
+ # def ready(self):
+ # """
+ # 应用初始化完成时调用的方法
+ #
+ # 当Django启动完成,所有应用加载完毕后会自动调用此方法。
+ # 常用于信号注册、配置检查等初始化操作。
+ # """
+ # # 导入并注册信号处理器
+ # from . import signals
\ No newline at end of file
diff --git a/src/DjangoBlog/accounts/forms.py b/src/DjangoBlog/accounts/forms.py
index fce4137..de03466 100644
--- a/src/DjangoBlog/accounts/forms.py
+++ b/src/DjangoBlog/accounts/forms.py
@@ -1,3 +1,15 @@
+"""
+用户认证表单模块
+
+本模块定义用户相关的Django表单,包括:
+- 用户登录表单
+- 用户注册表单
+- 密码重置表单
+- 验证码表单
+
+所有表单都包含Bootstrap样式类,提供一致的用户界面体验。
+"""
+
from django import forms
from django.contrib.auth import get_user_model, password_validation
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
@@ -9,109 +21,254 @@ from .models import BlogUser
class LoginForm(AuthenticationForm):
+ """
+ 用户登录表单
+
+ 继承自Django的AuthenticationForm,添加Bootstrap样式支持。
+ 用于用户通过用户名和密码登录系统。
+ """
+
def __init__(self, *args, **kwargs):
+ """
+ 初始化表单,设置字段的widget属性添加Bootstrap样式
+
+ Args:
+ *args: 可变位置参数
+ **kwargs: 可变关键字参数
+ """
+ # 调用父类初始化方法
super(LoginForm, self).__init__(*args, **kwargs)
+
+ # 设置用户名字段的输入控件和样式
self.fields['username'].widget = widgets.TextInput(
- attrs={'placeholder': "username", "class": "form-control"})
+ attrs={
+ 'placeholder': "username", # 输入框占位符文本
+ "class": "form-control" # Bootstrap表单控件样式类
+ })
+
+ # 设置密码字段的输入控件和样式
self.fields['password'].widget = widgets.PasswordInput(
- attrs={'placeholder': "password", "class": "form-control"})
+ attrs={
+ 'placeholder': "password", # 输入框占位符文本
+ "class": "form-control" # Bootstrap表单控件样式类
+ })
class RegisterForm(UserCreationForm):
+ """
+ 用户注册表单
+
+ 继承自Django的UserCreationForm,扩展邮箱字段和样式支持。
+ 用于新用户注册账号,包含用户名、邮箱和密码确认功能。
+ """
+
def __init__(self, *args, **kwargs):
+ """
+ 初始化表单,设置所有字段的widget属性添加Bootstrap样式
+
+ Args:
+ *args: 可变位置参数
+ **kwargs: 可变关键字参数
+ """
+ # 调用父类初始化方法
super(RegisterForm, self).__init__(*args, **kwargs)
+ # 设置用户名字段的输入控件和样式
self.fields['username'].widget = widgets.TextInput(
- attrs={'placeholder': "username", "class": "form-control"})
+ attrs={
+ 'placeholder': "username", # 输入框占位符文本
+ "class": "form-control" # Bootstrap表单控件样式类
+ })
+
+ # 设置邮箱字段的输入控件和样式
self.fields['email'].widget = widgets.EmailInput(
- attrs={'placeholder': "email", "class": "form-control"})
+ attrs={
+ 'placeholder': "email", # 输入框占位符文本
+ "class": "form-control" # Bootstrap表单控件样式类
+ })
+
+ # 设置密码字段的输入控件和样式
self.fields['password1'].widget = widgets.PasswordInput(
- attrs={'placeholder': "password", "class": "form-control"})
+ attrs={
+ 'placeholder': "password", # 输入框占位符文本
+ "class": "form-control" # Bootstrap表单控件样式类
+ })
+
+ # 设置密码确认字段的输入控件和样式
self.fields['password2'].widget = widgets.PasswordInput(
- attrs={'placeholder': "repeat password", "class": "form-control"})
+ attrs={
+ 'placeholder': "repeat password", # 输入框占位符文本
+ "class": "form-control" # Bootstrap表单控件样式类
+ })
def clean_email(self):
+ """
+ 邮箱字段验证方法
+
+ 验证邮箱是否已被注册,确保邮箱地址的唯一性。
+
+ Returns:
+ str: 验证通过的邮箱地址
+
+ Raises:
+ 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):
+ """
+ 忘记密码重置表单
+
+ 用于用户通过邮箱和验证码重置密码,包含密码强度验证和验证码校验。
+ """
+
+ # 新密码字段1
new_password1 = forms.CharField(
- label=_("New password"),
+ label=_("New password"), # 字段标签
widget=forms.PasswordInput(
attrs={
- "class": "form-control",
- 'placeholder': _("New password")
+ "class": "form-control", # Bootstrap样式类
+ 'placeholder': _("New password") # 占位符文本
}
),
)
+ # 新密码字段2 - 用于密码确认
new_password2 = forms.CharField(
- label="确认密码",
+ label="确认密码", # 中文标签
widget=forms.PasswordInput(
attrs={
- "class": "form-control",
- 'placeholder': _("Confirm password")
+ "class": "form-control", # Bootstrap样式类
+ 'placeholder': _("Confirm password") # 占位符文本
}
),
)
+ # 邮箱字段 - 用于标识用户和发送验证码
email = forms.EmailField(
- label='邮箱',
+ label='邮箱', # 中文标签
widget=forms.TextInput(
attrs={
- 'class': 'form-control',
- 'placeholder': _("Email")
+ 'class': 'form-control', # Bootstrap样式类
+ 'placeholder': _("Email") # 占位符文本
}
),
)
+ # 验证码字段 - 用于验证用户身份
code = forms.CharField(
- label=_('Code'),
+ label=_('Code'), # 字段标签
widget=forms.TextInput(
attrs={
- 'class': 'form-control',
- 'placeholder': _("Code")
+ 'class': 'form-control', # Bootstrap样式类
+ 'placeholder': _("Code") # 占位符文本
}
),
)
def clean_new_password2(self):
+ """
+ 密码确认字段验证方法
+
+ 验证两次输入的密码是否一致,并检查密码强度。
+
+ Returns:
+ str: 验证通过的密码
+
+ Raises:
+ 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"))
+
+ # 使用Django内置密码验证器验证密码强度
password_validation.validate_password(password2)
+ # 返回验证通过的密码
return password2
def clean_email(self):
+ """
+ 邮箱字段验证方法
+
+ 验证邮箱是否在系统中注册过。
+
+ Returns:
+ str: 验证通过的邮箱地址
+
+ Raises:
+ ValidationError: 当邮箱未注册时抛出验证错误
+ """
+ # 获取清洗后的邮箱数据
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():
+ # TODO: 这里会暴露邮箱是否注册的信息,根据安全需求可修改提示
raise ValidationError(_("email does not exist"))
+
return user_email
def clean_code(self):
+ """
+ 验证码字段验证方法
+
+ 验证邮箱和验证码的匹配关系。
+
+ Returns:
+ str: 验证通过的验证码
+
+ Raises:
+ ValidationError: 当验证码无效或过期时抛出验证错误
+ """
+ # 获取清洗后的验证码数据
code = self.cleaned_data.get("code")
+
+ # 调用utils模块的verify函数验证验证码
error = utils.verify(
- email=self.cleaned_data.get("email"),
- code=code,
+ email=self.cleaned_data.get("email"), # 邮箱地址
+ code=code, # 验证码
)
+
+ # 如果验证返回错误信息,抛出验证错误
if error:
raise ValidationError(error)
+
return code
class ForgetPasswordCodeForm(forms.Form):
+ """
+ 忘记密码验证码请求表单
+
+ 用于用户请求发送密码重置验证码,仅包含邮箱字段。
+ """
+
+ # 邮箱字段 - 用于发送验证码
email = forms.EmailField(
- label=_('Email'),
- )
+ label=_('Email'), # 字段标签
+ # 可以添加widget配置来设置样式
+ )
\ No newline at end of file
diff --git a/src/DjangoBlog/accounts/migrations/0001_initial.py b/src/DjangoBlog/accounts/migrations/0001_initial.py
index 88f5173..7b19380 100644
--- a/src/DjangoBlog/accounts/migrations/0001_initial.py
+++ b/src/DjangoBlog/accounts/migrations/0001_initial.py
@@ -25,81 +25,113 @@ class Migration(migrations.Migration):
继承自migrations.Migration,定义自定义用户模型的数据库表创建操作。
initial = True 表示这是该应用的第一个迁移文件。
+
+ 主要功能:
+ - 创建自定义用户模型BlogUser的数据库表
+ - 继承Django认证系统的所有基础字段
+ - 添加博客系统特有的自定义字段
+ - 设置模型的管理器和配置选项
"""
+ # 标记为初始迁移文件,Django迁移系统会首先执行此文件
initial = True
+ # 定义迁移依赖关系
dependencies = [
# 声明对Django认证系统组的依赖
+ # 使用auth应用的0012迁移文件,确保用户权限系统正常工作
('auth', '0012_alter_user_first_name_max_length'),
]
+ # 定义迁移操作序列
operations = [
- # 创建博客用户表
+ # 创建博客用户表的迁移操作
migrations.CreateModel(
+ # 模型名称 - 对应数据库表名 accounts_bloguser
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='修改时间')),
+
# 来源字段 - 博客系统自定义字段,记录用户创建来源(如注册、OAuth等)
('source', models.CharField(blank=True, max_length=100, verbose_name='创建来源')),
+
# 组关联字段 - Django权限系统的组多对多关联
('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')),
+
# 权限关联字段 - Django权限系统的用户权限多对多关联
('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={
- # 管理后台显示名称(中文)
+ # 管理后台单数显示名称(中文)
'verbose_name': '用户',
+ # 管理后台复数显示名称(中文)
'verbose_name_plural': '用户',
- # 默认排序规则 - 按ID倒序排列
+ # 默认排序规则 - 按ID倒序排列(最新的记录在前)
'ordering': ['-id'],
- # 指定获取最新记录的字段
+ # 指定获取最新记录的字段 - 使用id字段确定最新记录
'get_latest_by': 'id',
},
+ # 定义模型管理器
managers=[
# 使用Django内置的UserManager管理用户对象
+ # 提供create_user、create_superuser等用户管理方法
('objects', django.contrib.auth.models.UserManager()),
],
),
diff --git a/src/DjangoBlog/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py b/src/DjangoBlog/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py
index 1a9f509..825a10e 100644
--- a/src/DjangoBlog/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py
+++ b/src/DjangoBlog/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py
@@ -1,3 +1,14 @@
+"""
+用户账户应用数据库迁移文件 - 字段优化更新
+
+本迁移文件对初始用户模型进行字段优化和国际化改进:
+- 重命名时间字段,使用更清晰的英文命名
+- 更新字段显示名称,统一使用英文verbose_name
+- 移除冗余字段,优化数据库结构
+
+这是对0001_initial迁移的后续更新,依赖于初始迁移创建的表结构。
+"""
+
# Generated by Django 4.2.5 on 2023-09-06 13:13
from django.db import migrations, models
@@ -5,42 +16,85 @@ import django.utils.timezone
class Migration(migrations.Migration):
+ """
+ 用户模型字段优化迁移类
+ 对BlogUser模型进行字段级别的优化和改进:
+ - 标准化字段命名约定
+ - 改进国际化支持
+ - 优化时间字段的语义清晰度
+
+ 此迁移依赖于accounts应用的0001_initial迁移文件。
+ """
+
+ # 定义迁移依赖关系 - 依赖于本应用的初始迁移
dependencies = [
+ # 依赖accounts应用的第一个迁移文件,确保BlogUser表已创建
('accounts', '0001_initial'),
]
+ # 定义迁移操作序列 - 按顺序执行以下数据库变更
operations = [
+ # 修改模型选项 - 更新管理后台显示名称
migrations.AlterModelOptions(
name='bloguser',
- options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'user', 'verbose_name_plural': 'user'},
+ options={
+ # 指定获取最新记录的字段 - 保持使用id字段
+ 'get_latest_by': 'id',
+ # 保持默认排序规则 - 按ID倒序排列
+ 'ordering': ['-id'],
+ # 更新单数显示名称为英文
+ 'verbose_name': 'user',
+ # 更新复数显示名称为英文
+ 'verbose_name_plural': 'user'
+ },
),
+
+ # 移除字段 - 删除created_time字段
+ # 该字段功能被creation_time字段替代
migrations.RemoveField(
model_name='bloguser',
name='created_time',
),
+
+ # 移除字段 - 删除last_mod_time字段
+ # 该字段功能被last_modify_time字段替代
migrations.RemoveField(
model_name='bloguser',
name='last_mod_time',
),
+
+ # 添加新字段 - 创建时间字段(新命名)
migrations.AddField(
model_name='bloguser',
name='creation_time',
+ # 使用DateTimeField存储完整的时间戳
+ # default参数使用Django的时区感知当前时间
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
+
+ # 添加新字段 - 最后修改时间字段(新命名)
migrations.AddField(
model_name='bloguser',
name='last_modify_time',
+ # 使用DateTimeField存储完整的时间戳
+ # default参数使用Django的时区感知当前时间
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
),
+
+ # 修改字段选项 - 更新昵称字段的显示名称
migrations.AlterField(
model_name='bloguser',
name='nickname',
+ # 保持字段类型和约束不变,仅更新verbose_name为英文
field=models.CharField(blank=True, max_length=100, verbose_name='nick name'),
),
+
+ # 修改字段选项 - 更新来源字段的显示名称
migrations.AlterField(
model_name='bloguser',
name='source',
+ # 保持字段类型和约束不变,仅更新verbose_name为英文
field=models.CharField(blank=True, max_length=100, verbose_name='create source'),
),
- ]
+ ]
\ No newline at end of file
diff --git a/src/DjangoBlog/accounts/models.py b/src/DjangoBlog/accounts/models.py
index 3baddbb..6fb5cb0 100644
--- a/src/DjangoBlog/accounts/models.py
+++ b/src/DjangoBlog/accounts/models.py
@@ -1,3 +1,10 @@
+"""
+自定义用户模型模块
+
+本模块定义博客系统的自定义用户模型BlogUser,扩展Django内置的AbstractUser模型,
+添加博客系统特有的用户字段和方法。
+"""
+
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.urls import reverse
@@ -6,30 +13,113 @@ from django.utils.translation import gettext_lazy as _
from djangoblog.utils import get_current_site
-# Create your models here.
-
class BlogUser(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)
+ """
+ 博客系统自定义用户模型
+
+ 继承自Django的AbstractUser,在标准用户模型基础上添加博客系统特有的字段:
+ - 昵称字段
+ - 创建时间字段
+ - 最后修改时间字段
+ - 用户来源字段
+
+ 同时提供获取用户相关URL的便捷方法。
+ """
+
+ # 昵称字段 - 用户的显示名称,可以为空
+ nickname = models.CharField(
+ _('nick name'), # 字段显示名称(支持国际化)
+ max_length=100, # 最大长度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, # 最大长度100字符
+ blank=True # 允许为空(非必填字段)
+ )
def get_absolute_url(self):
+ """
+ 获取用户的绝对URL(相对路径)
+
+ 用于Django的通用视图和模板中生成用户详情页链接。
+
+ Returns:
+ str: 用户详情页的URL路径
+
+ Example:
+ >>> user.get_absolute_url()
+ '/author/admin/'
+ """
+ # 使用reverse函数通过URL名称和参数生成URL路径
return reverse(
- 'blog:author_detail', kwargs={
- 'author_name': self.username})
+ 'blog:author_detail', # URL配置的名称
+ kwargs={
+ 'author_name': self.username # URL参数:作者用户名
+ })
def __str__(self):
+ """
+ 对象字符串表示方法
+
+ 定义模型实例在Django管理后台和shell中的显示内容。
+
+ Returns:
+ str: 用户的邮箱地址
+ """
return self.email
def get_full_url(self):
+ """
+ 获取用户的完整URL(包含域名)
+
+ 生成包含协议和域名的完整用户详情页URL,用于外部链接。
+
+ Returns:
+ str: 完整的用户详情页URL
+
+ Example:
+ >>> user.get_full_url()
+ 'https://example.com/author/admin/'
+ """
+ # 获取当前站点的域名
site = get_current_site().domain
- url = "https://{site}{path}".format(site=site,
- path=self.get_absolute_url())
+ # 生成完整的URL,包含HTTPS协议和域名
+ url = "https://{site}{path}".format(
+ site=site, # 站点域名
+ path=self.get_absolute_url() # 相对路径
+ )
return url
class Meta:
+ """
+ 模型元数据配置类
+
+ 定义模型的数据库表配置和Django管理后台显示选项。
+ """
+
+ # 默认排序规则 - 按ID倒序排列(最新的记录在前)
ordering = ['-id']
+
+ # 管理后台单数显示名称(支持国际化)
verbose_name = _('user')
+
+ # 管理后台复数显示名称 - 使用与单数相同的名称
verbose_name_plural = verbose_name
- get_latest_by = 'id'
+
+ # 指定获取最新记录的字段 - 使用id字段确定最新记录1
+ get_latest_by = 'id'
\ No newline at end of file
diff --git a/src/DjangoBlog/accounts/tests.py b/src/DjangoBlog/accounts/tests.py
index 6893411..5232e2b 100644
--- a/src/DjangoBlog/accounts/tests.py
+++ b/src/DjangoBlog/accounts/tests.py
@@ -1,3 +1,10 @@
+"""
+用户账户应用测试模块
+
+本模块包含用户账户相关的所有测试用例,覆盖用户注册、登录、密码重置、
+邮箱验证等核心功能的测试。
+"""
+
from django.test import Client, RequestFactory, TestCase
from django.urls import reverse
from django.utils import timezone
@@ -9,128 +16,203 @@ from djangoblog.utils import *
from . import utils
-# Create your tests here.
-
class AccountTest(TestCase):
+ """
+ 用户账户功能测试类
+
+ 测试用户账户相关的所有功能,包括:
+ - 用户认证和登录
+ - 用户注册流程
+ - 邮箱验证码功能
+ - 密码重置流程
+ - 权限访问控制
+ """
+
def setUp(self):
+ """
+ 测试初始化方法
+
+ 在每个测试方法执行前运行,创建测试所需的初始数据和环境。
+ """
+ # 创建测试客户端,用于模拟HTTP请求
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--="
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.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.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'
+ '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)
+ self.assertEqual(err, None) # 验证成功应返回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")
@@ -140,32 +222,49 @@ class AccountTest(TestCase):
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()
+ data=dict() # 空数据
)
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
+ # 测试错误格式邮箱提交
resp = self.client.post(
path=reverse("account:forget_password_code"),
- data=dict(email="admin@com")
+ 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)
+ self.assertEqual(resp.status_code, 302) # 重定向响应
# 验证用户密码是否修改成功
blog_user = BlogUser.objects.filter(
@@ -175,10 +274,15 @@ class AccountTest(TestCase):
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 +290,25 @@ 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)
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/DjangoBlog/accounts/urls.py b/src/DjangoBlog/accounts/urls.py
index 107a801..501f7f1 100644
--- a/src/DjangoBlog/accounts/urls.py
+++ b/src/DjangoBlog/accounts/urls.py
@@ -1,28 +1,60 @@
+"""
+用户账户应用URL配置模块
+
+本模块定义用户账户相关的所有URL路由,包括登录、注册、登出、
+密码重置等用户认证相关的端点。
+
+URL模式使用正则表达式和路径转换器来匹配不同的用户操作请求。
+"""
+
from django.urls import path
from django.urls import re_path
+# 导入视图模块
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 = [
+ # 用户登录URL
+ re_path(r'^login/$', # 匹配 /login/ 路径
+ # 使用类视图,设置登录成功后的重定向URL为首页
+ views.LoginView.as_view(success_url='/'),
+ name='login', # URL名称,用于反向解析
+ # 传递额外参数,指定使用自定义登录表单
+ kwargs={'authentication_form': LoginForm}),
+
+ # 用户注册URL
+ re_path(r'^register/$', # 匹配 /register/ 路径
+ # 使用类视图,设置注册成功后的重定向URL为首页
+ views.RegisterView.as_view(success_url="/"),
+ name='register'), # URL名称,用于反向解析
+
+ # 用户登出URL
+ re_path(r'^logout/$', # 匹配 /logout/ 路径
+ # 使用类视图,处理用户登出逻辑
+ views.LogoutView.as_view(),
+ name='logout'), # URL名称,用于反向解析
+
+ # 账户操作结果页面URL
+ path(r'account/result.html', # 匹配 /account/result.html 路径
+ # 使用函数视图,显示账户操作结果(如注册成功、验证结果等)
+ views.account_result,
+ name='result'), # URL名称,用于反向解析
+
+ # 忘记密码页面URL
+ re_path(r'^forget_password/$', # 匹配 /forget_password/ 路径
+ # 使用类视图,处理密码重置请求
+ views.ForgetPasswordView.as_view(),
+ name='forget_password'), # URL名称,用于反向解析
+
+ # 忘记密码验证码请求URL
+ re_path(r'^forget_password_code/$', # 匹配 /forget_password_code/ 路径
+ # 使用类视图,处理发送密码重置验证码的请求
+ views.ForgetPasswordEmailCode.as_view(),
+ name='forget_password_code'), # URL名称,用于反向解析
+]
\ No newline at end of file
diff --git a/src/DjangoBlog/accounts/user_login_backend.py b/src/DjangoBlog/accounts/user_login_backend.py
index 73cdca1..b90f615 100644
--- a/src/DjangoBlog/accounts/user_login_backend.py
+++ b/src/DjangoBlog/accounts/user_login_backend.py
@@ -1,26 +1,91 @@
+"""
+自定义用户认证后端模块
+
+本模块提供扩展的用户认证功能,支持使用用户名或邮箱进行登录。
+扩展了Django标准的ModelBackend认证后端。
+"""
+
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
class EmailOrUsernameModelBackend(ModelBackend):
"""
- 允许使用用户名或邮箱登录
+ 自定义用户认证后端 - 支持用户名或邮箱登录
+
+ 继承自Django的ModelBackend,扩展认证功能:
+ - 允许用户使用用户名或邮箱地址进行登录
+ - 自动检测输入的是用户名还是邮箱格式
+ - 保持与Django原生认证系统的兼容性
+
+ 使用场景:
+ 当用户输入包含'@'符号时,系统将其识别为邮箱进行认证;
+ 否则将其识别为用户名进行认证。
"""
def authenticate(self, request, username=None, password=None, **kwargs):
+ """
+ 用户认证方法
+
+ 重写认证逻辑,支持通过用户名或邮箱进行用户身份验证。
+
+ Args:
+ request: HttpRequest对象,包含请求信息
+ username (str): 用户输入的用户名或邮箱地址
+ password (str): 用户输入的密码
+ **kwargs: 其他关键字参数
+
+ Returns:
+ User: 认证成功的用户对象
+ None: 认证失败返回None
+
+ Example:
+ >>> backend = EmailOrUsernameModelBackend()
+ >>> # 使用用户名认证
+ >>> user = backend.authenticate(request, username='admin', password='password')
+ >>> # 使用邮箱认证
+ >>> user = backend.authenticate(request, username='admin@example.com', password='password')
+ """
+ # 判断输入的是邮箱还是用户名
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, username):
+ """
+ 根据用户ID获取用户对象
+
+ 重写用户获取方法,通过用户ID(主键)获取用户实例。
+
+ Args:
+ username (int/str): 用户的ID(主键值)
+
+ Returns:
+ User: 对应的用户对象
+ None: 用户不存在时返回None
+
+ Note:
+ 这里的参数名username实际上是用户ID,这是为了保持与父类接口一致
+ """
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/DjangoBlog/accounts/utils.py b/src/DjangoBlog/accounts/utils.py
index 4b94bdf..365afb7 100644
--- a/src/DjangoBlog/accounts/utils.py
+++ b/src/DjangoBlog/accounts/utils.py
@@ -1,3 +1,15 @@
+"""
+邮箱验证码工具模块
+
+本模块提供邮箱验证码的生成、发送、验证和缓存管理功能。
+用于用户注册、密码重置等需要邮箱验证的场景。
+
+主要功能:
+- 发送验证码邮件
+- 验证码的存储和读取
+- 验证码有效性验证
+"""
+
import typing
from datetime import timedelta
@@ -7,43 +19,109 @@ 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")):
- """发送重设密码验证码
+ """
+ 发送验证码邮件
+
+ 向指定邮箱发送包含验证码的邮件,用于用户身份验证。
+
Args:
- to_mail: 接受邮箱
- subject: 邮件主题
- code: 验证码
+ to_mail (str): 接收邮件的邮箱地址
+ code (str): 要发送的验证码
+ subject (str): 邮件主题,默认为"Verify Email"
+
+ Example:
+ >>> send_verify_email("user@example.com", "123456")
+ # 向user@example.com发送验证码123456
"""
+ # 构建邮件HTML内容,包含验证码信息
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: 验证码
- Return:
- 如果有错误就返回错误str
- Node:
- 这里的错误处理不太合理,应该采用raise抛出
- 否测调用方也需要对error进行处理
+ email (str): 用户邮箱地址,作为缓存键
+ code (str): 用户输入的验证码
+
+ Returns:
+ typing.Optional[str]:
+ - None: 验证码正确且有效
+ - str: 错误信息字符串(验证码错误或无效)
+
+ Note:
+ 当前错误处理方式不够合理,应该使用异常抛出机制,
+ 这样调用方可以通过try-except处理错误,而不是检查返回值。
+
+ Example:
+ >>> result = verify("user@example.com", "123456")
+ >>> if result:
+ >>> print(f"验证失败: {result}")
+ >>> else:
+ >>> print("验证成功")
"""
+ # 从缓存中获取该邮箱对应的验证码
cache_code = get_code(email)
+
+ # 比较用户输入的验证码与缓存中的验证码
if cache_code != code:
+ # 验证码不匹配,返回错误信息
return gettext("Verification code error")
+ # 验证成功,返回None
+
def set_code(email: str, code: str):
- """设置code"""
+ """
+ 设置验证码到缓存
+
+ 将验证码存储到Django缓存系统中,并设置有效期。
+
+ Args:
+ email (str): 邮箱地址,作为缓存键
+ code (str): 要存储的验证码
+
+ Example:
+ >>> set_code("user@example.com", "123456")
+ # 将验证码123456存储到缓存,键为"user@example.com"
+ """
+ # 使用Django缓存系统存储验证码,设置5分钟有效期
cache.set(email, code, _code_ttl.seconds)
def get_code(email: str) -> typing.Optional[str]:
- """获取code"""
- return cache.get(email)
+ """
+ 从缓存中获取验证码
+
+ 根据邮箱地址从缓存中获取对应的验证码。
+
+ Args:
+ email (str): 邮箱地址,作为缓存键
+
+ Returns:
+ typing.Optional[str]:
+ - str: 找到的验证码
+ - None: 验证码不存在或已过期
+
+ Example:
+ >>> code = get_code("user@example.com")
+ >>> if code:
+ >>> print(f"验证码是: {code}")
+ >>> else:
+ >>> print("验证码不存在或已过期")
+ """
+ # 从Django缓存系统中获取验证码
+ return cache.get(email)
\ No newline at end of file
diff --git a/src/DjangoBlog/accounts/views.py b/src/DjangoBlog/accounts/views.py
index ae67aec..ccef460 100644
--- a/src/DjangoBlog/accounts/views.py
+++ b/src/DjangoBlog/accounts/views.py
@@ -1,3 +1,15 @@
+"""
+用户账户视图模块
+
+本模块包含用户账户相关的所有视图处理逻辑,包括:
+- 用户注册、登录、登出
+- 邮箱验证
+- 密码重置
+- 验证码发送
+
+使用类视图和函数视图结合的方式处理用户认证流程。
+"""
+
import logging
from django.utils.translation import gettext_lazy as _
from django.conf import settings
@@ -26,34 +38,68 @@ 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保护装饰器
+
+ 确保注册请求受到CSRF保护,防止跨站请求伪造攻击。
+ """
return super(RegisterView, self).dispatch(*args, **kwargs)
def form_valid(self, form):
+ """
+ 表单验证通过后的处理逻辑
+
+ 创建新用户、发送验证邮件、重定向到结果页面。
+
+ Args:
+ form: 验证通过的注册表单实例
+
+ Returns:
+ HttpResponseRedirect: 重定向到结果页面
+ """
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'
+
+ # 构建验证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)
+ # 构建邮件内容
content = """
请点击下面链接验证您的邮箱
@@ -64,6 +110,8 @@ class RegisterView(FormView):
如果上面链接无法打开,请将此链接复制至浏览器。
{url}
""".format(url=url)
+
+ # 发送验证邮件
send_email(
emailto=[
user.email,
@@ -71,43 +119,88 @@ class RegisterView(FormView):
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
url = '/login/'
@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)
+ # 清理侧边栏缓存
delete_sidebar_cache()
+ # 调用父类方法进行重定向
return super(LogoutView, self).get(request, *args, **kwargs)
class LoginView(FormView):
+ """
+ 用户登录视图
+
+ 处理用户登录认证,支持记住登录状态功能。
+ """
+
+ # 指定使用的表单类
form_class = LoginForm
+ # 指定登录页面模板
template_name = 'account/login.html'
+ # 登录成功后的默认重定向URL
success_url = '/'
+ # 重定向字段名称
redirect_field_name = REDIRECT_FIELD_NAME
- login_ttl = 2626560 # 一个月的时间
+ # 记住登录状态的会话有效期(一个月)
+ login_ttl = 2626560
@method_decorator(sensitive_post_parameters('password'))
@method_decorator(csrf_protect)
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
+ """
+ 请求分发方法,添加安全装饰器
+ - sensitive_post_parameters: 保护密码参数
+ - csrf_protect: CSRF保护
+ - never_cache: 禁止缓存
+ """
return super(LoginView, self).dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
+ """
+ 获取模板上下文数据
+
+ 添加重定向URL到上下文。
+ """
+ # 从GET参数获取重定向URL
redirect_to = self.request.GET.get(self.redirect_field_name)
if redirect_to is None:
redirect_to = '/'
@@ -116,25 +209,43 @@ class LoginView(FormView):
return super(LoginView, self).get_context_data(**kwargs)
def form_valid(self, form):
+ """
+ 表单验证通过后的处理逻辑
+
+ 执行用户登录认证,处理记住登录状态。
+ """
+ # 使用Django认证表单进行验证
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)
- # return HttpResponseRedirect('/')
else:
+ # 登录失败,重新渲染登录页面
return self.render_to_response({
'form': form
})
def get_success_url(self):
+ """
+ 获取登录成功后的重定向URL
+ 验证重定向URL的安全性,防止开放重定向攻击。
+ """
+ # 从POST数据获取重定向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()]):
@@ -143,62 +254,124 @@ class LoginView(FormView):
def account_result(request):
+ """
+ 账户操作结果页面视图
+
+ 显示注册结果或处理邮箱验证。
+
+ Args:
+ request: HTTP请求对象
+
+ Returns:
+ HttpResponse: 结果页面响应
+ """
+ # 获取操作类型和用户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)
+
+ # 如果用户已激活,重定向到首页
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请求,发送密码重置验证码
+
+ Args:
+ request: HTTP请求对象
+
+ Returns:
+ HttpResponse: 操作结果响应
+ """
+ # 验证表单数据
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")
+ return HttpResponse("ok")
\ No newline at end of file
diff --git a/src/DjangoBlog/oauth/admin.py b/src/DjangoBlog/oauth/admin.py
index 57eab5f..8136cf9 100644
--- a/src/DjangoBlog/oauth/admin.py
+++ b/src/DjangoBlog/oauth/admin.py
@@ -1,54 +1,144 @@
+"""
+Django Admin 管理站点配置模块 - OAuth 认证
+
+该模块用于配置OAuth相关模型在Django Admin管理站点的显示和操作方式。
+包含OAuth用户和OAuth配置两个管理类,用于自定义管理界面。
+"""
+
import logging
+# 导入Django Admin管理模块
from django.contrib import admin
-# Register your models here.
+# 导入URL反向解析功能
from django.urls import reverse
+# 导入HTML格式化工具
from django.utils.html import format_html
+# 获取当前模块的日志记录器
logger = logging.getLogger(__name__)
class OAuthUserAdmin(admin.ModelAdmin):
+ """
+ OAuth用户模型的管理配置类
+
+ 自定义OAuthUser模型在Django Admin中的显示和行为,
+ 包括搜索、列表显示、过滤等功能。
+ """
+
+ # 设置可搜索的字段
search_fields = ('nickname', 'email')
+ # 设置每页显示的项目数量
list_per_page = 20
+ # 设置列表页面显示的字段
list_display = (
- 'id',
- 'nickname',
- 'link_to_usermodel',
- 'show_user_image',
- 'type',
- 'email',
+ 'id', # 用户ID
+ 'nickname', # 昵称
+ 'link_to_usermodel', # 自定义方法:关联本地用户链接
+ 'show_user_image', # 自定义方法:显示用户头像
+ 'type', # OAuth类型
+ 'email', # 邮箱
)
+ # 设置可作为链接点击的字段(跳转到编辑页面)
list_display_links = ('id', 'nickname')
+ # 设置右侧过滤侧边栏的过滤字段
list_filter = ('author', 'type',)
+ # 初始化只读字段列表
readonly_fields = []
def get_readonly_fields(self, request, obj=None):
+ """
+ 动态获取只读字段列表
+
+ 重写方法使所有字段在Admin中均为只读,防止在管理界面修改OAuth用户数据。
+
+ Args:
+ request: HttpRequest对象
+ obj: 模型实例对象
+
+ Returns:
+ list: 包含所有字段名的列表,使所有字段只读
+ """
+ # 返回所有字段名称的列表,包括普通字段和多对多字段
return list(self.readonly_fields) + \
- [field.name for field in obj._meta.fields] + \
- [field.name for field in obj._meta.many_to_many]
+ [field.name for field in obj._meta.fields] + \
+ [field.name for field in obj._meta.many_to_many]
def has_add_permission(self, request):
+ """
+ 禁用添加权限
+
+ 防止在Admin界面手动添加OAuth用户,OAuth用户只能通过认证流程自动创建。
+
+ Args:
+ request: HttpRequest对象
+
+ Returns:
+ bool: 始终返回False,禁止添加新记录
+ """
return False
def link_to_usermodel(self, obj):
+ """
+ 自定义方法:生成关联本地用户的链接
+
+ 在Admin列表中显示关联的本地用户,并提供跳转到用户编辑页面的链接。
+
+ Args:
+ obj: OAuthUser实例对象
+
+ Returns:
+ str: 格式化的HTML链接,包含用户昵称或邮箱显示
+ """
+ # 检查是否存在关联的本地用户
if obj.author:
+ # 获取关联用户模型的app和model信息
info = (obj.author._meta.app_label, obj.author._meta.model_name)
+ # 生成编辑页面的URL
link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,))
+ # 返回格式化的HTML链接,显示用户昵称或邮箱
return format_html(
u'%s' %
(link, obj.author.nickname if obj.author.nickname else obj.author.email))
def show_user_image(self, obj):
+ """
+ 自定义方法:显示用户头像
+
+ 在Admin列表中以缩略图形式显示用户的第三方平台头像。
+
+ Args:
+ obj: OAuthUser实例对象
+
+ Returns:
+ str: 格式化的HTML图片标签
+ """
+ # 获取用户头像URL
img = obj.picture
+ # 返回格式化的HTML图片标签,设置固定尺寸
return format_html(
u'
' %
(img))
- link_to_usermodel.short_description = '用户'
- show_user_image.short_description = '用户头像'
+ # 设置自定义方法在Admin中的显示名称
+ link_to_usermodel.short_description = '用户' # 关联用户列的显示名称
+ show_user_image.short_description = '用户头像' # 用户头像列的显示名称
class OAuthConfigAdmin(admin.ModelAdmin):
- list_display = ('type', 'appkey', 'appsecret', 'is_enable')
- list_filter = ('type',)
+ """
+ OAuth配置模型的管理配置类
+
+ 自定义OAuthConfig模型在Django Admin中的显示方式,
+ 用于管理不同第三方平台的OAuth应用配置。
+ """
+
+ # 设置列表页面显示的字段
+ list_display = (
+ 'type', # OAuth类型
+ 'appkey', # 应用Key
+ 'appsecret', # 应用Secret
+ 'is_enable' # 是否启用
+ )
+ # 设置右侧过滤侧边栏的过滤字段
+ list_filter = ('type',) # 按OAuth类型过滤
\ No newline at end of file
diff --git a/src/DjangoBlog/oauth/forms.py b/src/DjangoBlog/oauth/forms.py
index 0e4ede3..b678125 100644
--- a/src/DjangoBlog/oauth/forms.py
+++ b/src/DjangoBlog/oauth/forms.py
@@ -1,12 +1,46 @@
+"""
+OAuth 认证表单模块 - 邮箱补充表单
+
+该模块定义了在OAuth认证过程中需要用户补充邮箱信息时使用的表单。
+当第三方登录未返回邮箱地址时,使用此表单让用户手动输入邮箱。
+"""
+
+# 导入Django表单相关模块
from django.contrib.auth.forms import forms
from django.forms import widgets
class RequireEmailForm(forms.Form):
+ """
+ 邮箱补充表单类
+
+ 用于OAuth登录过程中,当第三方平台未提供用户邮箱时,
+ 要求用户手动输入邮箱地址的表单。
+ """
+
+ # 邮箱字段,必填字段,用于用户输入电子邮箱
email = forms.EmailField(label='电子邮箱', required=True)
+
+ # 隐藏的OAuth用户ID字段,用于关联OAuth用户记录
oauthid = forms.IntegerField(widget=forms.HiddenInput, required=False)
def __init__(self, *args, **kwargs):
+ """
+ 初始化表单,自定义字段控件属性
+
+ 重写初始化方法,为邮箱字段添加HTML属性和样式类,
+ 改善用户体验和界面美观。
+
+ Args:
+ *args: 可变位置参数
+ **kwargs: 可变关键字参数
+ """
+ # 调用父类的初始化方法
super(RequireEmailForm, self).__init__(*args, **kwargs)
+
+ # 自定义邮箱字段的widget,添加placeholder和CSS类
self.fields['email'].widget = widgets.EmailInput(
- attrs={'placeholder': "email", "class": "form-control"})
+ attrs={
+ 'placeholder': "email", # 输入框内的提示文本
+ "class": "form-control" # Bootstrap样式类,用于表单控件样式
+ })
\ No newline at end of file
diff --git a/src/DjangoBlog/oauth/migrations/0001_initial.py b/src/DjangoBlog/oauth/migrations/0001_initial.py
index bb26801..e8776b0 100644
--- a/src/DjangoBlog/oauth/migrations/0001_initial.py
+++ b/src/DjangoBlog/oauth/migrations/0001_initial.py
@@ -1,4 +1,7 @@
"""
+<<<<<<< HEAD
+Django 数据库迁移模块 - OAuth 认证配置
+=======
OAuth应用数据库迁移文件
本迁移文件由Django自动生成,用于创建OAuth认证相关的数据库表结构。
@@ -13,29 +16,74 @@ OAuth应用数据库迁移文件
"""
# Generated by Django 4.1.7 on 2023-03-07 09:53
+>>>>>>> d4786ee23b15aa002b21504f1056098d46f303c5
-from django.conf import settings
-from django.db import migrations, models
-import django.db.models.deletion
-import django.utils.timezone
+该模块用于创建OAuth认证相关的数据库表结构,包含OAuth服务提供商配置和OAuth用户信息两个主要模型。
+这是Django迁移系统自动生成的迁移文件,在Django 4.1.7版本中创建于2023-03-07。
+"""
+
+# 导入Django核心模块
+from django.conf import settings # 导入Django设置
+from django.db import migrations, models # 导入数据库迁移和模型相关功能
+import django.db.models.deletion # 导入外键删除操作
+import django.utils.timezone # 导入时区工具
class Migration(migrations.Migration):
"""
+<<<<<<< HEAD
+ OAuth认证系统的数据库迁移类
+=======
OAuth应用初始迁移类
继承自migrations.Migration,定义数据库表结构的创建操作。
initial = True 表示这是该应用的第一个迁移文件。
"""
+>>>>>>> d4786ee23b15aa002b21504f1056098d46f303c5
+ 这个迁移类负责创建OAuth认证功能所需的数据库表结构,
+ 包括OAuth服务提供商配置和第三方登录用户信息存储。
+ """
+
+ # 标记为初始迁移
initial = True
+ # 定义依赖关系 - 依赖于可切换的用户模型
dependencies = [
# 声明对Django用户模型的依赖
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
+ # 定义要执行的数据库操作
operations = [
+<<<<<<< HEAD
+ # 创建OAuthConfig模型对应的数据库表
+ migrations.CreateModel(
+ name='OAuthConfig',
+ fields=[
+ # 主键ID字段,自增BigAutoField
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ # OAuth服务类型选择字段,支持多种第三方登录
+ ('type', models.CharField(
+ choices=[('weibo', '微博'), ('google', '谷歌'), ('github', 'GitHub'), ('facebook', 'FaceBook'),
+ ('qq', 'QQ')], default='a', max_length=10, verbose_name='类型')),
+ # OAuth应用的AppKey字段
+ ('appkey', models.CharField(max_length=200, verbose_name='AppKey')),
+ # OAuth应用的AppSecret字段,用于安全认证
+ ('appsecret', models.CharField(max_length=200, verbose_name='AppSecret')),
+ # OAuth回调地址字段,用于接收授权码
+ ('callback_url',
+ models.CharField(default='http://www.baidu.com', max_length=200, verbose_name='回调地址')),
+ # 是否启用该OAuth配置的布尔字段
+ ('is_enable', models.BooleanField(default=True, 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='修改时间')),
+ ],
+ options={
+ # 设置模型在Admin中的单数显示名称
+=======
# 创建OAuth配置表
migrations.CreateModel(
name='OAuthConfig',
@@ -62,8 +110,41 @@ class Migration(migrations.Migration):
],
options={
# 管理后台显示名称
+>>>>>>> d4786ee23b15aa002b21504f1056098d46f303c5
'verbose_name': 'oauth配置',
+ # 设置模型在Admin中的复数显示名称
'verbose_name_plural': 'oauth配置',
+<<<<<<< HEAD
+ # 设置默认排序字段,按创建时间降序排列
+ 'ordering': ['-created_time'],
+ },
+ ),
+ # 创建OAuthUser模型对应的数据库表
+ migrations.CreateModel(
+ name='OAuthUser',
+ fields=[
+ # 主键ID字段,自增BigAutoField
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ # 第三方平台的用户唯一标识
+ ('openid', models.CharField(max_length=50)),
+ # 用户在第三方平台的昵称
+ ('nickname', models.CharField(max_length=50, verbose_name='昵称')),
+ # OAuth访问令牌,可为空
+ ('token', models.CharField(blank=True, max_length=150, null=True)),
+ # 用户头像URL,可为空
+ ('picture', models.CharField(blank=True, max_length=350, null=True)),
+ # OAuth服务类型
+ ('type', models.CharField(max_length=50)),
+ # 用户邮箱,可为空
+ ('email', models.CharField(blank=True, max_length=50, null=True)),
+ # 存储额外的元数据信息,使用Text字段
+ ('metadata', models.TextField(blank=True, null=True)),
+ # 记录创建时间,默认使用当前时间
+ ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
+ # 记录最后修改时间,默认使用当前时间
+ ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
+ # 外键关联到本地用户模型,建立第三方账号与本地用户的关联
+=======
# 默认排序规则 - 按创建时间倒序
'ordering': ['-created_time'],
},
@@ -93,14 +174,24 @@ class Migration(migrations.Migration):
# 最后修改时间
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
# 外键关联到本地用户 - 建立第三方账号与本地账号的关联
+>>>>>>> d4786ee23b15aa002b21504f1056098d46f303c5
('author', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL, verbose_name='用户')),
],
options={
+<<<<<<< HEAD
+ # 设置模型在Admin中的单数显示名称
+=======
# 管理后台显示名称
+>>>>>>> d4786ee23b15aa002b21504f1056098d46f303c5
'verbose_name': 'oauth用户',
+ # 设置模型在Admin中的复数显示名称
'verbose_name_plural': 'oauth用户',
+<<<<<<< HEAD
+ # 设置默认排序字段,按创建时间降序排列
+=======
# 默认排序规则
+>>>>>>> d4786ee23b15aa002b21504f1056098d46f303c5
'ordering': ['-created_time'],
},
),
diff --git a/src/DjangoBlog/oauth/migrations/0002_alter_oauthconfig_options_alter_oauthuser_options_and_more.py b/src/DjangoBlog/oauth/migrations/0002_alter_oauthconfig_options_alter_oauthuser_options_and_more.py
index d5cc70e..091fd59 100644
--- a/src/DjangoBlog/oauth/migrations/0002_alter_oauthconfig_options_alter_oauthuser_options_and_more.py
+++ b/src/DjangoBlog/oauth/migrations/0002_alter_oauthconfig_options_alter_oauthuser_options_and_more.py
@@ -1,86 +1,138 @@
-# Generated by Django 4.2.5 on 2023-09-06 13:13
+"""
+Django 数据库迁移模块 - OAuth 认证配置更新
-from django.conf import settings
-from django.db import migrations, models
-import django.db.models.deletion
-import django.utils.timezone
+该模块是OAuth认证系统的第二次迁移,主要用于优化字段命名和国际化显示。
+对已有的OAuthConfig和OAuthUser模型进行字段调整和选项更新。
+这是Django迁移系统自动生成的迁移文件,在Django 4.2.5版本中创建于2023-09-06。
+"""
+
+# 导入Django核心模块
+from django.conf import settings # 导入Django设置
+from django.db import migrations, models # 导入数据库迁移和模型相关功能
+import django.db.models.deletion # 导入外键删除操作
+import django.utils.timezone # 导入时区工具
class Migration(migrations.Migration):
+ """
+ OAuth认证系统的数据库迁移更新类
+
+ 这个迁移类负责对已有的OAuth相关模型进行字段优化和国际化改进,
+ 主要涉及时间字段重命名和verbose_name的英文标准化。
+ """
+ # 定义依赖关系 - 依赖于初始迁移和用户模型
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
- ('oauth', '0001_initial'),
+ ('oauth', '0001_initial'), # 依赖于oauth应用的初始迁移
]
+ # 定义要执行的数据库操作序列
operations = [
+ # 修改OAuthConfig模型的选项配置
migrations.AlterModelOptions(
name='oauthconfig',
- options={'ordering': ['-creation_time'], 'verbose_name': 'oauth配置', 'verbose_name_plural': 'oauth配置'},
+ options={
+ # 更新排序字段为新的creation_time字段
+ 'ordering': ['-creation_time'],
+ # 保持中文显示名称不变
+ 'verbose_name': 'oauth配置',
+ 'verbose_name_plural': 'oauth配置'
+ },
),
+ # 修改OAuthUser模型的选项配置
migrations.AlterModelOptions(
name='oauthuser',
- options={'ordering': ['-creation_time'], 'verbose_name': 'oauth user', 'verbose_name_plural': 'oauth user'},
+ options={
+ # 更新排序字段为新的creation_time字段
+ 'ordering': ['-creation_time'],
+ # 将显示名称改为英文
+ 'verbose_name': 'oauth user',
+ 'verbose_name_plural': 'oauth user'
+ },
),
+ # 移除OAuthConfig模型的旧时间字段
migrations.RemoveField(
model_name='oauthconfig',
- name='created_time',
+ name='created_time', # 删除旧的创建时间字段
),
migrations.RemoveField(
model_name='oauthconfig',
- name='last_mod_time',
+ name='last_mod_time', # 删除旧的修改时间字段
),
+ # 移除OAuthUser模型的旧时间字段
migrations.RemoveField(
model_name='oauthuser',
- name='created_time',
+ name='created_time', # 删除旧的创建时间字段
),
migrations.RemoveField(
model_name='oauthuser',
- name='last_mod_time',
+ name='last_mod_time', # 删除旧的修改时间字段
),
+ # 为OAuthConfig模型添加新的创建时间字段
migrations.AddField(
model_name='oauthconfig',
name='creation_time',
+ # 使用当前时间作为默认值,字段标签改为英文
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
+ # 为OAuthConfig模型添加新的修改时间字段
migrations.AddField(
model_name='oauthconfig',
name='last_modify_time',
+ # 使用当前时间作为默认值,字段标签改为英文
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
),
+ # 为OAuthUser模型添加新的创建时间字段
migrations.AddField(
model_name='oauthuser',
name='creation_time',
+ # 使用当前时间作为默认值,字段标签改为英文
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
+ # 为OAuthUser模型添加新的修改时间字段
migrations.AddField(
model_name='oauthuser',
name='last_modify_time',
+ # 使用当前时间作为默认值,字段标签改为英文
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
),
+ # 修改OAuthConfig回调地址字段的配置
migrations.AlterField(
model_name='oauthconfig',
name='callback_url',
+ # 将默认回调地址改为空字符串,字段标签改为英文
field=models.CharField(default='', max_length=200, verbose_name='callback url'),
),
+ # 修改OAuthConfig启用状态字段的标签
migrations.AlterField(
model_name='oauthconfig',
name='is_enable',
+ # 保持字段定义不变,只修改verbose_name为英文
field=models.BooleanField(default=True, verbose_name='is enable'),
),
+ # 修改OAuthConfig类型字段的选项和标签
migrations.AlterField(
model_name='oauthconfig',
name='type',
- field=models.CharField(choices=[('weibo', 'weibo'), ('google', 'google'), ('github', 'GitHub'), ('facebook', 'FaceBook'), ('qq', 'QQ')], default='a', max_length=10, verbose_name='type'),
+ # 将微博和谷歌的显示名称改为英文,其他保持不变
+ field=models.CharField(
+ choices=[('weibo', 'weibo'), ('google', 'google'), ('github', 'GitHub'), ('facebook', 'FaceBook'),
+ ('qq', 'QQ')], default='a', max_length=10, verbose_name='type'),
),
+ # 修改OAuthUser作者字段的标签
migrations.AlterField(
model_name='oauthuser',
name='author',
- field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'),
+ # 保持外键关系不变,修改verbose_name为英文
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
+ to=settings.AUTH_USER_MODEL, verbose_name='author'),
),
+ # 修改OAuthUser昵称字段的标签
migrations.AlterField(
model_name='oauthuser',
name='nickname',
+ # 保持字段定义不变,只修改verbose_name为英文
field=models.CharField(max_length=50, verbose_name='nickname'),
),
- ]
+ ]
\ No newline at end of file
diff --git a/src/DjangoBlog/oauth/migrations/0003_alter_oauthuser_nickname.py b/src/DjangoBlog/oauth/migrations/0003_alter_oauthuser_nickname.py
index 6af08eb..3586462 100644
--- a/src/DjangoBlog/oauth/migrations/0003_alter_oauthuser_nickname.py
+++ b/src/DjangoBlog/oauth/migrations/0003_alter_oauthuser_nickname.py
@@ -1,18 +1,37 @@
-# Generated by Django 4.2.7 on 2024-01-26 02:41
+"""
+Django 数据库迁移模块 - OAuth 用户昵称字段优化
-from django.db import migrations, models
+该模块是OAuth认证系统的第三次迁移,主要用于微调OAuthUser模型中昵称字段的显示标签。
+这是一个小的优化迁移,仅修改字段的verbose_name以改善可读性。
+这是Django迁移系统自动生成的迁移文件,在Django 4.2.7版本中创建于2024-01-26。
+"""
+
+# 导入Django核心模块
+from django.db import migrations, models # 导入数据库迁移和模型相关功能
class Migration(migrations.Migration):
+ """
+ OAuth认证系统的数据库微调迁移类
+
+ 这个迁移类负责对OAuthUser模型的昵称字段进行显示标签优化,
+ 将'nickname'改为'nick name'以改善管理界面的可读性。
+ """
+ # 定义依赖关系 - 依赖于前一次迁移
dependencies = [
+ # 依赖于oauth应用的第二次迁移(字段重命名和国际化迁移)
('oauth', '0002_alter_oauthconfig_options_alter_oauthuser_options_and_more'),
]
+ # 定义要执行的数据库操作序列
operations = [
+ # 修改OAuthUser模型昵称字段的显示标签
migrations.AlterField(
- model_name='oauthuser',
- name='nickname',
+ model_name='oauthuser', # 指定要修改的模型名称
+ name='nickname', # 指定要修改的字段名称
+ # 保持字段类型和约束不变,仅优化verbose_name显示
+ # 将'nickname'改为'nick name',增加空格提高可读性
field=models.CharField(max_length=50, verbose_name='nick name'),
),
- ]
+ ]
\ No newline at end of file
diff --git a/src/DjangoBlog/oauth/models.py b/src/DjangoBlog/oauth/models.py
index be838ed..e530107 100644
--- a/src/DjangoBlog/oauth/models.py
+++ b/src/DjangoBlog/oauth/models.py
@@ -1,4 +1,11 @@
-# Create your models here.
+"""
+OAuth 认证数据模型模块
+
+该模块定义了OAuth认证系统所需的数据模型,包括OAuth用户信息和OAuth服务商配置。
+用于存储第三方登录的用户数据和OAuth应用配置信息。
+"""
+
+# 导入Django核心模块
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
@@ -7,61 +14,135 @@ from django.utils.translation import gettext_lazy as _
class OAuthUser(models.Model):
+ """
+ OAuth用户模型
+
+ 存储通过第三方OAuth服务登录的用户信息,包括用户基本信息、
+ 认证令牌以及与本地用户的关联关系。
+ """
+
+ # 关联本地用户模型的外键,可为空(未绑定本地用户时)
author = models.ForeignKey(
- settings.AUTH_USER_MODEL,
- verbose_name=_('author'),
- blank=True,
- null=True,
- on_delete=models.CASCADE)
+ settings.AUTH_USER_MODEL, # 使用Django的可切换用户模型
+ verbose_name=_('author'), # 字段显示名称(支持国际化)
+ blank=True, # 允许表单中为空
+ null=True, # 允许数据库中为NULL
+ on_delete=models.CASCADE # 关联用户删除时级联删除OAuth用户
+ )
+
+ # 第三方平台的用户唯一标识符
openid = models.CharField(max_length=50)
+
+ # 用户在第三方平台的昵称
nickname = models.CharField(max_length=50, verbose_name=_('nick name'))
+
+ # OAuth访问令牌,用于调用第三方API
token = models.CharField(max_length=150, null=True, blank=True)
+
+ # 用户头像的URL地址
picture = models.CharField(max_length=350, blank=True, null=True)
+
+ # OAuth服务类型(如:weibo, github等)
type = models.CharField(blank=False, null=False, max_length=50)
+
+ # 用户邮箱地址,可能为空(某些平台不提供邮箱)
email = models.CharField(max_length=50, null=True, blank=True)
+
+ # 存储额外的元数据信息,使用JSON格式
metadata = models.TextField(null=True, blank=True)
+
+ # 记录创建时间,默认使用当前时间
creation_time = models.DateTimeField(_('creation time'), default=now)
+
+ # 记录最后修改时间,默认使用当前时间
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
def __str__(self):
+ """
+ 定义模型的字符串表示形式
+
+ Returns:
+ str: 返回用户的昵称,用于Admin和其他显示场景
+ """
return self.nickname
class Meta:
- verbose_name = _('oauth user')
- verbose_name_plural = verbose_name
- ordering = ['-creation_time']
+ """模型元数据配置"""
+ verbose_name = _('oauth user') # 模型在Admin中的单数显示名称
+ verbose_name_plural = verbose_name # 模型在Admin中的复数显示名称
+ ordering = ['-creation_time'] # 默认按创建时间降序排列
class OAuthConfig(models.Model):
+ """
+ OAuth服务配置模型
+
+ 存储不同第三方OAuth服务的应用配置信息,包括AppKey、AppSecret等。
+ 用于管理多个OAuth服务的认证参数。
+ """
+
+ # OAuth服务类型选择项
TYPE = (
- ('weibo', _('weibo')),
- ('google', _('google')),
- ('github', 'GitHub'),
- ('facebook', 'FaceBook'),
- ('qq', 'QQ'),
+ ('weibo', _('weibo')), # 微博OAuth
+ ('google', _('google')), # 谷歌OAuth
+ ('github', 'GitHub'), # GitHub OAuth
+ ('facebook', 'FaceBook'), # Facebook OAuth
+ ('qq', 'QQ'), # QQ OAuth
)
+
+ # OAuth服务类型字段,使用选择项限制输入
type = models.CharField(_('type'), max_length=10, choices=TYPE, default='a')
+
+ # OAuth应用的客户端ID(App Key)
appkey = models.CharField(max_length=200, verbose_name='AppKey')
+
+ # OAuth应用的客户端密钥(App Secret)
appsecret = models.CharField(max_length=200, verbose_name='AppSecret')
+
+ # OAuth认证成功后的回调地址
callback_url = models.CharField(
max_length=200,
verbose_name=_('callback url'),
- blank=False,
- default='')
+ blank=False, # 不允许为空
+ default='' # 默认值为空字符串
+ )
+
+ # 标识该OAuth配置是否启用
is_enable = models.BooleanField(
_('is enable'), default=True, blank=False, null=False)
+
+ # 记录配置创建时间
creation_time = models.DateTimeField(_('creation time'), default=now)
+
+ # 记录配置最后修改时间
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
def clean(self):
+ """
+ 模型验证方法
+
+ 确保同一类型的OAuth配置只能存在一个,防止重复配置。
+
+ Raises:
+ ValidationError: 当同一类型的配置已存在时抛出异常
+ """
+ # 检查是否已存在相同类型的配置(排除当前记录)
if OAuthConfig.objects.filter(
type=self.type).exclude(id=self.id).count():
+ # 抛出验证错误,提示该类型配置已存在
raise ValidationError(_(self.type + _('already exists')))
def __str__(self):
+ """
+ 定义模型的字符串表示形式
+
+ Returns:
+ str: 返回OAuth服务类型名称
+ """
return self.type
class Meta:
- verbose_name = 'oauth配置'
- verbose_name_plural = verbose_name
- ordering = ['-creation_time']
+ """模型元数据配置"""
+ verbose_name = 'oauth配置' # 模型在Admin中的中文显示名称
+ verbose_name_plural = verbose_name # 复数显示名称
+ ordering = ['-creation_time'] # 默认按创建时间降序排列
\ No newline at end of file
diff --git a/src/DjangoBlog/oauth/oauthmanager.py b/src/DjangoBlog/oauth/oauthmanager.py
index 2e7ceef..48f5afa 100644
--- a/src/DjangoBlog/oauth/oauthmanager.py
+++ b/src/DjangoBlog/oauth/oauthmanager.py
@@ -1,3 +1,11 @@
+"""
+OAuth 认证管理器模块
+
+该模块实现了多平台OAuth认证的核心逻辑,包含基类定义和具体平台实现。
+支持微博、谷歌、GitHub、Facebook、QQ等主流第三方登录平台。
+采用抽象基类和混合类设计模式,提供统一的OAuth认证接口。
+"""
+
import json
import logging
import os
@@ -9,79 +17,139 @@ import requests
from djangoblog.utils import cache_decorator
from oauth.models import OAuthUser, OAuthConfig
+# 获取当前模块的日志记录器
logger = logging.getLogger(__name__)
class OAuthAccessTokenException(Exception):
'''
- oauth授权失败异常
+ OAuth授权令牌获取异常类
+
+ 当从OAuth服务商获取访问令牌失败时抛出此异常,
+ 通常由于错误的授权码、应用配置问题或网络问题导致。
'''
class BaseOauthManager(metaclass=ABCMeta):
- """获取用户授权"""
+ """
+ OAuth认证管理器抽象基类
+
+ 定义所有OAuth平台必须实现的接口和方法,
+ 提供统一的OAuth认证流程模板。
+ """
+
+ # OAuth授权页面URL(需要子类实现)
AUTH_URL = None
- """获取token"""
+ # 获取访问令牌的URL(需要子类实现)
TOKEN_URL = None
- """获取用户信息"""
+ # 获取用户信息的API URL(需要子类实现)
API_URL = None
- '''icon图标名'''
+ # 平台图标名称,用于标识和显示(需要子类实现)
ICON_NAME = None
def __init__(self, access_token=None, openid=None):
+ """
+ 初始化OAuth管理器
+
+ Args:
+ access_token: 已存在的访问令牌(可选)
+ openid: 已存在的用户OpenID(可选)
+ """
self.access_token = access_token
self.openid = openid
@property
def is_access_token_set(self):
+ """检查访问令牌是否已设置"""
return self.access_token is not None
@property
def is_authorized(self):
+ """检查是否已完成授权(拥有令牌和OpenID)"""
return self.is_access_token_set and self.access_token is not None and self.openid is not None
@abstractmethod
def get_authorization_url(self, nexturl='/'):
+ """获取授权页面URL(抽象方法,子类必须实现)"""
pass
@abstractmethod
def get_access_token_by_code(self, code):
+ """通过授权码获取访问令牌(抽象方法,子类必须实现)"""
pass
@abstractmethod
def get_oauth_userinfo(self):
+ """获取OAuth用户信息(抽象方法,子类必须实现)"""
pass
@abstractmethod
def get_picture(self, metadata):
+ """从元数据中提取用户头像URL(抽象方法,子类必须实现)"""
pass
def do_get(self, url, params, headers=None):
+ """
+ 执行GET请求的通用方法
+
+ Args:
+ url: 请求URL
+ params: 请求参数
+ headers: 请求头(可选)
+
+ Returns:
+ str: 响应文本内容
+ """
rsp = requests.get(url=url, params=params, headers=headers)
- logger.info(rsp.text)
+ logger.info(rsp.text) # 记录响应日志
return rsp.text
def do_post(self, url, params, headers=None):
+ """
+ 执行POST请求的通用方法
+
+ Args:
+ url: 请求URL
+ params: 请求参数
+ headers: 请求头(可选)
+
+ Returns:
+ str: 响应文本内容
+ """
rsp = requests.post(url, params, headers=headers)
- logger.info(rsp.text)
+ logger.info(rsp.text) # 记录响应日志
return rsp.text
def get_config(self):
+ """
+ 从数据库获取当前平台的OAuth配置
+
+ Returns:
+ OAuthConfig: 配置对象,如果不存在则返回None
+ """
value = OAuthConfig.objects.filter(type=self.ICON_NAME)
return value[0] if value else None
class WBOauthManager(BaseOauthManager):
+ """
+ 微博OAuth认证管理器
+
+ 实现微博平台的OAuth2.0认证流程,包括授权、令牌获取和用户信息获取。
+ """
+
+ # 微博OAuth接口地址
AUTH_URL = 'https://api.weibo.com/oauth2/authorize'
TOKEN_URL = 'https://api.weibo.com/oauth2/access_token'
API_URL = 'https://api.weibo.com/2/users/show.json'
ICON_NAME = 'weibo'
def __init__(self, access_token=None, openid=None):
+ """初始化微博OAuth配置"""
config = self.get_config()
- self.client_id = config.appkey if config else ''
- self.client_secret = config.appsecret if config else ''
- self.callback_url = config.callback_url if config else ''
+ self.client_id = config.appkey if config else '' # 应用Key
+ self.client_secret = config.appsecret if config else '' # 应用Secret
+ self.callback_url = config.callback_url if config else '' # 回调地址
super(
WBOauthManager,
self).__init__(
@@ -89,6 +157,15 @@ class WBOauthManager(BaseOauthManager):
openid=openid)
def get_authorization_url(self, nexturl='/'):
+ """
+ 生成微博授权页面URL
+
+ Args:
+ nexturl: 授权成功后跳转的URL
+
+ Returns:
+ str: 完整的授权URL
+ """
params = {
'client_id': self.client_id,
'response_type': 'code',
@@ -98,7 +175,18 @@ class WBOauthManager(BaseOauthManager):
return url
def get_access_token_by_code(self, code):
+ """
+ 使用授权码获取访问令牌
+
+ Args:
+ code: OAuth授权码
+ Returns:
+ OAuthUser: 用户信息对象
+
+ Raises:
+ OAuthAccessTokenException: 令牌获取失败时抛出
+ """
params = {
'client_id': self.client_id,
'client_secret': self.client_secret,
@@ -110,13 +198,20 @@ class WBOauthManager(BaseOauthManager):
obj = json.loads(rsp)
if 'access_token' in obj:
+ # 设置访问令牌和用户ID
self.access_token = str(obj['access_token'])
self.openid = str(obj['uid'])
- return self.get_oauth_userinfo()
+ return self.get_oauth_userinfo() # 获取并返回用户信息
else:
raise OAuthAccessTokenException(rsp)
def get_oauth_userinfo(self):
+ """
+ 获取微博用户信息
+
+ Returns:
+ OAuthUser: 包含用户信息的对象,获取失败返回None
+ """
if not self.is_authorized:
return None
params = {
@@ -127,14 +222,14 @@ class WBOauthManager(BaseOauthManager):
try:
datas = json.loads(rsp)
user = OAuthUser()
- user.metadata = rsp
- user.picture = datas['avatar_large']
- user.nickname = datas['screen_name']
- user.openid = datas['id']
- user.type = 'weibo'
- user.token = self.access_token
+ user.metadata = rsp # 存储原始响应数据
+ user.picture = datas['avatar_large'] # 用户头像
+ user.nickname = datas['screen_name'] # 用户昵称
+ user.openid = datas['id'] # 用户OpenID
+ user.type = 'weibo' # 平台类型
+ user.token = self.access_token # 访问令牌
if 'email' in datas and datas['email']:
- user.email = datas['email']
+ user.email = datas['email'] # 用户邮箱
return user
except Exception as e:
logger.error(e)
@@ -142,13 +237,30 @@ class WBOauthManager(BaseOauthManager):
return None
def get_picture(self, metadata):
+ """
+ 从元数据中提取微博用户头像
+
+ Args:
+ metadata: 用户元数据JSON字符串
+
+ Returns:
+ str: 用户头像URL
+ """
datas = json.loads(metadata)
return datas['avatar_large']
class ProxyManagerMixin:
+ """
+ 代理管理器混合类
+
+ 为OAuth管理器添加HTTP代理支持,用于网络访问受限的环境。
+ """
+
def __init__(self, *args, **kwargs):
+ """初始化代理配置"""
if os.environ.get("HTTP_PROXY"):
+ # 设置HTTP和HTTPS代理
self.proxies = {
"http": os.environ.get("HTTP_PROXY"),
"https": os.environ.get("HTTP_PROXY")
@@ -157,23 +269,32 @@ class ProxyManagerMixin:
self.proxies = None
def do_get(self, url, params, headers=None):
+ """带代理支持的GET请求"""
rsp = requests.get(url=url, params=params, headers=headers, proxies=self.proxies)
logger.info(rsp.text)
return rsp.text
def do_post(self, url, params, headers=None):
+ """带代理支持的POST请求"""
rsp = requests.post(url, params, headers=headers, proxies=self.proxies)
logger.info(rsp.text)
return rsp.text
class GoogleOauthManager(ProxyManagerMixin, BaseOauthManager):
+ """
+ 谷歌OAuth认证管理器
+
+ 实现谷歌平台的OAuth2.0认证流程,支持代理访问。
+ """
+
AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth'
TOKEN_URL = 'https://www.googleapis.com/oauth2/v4/token'
API_URL = 'https://www.googleapis.com/oauth2/v3/userinfo'
ICON_NAME = 'google'
def __init__(self, access_token=None, openid=None):
+ """初始化谷歌OAuth配置"""
config = self.get_config()
self.client_id = config.appkey if config else ''
self.client_secret = config.appsecret if config else ''
@@ -185,22 +306,23 @@ class GoogleOauthManager(ProxyManagerMixin, BaseOauthManager):
openid=openid)
def get_authorization_url(self, nexturl='/'):
+ """生成谷歌授权页面URL"""
params = {
'client_id': self.client_id,
'response_type': 'code',
'redirect_uri': self.callback_url,
- 'scope': 'openid email',
+ 'scope': 'openid email', # 请求openid和email权限
}
url = self.AUTH_URL + "?" + urllib.parse.urlencode(params)
return url
def get_access_token_by_code(self, code):
+ """使用授权码获取谷歌访问令牌"""
params = {
'client_id': self.client_id,
'client_secret': self.client_secret,
'grant_type': 'authorization_code',
'code': code,
-
'redirect_uri': self.callback_url
}
rsp = self.do_post(self.TOKEN_URL, params)
@@ -216,6 +338,7 @@ class GoogleOauthManager(ProxyManagerMixin, BaseOauthManager):
raise OAuthAccessTokenException(rsp)
def get_oauth_userinfo(self):
+ """获取谷歌用户信息"""
if not self.is_authorized:
return None
params = {
@@ -223,17 +346,16 @@ class GoogleOauthManager(ProxyManagerMixin, BaseOauthManager):
}
rsp = self.do_get(self.API_URL, params)
try:
-
datas = json.loads(rsp)
user = OAuthUser()
user.metadata = rsp
- user.picture = datas['picture']
- user.nickname = datas['name']
- user.openid = datas['sub']
+ user.picture = datas['picture'] # 谷歌用户头像
+ user.nickname = datas['name'] # 谷歌用户姓名
+ user.openid = datas['sub'] # 谷歌用户唯一标识
user.token = self.access_token
user.type = 'google'
if datas['email']:
- user.email = datas['email']
+ user.email = datas['email'] # 谷歌邮箱
return user
except Exception as e:
logger.error(e)
@@ -241,17 +363,25 @@ class GoogleOauthManager(ProxyManagerMixin, BaseOauthManager):
return None
def get_picture(self, metadata):
+ """从元数据中提取谷歌用户头像"""
datas = json.loads(metadata)
return datas['picture']
class GitHubOauthManager(ProxyManagerMixin, BaseOauthManager):
+ """
+ GitHub OAuth认证管理器
+
+ 实现GitHub平台的OAuth2.0认证流程,支持代理访问。
+ """
+
AUTH_URL = 'https://github.com/login/oauth/authorize'
TOKEN_URL = 'https://github.com/login/oauth/access_token'
API_URL = 'https://api.github.com/user'
ICON_NAME = 'github'
def __init__(self, access_token=None, openid=None):
+ """初始化GitHub OAuth配置"""
config = self.get_config()
self.client_id = config.appkey if config else ''
self.client_secret = config.appsecret if config else ''
@@ -263,28 +393,29 @@ class GitHubOauthManager(ProxyManagerMixin, BaseOauthManager):
openid=openid)
def get_authorization_url(self, next_url='/'):
+ """生成GitHub授权页面URL"""
params = {
'client_id': self.client_id,
'response_type': 'code',
'redirect_uri': f'{self.callback_url}&next_url={next_url}',
- 'scope': 'user'
+ 'scope': 'user' # 请求用户信息权限
}
url = self.AUTH_URL + "?" + urllib.parse.urlencode(params)
return url
def get_access_token_by_code(self, code):
+ """使用授权码获取GitHub访问令牌"""
params = {
'client_id': self.client_id,
'client_secret': self.client_secret,
'grant_type': 'authorization_code',
'code': code,
-
'redirect_uri': self.callback_url
}
rsp = self.do_post(self.TOKEN_URL, params)
from urllib import parse
- r = parse.parse_qs(rsp)
+ r = parse.parse_qs(rsp) # 解析查询字符串格式的响应
if 'access_token' in r:
self.access_token = (r['access_token'][0])
return self.access_token
@@ -292,21 +423,22 @@ class GitHubOauthManager(ProxyManagerMixin, BaseOauthManager):
raise OAuthAccessTokenException(rsp)
def get_oauth_userinfo(self):
-
+ """获取GitHub用户信息"""
+ # 使用Bearer Token认证方式调用GitHub API
rsp = self.do_get(self.API_URL, params={}, headers={
"Authorization": "token " + self.access_token
})
try:
datas = json.loads(rsp)
user = OAuthUser()
- user.picture = datas['avatar_url']
- user.nickname = datas['name']
- user.openid = datas['id']
+ user.picture = datas['avatar_url'] # GitHub头像
+ user.nickname = datas['name'] # GitHub姓名
+ user.openid = datas['id'] # GitHub用户ID
user.type = 'github'
user.token = self.access_token
user.metadata = rsp
if 'email' in datas and datas['email']:
- user.email = datas['email']
+ user.email = datas['email'] # GitHub邮箱
return user
except Exception as e:
logger.error(e)
@@ -314,17 +446,25 @@ class GitHubOauthManager(ProxyManagerMixin, BaseOauthManager):
return None
def get_picture(self, metadata):
+ """从元数据中提取GitHub用户头像"""
datas = json.loads(metadata)
return datas['avatar_url']
class FaceBookOauthManager(ProxyManagerMixin, BaseOauthManager):
+ """
+ Facebook OAuth认证管理器
+
+ 实现Facebook平台的OAuth2.0认证流程,支持代理访问。
+ """
+
AUTH_URL = 'https://www.facebook.com/v16.0/dialog/oauth'
TOKEN_URL = 'https://graph.facebook.com/v16.0/oauth/access_token'
API_URL = 'https://graph.facebook.com/me'
ICON_NAME = 'facebook'
def __init__(self, access_token=None, openid=None):
+ """初始化Facebook OAuth配置"""
config = self.get_config()
self.client_id = config.appkey if config else ''
self.client_secret = config.appsecret if config else ''
@@ -336,22 +476,22 @@ class FaceBookOauthManager(ProxyManagerMixin, BaseOauthManager):
openid=openid)
def get_authorization_url(self, next_url='/'):
+ """生成Facebook授权页面URL"""
params = {
'client_id': self.client_id,
'response_type': 'code',
'redirect_uri': self.callback_url,
- 'scope': 'email,public_profile'
+ 'scope': 'email,public_profile' # 请求邮箱和公开资料权限
}
url = self.AUTH_URL + "?" + urllib.parse.urlencode(params)
return url
def get_access_token_by_code(self, code):
+ """使用授权码获取Facebook访问令牌"""
params = {
'client_id': self.client_id,
'client_secret': self.client_secret,
- # 'grant_type': 'authorization_code',
'code': code,
-
'redirect_uri': self.callback_url
}
rsp = self.do_post(self.TOKEN_URL, params)
@@ -365,21 +505,23 @@ class FaceBookOauthManager(ProxyManagerMixin, BaseOauthManager):
raise OAuthAccessTokenException(rsp)
def get_oauth_userinfo(self):
+ """获取Facebook用户信息"""
params = {
'access_token': self.access_token,
- 'fields': 'id,name,picture,email'
+ 'fields': 'id,name,picture,email' # 指定需要返回的字段
}
try:
rsp = self.do_get(self.API_URL, params)
datas = json.loads(rsp)
user = OAuthUser()
- user.nickname = datas['name']
- user.openid = datas['id']
+ user.nickname = datas['name'] # Facebook姓名
+ user.openid = datas['id'] # Facebook用户ID
user.type = 'facebook'
user.token = self.access_token
user.metadata = rsp
if 'email' in datas and datas['email']:
- user.email = datas['email']
+ user.email = datas['email'] # Facebook邮箱
+ # 处理嵌套的头像数据结构
if 'picture' in datas and datas['picture'] and datas['picture']['data'] and datas['picture']['data']['url']:
user.picture = str(datas['picture']['data']['url'])
return user
@@ -388,18 +530,26 @@ class FaceBookOauthManager(ProxyManagerMixin, BaseOauthManager):
return None
def get_picture(self, metadata):
+ """从元数据中提取Facebook用户头像"""
datas = json.loads(metadata)
return str(datas['picture']['data']['url'])
class QQOauthManager(BaseOauthManager):
+ """
+ QQ OAuth认证管理器
+
+ 实现QQ平台的OAuth2.0认证流程,包含特殊的OpenID获取步骤。
+ """
+
AUTH_URL = 'https://graph.qq.com/oauth2.0/authorize'
TOKEN_URL = 'https://graph.qq.com/oauth2.0/token'
API_URL = 'https://graph.qq.com/user/get_user_info'
- OPEN_ID_URL = 'https://graph.qq.com/oauth2.0/me'
+ OPEN_ID_URL = 'https://graph.qq.com/oauth2.0/me' # QQ特有的OpenID获取接口
ICON_NAME = 'qq'
def __init__(self, access_token=None, openid=None):
+ """初始化QQ OAuth配置"""
config = self.get_config()
self.client_id = config.appkey if config else ''
self.client_secret = config.appsecret if config else ''
@@ -411,6 +561,7 @@ class QQOauthManager(BaseOauthManager):
openid=openid)
def get_authorization_url(self, next_url='/'):
+ """生成QQ授权页面URL"""
params = {
'response_type': 'code',
'client_id': self.client_id,
@@ -420,6 +571,7 @@ class QQOauthManager(BaseOauthManager):
return url
def get_access_token_by_code(self, code):
+ """使用授权码获取QQ访问令牌"""
params = {
'grant_type': 'authorization_code',
'client_id': self.client_id,
@@ -429,7 +581,7 @@ class QQOauthManager(BaseOauthManager):
}
rsp = self.do_get(self.TOKEN_URL, params)
if rsp:
- d = urllib.parse.parse_qs(rsp)
+ d = urllib.parse.parse_qs(rsp) # 解析查询字符串响应
if 'access_token' in d:
token = d['access_token']
self.access_token = token[0]
@@ -438,23 +590,27 @@ class QQOauthManager(BaseOauthManager):
raise OAuthAccessTokenException(rsp)
def get_open_id(self):
+ """
+ 获取QQ用户的OpenID
+
+ QQ平台需要额外调用接口获取用户OpenID
+ """
if self.is_access_token_set:
params = {
'access_token': self.access_token
}
rsp = self.do_get(self.OPEN_ID_URL, params)
if rsp:
- rsp = rsp.replace(
- 'callback(', '').replace(
- ')', '').replace(
- ';', '')
+ # 清理JSONP响应格式
+ rsp = rsp.replace('callback(', '').replace(')', '').replace(';', '')
obj = json.loads(rsp)
openid = str(obj['openid'])
self.openid = openid
return openid
def get_oauth_userinfo(self):
- openid = self.get_open_id()
+ """获取QQ用户信息"""
+ openid = self.get_open_id() # 先获取OpenID
if openid:
params = {
'access_token': self.access_token,
@@ -465,40 +621,60 @@ class QQOauthManager(BaseOauthManager):
logger.info(rsp)
obj = json.loads(rsp)
user = OAuthUser()
- user.nickname = obj['nickname']
- user.openid = openid
+ user.nickname = obj['nickname'] # QQ昵称
+ user.openid = openid # QQ OpenID
user.type = 'qq'
user.token = self.access_token
user.metadata = rsp
if 'email' in obj:
- user.email = obj['email']
+ user.email = obj['email'] # QQ邮箱
if 'figureurl' in obj:
- user.picture = str(obj['figureurl'])
+ user.picture = str(obj['figureurl']) # QQ头像
return user
def get_picture(self, metadata):
+ """从元数据中提取QQ用户头像"""
datas = json.loads(metadata)
return str(datas['figureurl'])
@cache_decorator(expiration=100 * 60)
def get_oauth_apps():
+ """
+ 获取所有启用的OAuth应用配置
+
+ 使用缓存装饰器,缓存100分钟,减少数据库查询
+
+ Returns:
+ list: 启用的OAuth管理器实例列表
+ """
configs = OAuthConfig.objects.filter(is_enable=True).all()
if not configs:
return []
- configtypes = [x.type for x in configs]
- applications = BaseOauthManager.__subclasses__()
+ configtypes = [x.type for x in configs] # 提取启用的平台类型
+ applications = BaseOauthManager.__subclasses__() # 获取所有子类
+ # 过滤出已启用的平台管理器实例
apps = [x() for x in applications if x().ICON_NAME.lower() in configtypes]
return apps
def get_manager_by_type(type):
+ """
+ 根据平台类型获取对应的OAuth管理器
+
+ Args:
+ type: 平台类型字符串(如:'weibo', 'github')
+
+ Returns:
+ BaseOauthManager: 对应平台的OAuth管理器实例,未找到返回None
+ """
applications = get_oauth_apps()
if applications:
+ # 查找匹配平台类型的管理器
finds = list(
filter(
lambda x: x.ICON_NAME.lower() == type.lower(),
applications))
if finds:
return finds[0]
- return None
+ return None
\ No newline at end of file
diff --git a/src/DjangoBlog/oauth/templatetags/oauth_tags.py b/src/DjangoBlog/oauth/templatetags/oauth_tags.py
index 7b687d5..eeae7a7 100644
--- a/src/DjangoBlog/oauth/templatetags/oauth_tags.py
+++ b/src/DjangoBlog/oauth/templatetags/oauth_tags.py
@@ -1,22 +1,64 @@
+"""
+OAuth 认证模板标签模块
+
+该模块提供Django模板标签,用于在模板中动态加载和显示OAuth第三方登录应用列表。
+主要功能是生成可用的OAuth应用链接并在模板中渲染。
+"""
+
+# 导入Django模板模块
from django import template
+# 导入URL反向解析功能
from django.urls import reverse
+# 导入自定义的OAuth管理器,用于获取可用的OAuth应用
from oauth.oauthmanager import get_oauth_apps
+# 创建模板库实例
register = template.Library()
@register.inclusion_tag('oauth/oauth_applications.html')
def load_oauth_applications(request):
+ """
+ 自定义包含标签 - 加载OAuth应用列表
+
+ 该模板标签用于在页面中渲染OAuth第三方登录的应用图标和链接。
+ 它会获取所有可用的OAuth应用,并生成对应的登录URL。
+
+ Args:
+ request: HttpRequest对象,用于获取当前请求的完整路径
+
+ Returns:
+ dict: 包含应用列表的字典,用于模板渲染
+ - 'apps': 包含OAuth应用信息的列表,每个元素为(应用类型, 登录URL)的元组
+ """
+ # 获取所有可用的OAuth应用配置
applications = get_oauth_apps()
+
+ # 检查是否存在可用的OAuth应用
if applications:
+ # 生成OAuth登录的基础URL(不包含参数)
baseurl = reverse('oauth:oauthlogin')
+ # 获取当前请求的完整路径,用于登录成功后跳转回原页面
path = request.get_full_path()
- apps = list(map(lambda x: (x.ICON_NAME, '{baseurl}?type={type}&next_url={next}'.format(
- baseurl=baseurl, type=x.ICON_NAME, next=path)), applications))
+ # 使用map和lambda函数处理每个OAuth应用,生成应用信息列表
+ # 每个应用信息包含:应用类型图标名称和完整的登录URL
+ apps = list(map(lambda x: (
+ # OAuth应用的类型/图标名称(如:weibo, github等)
+ x.ICON_NAME,
+ # 生成完整的登录URL,包含应用类型和回调地址参数
+ '{baseurl}?type={type}&next_url={next}'.format(
+ baseurl=baseurl, # 基础登录URL
+ type=x.ICON_NAME, # OAuth应用类型
+ next=path # 登录成功后的回调地址
+ )),
+ applications)) # 遍历的应用列表
else:
+ # 如果没有可用的OAuth应用,返回空列表
apps = []
+
+ # 返回模板渲染所需的上下文数据
return {
- 'apps': apps
- }
+ 'apps': apps # OAuth应用列表,传递给模板进行渲染
+ }
\ No newline at end of file
diff --git a/src/DjangoBlog/oauth/tests.py b/src/DjangoBlog/oauth/tests.py
index bb23b9b..50973bc 100644
--- a/src/DjangoBlog/oauth/tests.py
+++ b/src/DjangoBlog/oauth/tests.py
@@ -1,55 +1,112 @@
+"""
+OAuth 认证测试模块
+
+该模块包含OAuth认证系统的完整测试用例,覆盖所有支持的第三方登录平台。
+测试包括配置验证、授权流程、用户信息获取和异常处理等场景。
+"""
+
import json
from unittest.mock import patch
+# 导入Django测试相关模块
from django.conf import settings
from django.contrib import auth
from django.test import Client, RequestFactory, TestCase
from django.urls import reverse
+# 导入项目工具函数和模型
from djangoblog.utils import get_sha256
from oauth.models import OAuthConfig
from oauth.oauthmanager import BaseOauthManager
-# Create your tests here.
class OAuthConfigTest(TestCase):
+ """
+ OAuth配置模型测试类
+
+ 测试OAuth配置的基本功能,包括配置保存和基础授权流程。
+ """
+
def setUp(self):
- self.client = Client()
- self.factory = RequestFactory()
+ """
+ 测试初始化方法
+
+ 在每个测试方法执行前运行,创建测试所需的客户端和工厂对象。
+ """
+ self.client = Client() # 创建测试客户端
+ self.factory = RequestFactory() # 创建请求工厂
def test_oauth_login_test(self):
+ """
+ 测试OAuth登录流程
+
+ 验证微博OAuth配置的创建和基础授权重定向功能。
+ """
+ # 创建微博OAuth配置
c = OAuthConfig()
c.type = 'weibo'
c.appkey = 'appkey'
c.appsecret = 'appsecret'
c.save()
+ # 测试OAuth登录请求,应该重定向到微博授权页面
response = self.client.get('/oauth/oauthlogin?type=weibo')
- self.assertEqual(response.status_code, 302)
- self.assertTrue("api.weibo.com" in response.url)
+ self.assertEqual(response.status_code, 302) # 验证重定向状态码
+ self.assertTrue("api.weibo.com" in response.url) # 验证重定向到微博
+ # 测试授权回调请求(模拟授权码流程)
response = self.client.get('/oauth/authorize?type=weibo&code=code')
- self.assertEqual(response.status_code, 302)
- self.assertEqual(response.url, '/')
+ self.assertEqual(response.status_code, 302) # 验证重定向状态码
+ self.assertEqual(response.url, '/') # 验证重定向到首页
class OauthLoginTest(TestCase):
+ """
+ OAuth登录流程测试类
+
+ 测试所有支持的OAuth平台的完整登录流程,包括模拟API调用和用户认证。
+ """
+
def setUp(self) -> None:
- self.client = Client()
- self.factory = RequestFactory()
- self.apps = self.init_apps()
+ """
+ 测试初始化方法
+
+ 创建测试环境,初始化所有OAuth应用配置。
+ """
+ self.client = Client() # 创建测试客户端
+ self.factory = RequestFactory() # 创建请求工厂
+ self.apps = self.init_apps() # 初始化所有OAuth应用配置
def init_apps(self):
+ """
+ 初始化所有OAuth应用配置
+
+ 为每个OAuth平台创建测试配置数据。
+
+ Returns:
+ list: 初始化的OAuth管理器实例列表
+ """
+ # 获取所有OAuth管理器的子类并实例化
applications = [p() for p in BaseOauthManager.__subclasses__()]
for application in applications:
+ # 为每个平台创建测试配置
c = OAuthConfig()
- c.type = application.ICON_NAME.lower()
- c.appkey = 'appkey'
- c.appsecret = 'appsecret'
+ c.type = application.ICON_NAME.lower() # 设置平台类型
+ c.appkey = 'appkey' # 测试应用Key
+ c.appsecret = 'appsecret' # 测试应用Secret
c.save()
return applications
def get_app_by_type(self, type):
+ """
+ 根据类型获取OAuth应用
+
+ Args:
+ type: 平台类型字符串
+
+ Returns:
+ BaseOauthManager: 对应平台的OAuth管理器实例
+ """
for app in self.apps:
if app.ICON_NAME.lower() == type:
return app
@@ -57,73 +114,129 @@ class OauthLoginTest(TestCase):
@patch("oauth.oauthmanager.WBOauthManager.do_post")
@patch("oauth.oauthmanager.WBOauthManager.do_get")
def test_weibo_login(self, mock_do_get, mock_do_post):
+ """
+ 测试微博OAuth登录流程
+
+ 使用模拟对象测试微博授权码获取令牌和用户信息的完整流程。
+
+ Args:
+ mock_do_get: 模拟GET请求的mock对象
+ mock_do_post: 模拟POST请求的mock对象
+ """
weibo_app = self.get_app_by_type('weibo')
- assert weibo_app
+ assert weibo_app # 验证微博应用存在
+
+ # 测试授权URL生成
url = weibo_app.get_authorization_url()
+
+ # 设置模拟返回值 - 令牌获取响应
mock_do_post.return_value = json.dumps({"access_token": "access_token",
"uid": "uid"
})
+ # 设置模拟返回值 - 用户信息获取响应
mock_do_get.return_value = json.dumps({
"avatar_large": "avatar_large",
"screen_name": "screen_name",
"id": "id",
"email": "email",
})
+
+ # 执行授权码换取用户信息流程
userinfo = weibo_app.get_access_token_by_code('code')
- self.assertEqual(userinfo.token, 'access_token')
- self.assertEqual(userinfo.openid, 'id')
+
+ # 验证返回的用户信息正确性
+ self.assertEqual(userinfo.token, 'access_token') # 验证访问令牌
+ self.assertEqual(userinfo.openid, 'id') # 验证用户OpenID
@patch("oauth.oauthmanager.GoogleOauthManager.do_post")
@patch("oauth.oauthmanager.GoogleOauthManager.do_get")
def test_google_login(self, mock_do_get, mock_do_post):
+ """
+ 测试谷歌OAuth登录流程
+
+ 验证谷歌OAuth的令牌获取和用户信息解析。
+ """
google_app = self.get_app_by_type('google')
- assert google_app
+ assert google_app # 验证谷歌应用存在
+
+ # 测试授权URL生成
url = google_app.get_authorization_url()
+
+ # 设置模拟返回值 - 令牌获取响应
mock_do_post.return_value = json.dumps({
"access_token": "access_token",
"id_token": "id_token",
})
+ # 设置模拟返回值 - 用户信息获取响应
mock_do_get.return_value = json.dumps({
"picture": "picture",
"name": "name",
"sub": "sub",
"email": "email",
})
+
+ # 执行授权流程
token = google_app.get_access_token_by_code('code')
userinfo = google_app.get_oauth_userinfo()
- self.assertEqual(userinfo.token, 'access_token')
- self.assertEqual(userinfo.openid, 'sub')
+
+ # 验证用户信息正确性
+ self.assertEqual(userinfo.token, 'access_token') # 验证访问令牌
+ self.assertEqual(userinfo.openid, 'sub') # 验证用户唯一标识
@patch("oauth.oauthmanager.GitHubOauthManager.do_post")
@patch("oauth.oauthmanager.GitHubOauthManager.do_get")
def test_github_login(self, mock_do_get, mock_do_post):
+ """
+ 测试GitHub OAuth登录流程
+
+ 验证GitHub的特殊令牌响应格式和用户信息获取。
+ """
github_app = self.get_app_by_type('github')
- assert github_app
+ assert github_app # 验证GitHub应用存在
+
+ # 测试授权URL生成
url = github_app.get_authorization_url()
- self.assertTrue("github.com" in url)
- self.assertTrue("client_id" in url)
+ self.assertTrue("github.com" in url) # 验证URL包含GitHub域名
+ self.assertTrue("client_id" in url) # 验证URL包含客户端ID
+
+ # 设置模拟返回值 - GitHub特殊的查询字符串格式响应
mock_do_post.return_value = "access_token=gho_16C7e42F292c6912E7710c838347Ae178B4a&scope=repo%2Cgist&token_type=bearer"
+ # 设置模拟返回值 - 用户信息获取响应
mock_do_get.return_value = json.dumps({
"avatar_url": "avatar_url",
"name": "name",
"id": "id",
"email": "email",
})
+
+ # 执行授权流程
token = github_app.get_access_token_by_code('code')
userinfo = github_app.get_oauth_userinfo()
- self.assertEqual(userinfo.token, 'gho_16C7e42F292c6912E7710c838347Ae178B4a')
- self.assertEqual(userinfo.openid, 'id')
+
+ # 验证用户信息正确性
+ self.assertEqual(userinfo.token, 'gho_16C7e42F292c6912E7710c838347Ae178B4a') # 验证GitHub令牌
+ self.assertEqual(userinfo.openid, 'id') # 验证用户ID
@patch("oauth.oauthmanager.FaceBookOauthManager.do_post")
@patch("oauth.oauthmanager.FaceBookOauthManager.do_get")
def test_facebook_login(self, mock_do_get, mock_do_post):
+ """
+ 测试Facebook OAuth登录流程
+
+ 验证Facebook的令牌获取和嵌套头像数据结构处理。
+ """
facebook_app = self.get_app_by_type('facebook')
- assert facebook_app
+ assert facebook_app # 验证Facebook应用存在
+
+ # 测试授权URL生成
url = facebook_app.get_authorization_url()
- self.assertTrue("facebook.com" in url)
+ self.assertTrue("facebook.com" in url) # 验证URL包含Facebook域名
+
+ # 设置模拟返回值 - 令牌获取响应
mock_do_post.return_value = json.dumps({
"access_token": "access_token",
})
+ # 设置模拟返回值 - 用户信息获取响应(包含嵌套的头像数据)
mock_do_get.return_value = json.dumps({
"name": "name",
"id": "id",
@@ -134,14 +247,18 @@ class OauthLoginTest(TestCase):
}
}
})
+
+ # 执行授权流程
token = facebook_app.get_access_token_by_code('code')
userinfo = facebook_app.get_oauth_userinfo()
+
+ # 验证访问令牌正确性
self.assertEqual(userinfo.token, 'access_token')
@patch("oauth.oauthmanager.QQOauthManager.do_get", side_effect=[
- 'access_token=access_token&expires_in=3600',
- 'callback({"client_id":"appid","openid":"openid"} );',
- json.dumps({
+ 'access_token=access_token&expires_in=3600', # 第一次调用:令牌获取响应
+ 'callback({"client_id":"appid","openid":"openid"} );', # 第二次调用:OpenID获取响应
+ json.dumps({ # 第三次调用:用户信息获取响应
"nickname": "nickname",
"email": "email",
"figureurl": "figureurl",
@@ -149,21 +266,41 @@ class OauthLoginTest(TestCase):
})
])
def test_qq_login(self, mock_do_get):
+ """
+ 测试QQ OAuth登录流程
+
+ 验证QQ的特殊三步骤流程:获取令牌 → 获取OpenID → 获取用户信息。
+
+ Args:
+ mock_do_get: 配置了side_effect的模拟GET请求对象
+ """
qq_app = self.get_app_by_type('qq')
- assert qq_app
+ assert qq_app # 验证QQ应用存在
+
+ # 测试授权URL生成
url = qq_app.get_authorization_url()
- self.assertTrue("qq.com" in url)
+ self.assertTrue("qq.com" in url) # 验证URL包含QQ域名
+
+ # 执行授权流程(mock_do_get会根据side_effect顺序返回不同的响应)
token = qq_app.get_access_token_by_code('code')
userinfo = qq_app.get_oauth_userinfo()
+
+ # 验证访问令牌正确性
self.assertEqual(userinfo.token, 'access_token')
@patch("oauth.oauthmanager.WBOauthManager.do_post")
@patch("oauth.oauthmanager.WBOauthManager.do_get")
def test_weibo_authoriz_login_with_email(self, mock_do_get, mock_do_post):
+ """
+ 测试带邮箱的微博授权登录完整流程
+ 验证用户首次登录和重复登录的场景,确保用户认证状态正确。
+ """
+ # 设置模拟返回值 - 令牌获取响应
mock_do_post.return_value = json.dumps({"access_token": "access_token",
"uid": "uid"
})
+ # 设置模拟用户信息(包含邮箱)
mock_user_info = {
"avatar_large": "avatar_large",
"screen_name": "screen_name1",
@@ -172,25 +309,32 @@ class OauthLoginTest(TestCase):
}
mock_do_get.return_value = json.dumps(mock_user_info)
+ # 第一步:发起OAuth登录请求
response = self.client.get('/oauth/oauthlogin?type=weibo')
- self.assertEqual(response.status_code, 302)
- self.assertTrue("api.weibo.com" in response.url)
+ self.assertEqual(response.status_code, 302) # 验证重定向
+ self.assertTrue("api.weibo.com" in response.url) # 验证重定向到微博
+ # 第二步:模拟授权回调(首次登录)
response = self.client.get('/oauth/authorize?type=weibo&code=code')
- self.assertEqual(response.status_code, 302)
- self.assertEqual(response.url, '/')
+ self.assertEqual(response.status_code, 302) # 验证重定向
+ self.assertEqual(response.url, '/') # 验证重定向到首页
+ # 验证用户认证状态
user = auth.get_user(self.client)
- assert user.is_authenticated
+ assert user.is_authenticated # 验证用户已认证
self.assertTrue(user.is_authenticated)
- self.assertEqual(user.username, mock_user_info['screen_name'])
- self.assertEqual(user.email, mock_user_info['email'])
+ self.assertEqual(user.username, mock_user_info['screen_name']) # 验证用户名
+ self.assertEqual(user.email, mock_user_info['email']) # 验证邮箱
+
+ # 登出用户
self.client.logout()
+ # 第三步:模拟再次登录(测试重复登录场景)
response = self.client.get('/oauth/authorize?type=weibo&code=code')
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, '/')
+ # 验证用户再次认证状态
user = auth.get_user(self.client)
assert user.is_authenticated
self.assertTrue(user.is_authenticated)
@@ -200,10 +344,16 @@ class OauthLoginTest(TestCase):
@patch("oauth.oauthmanager.WBOauthManager.do_post")
@patch("oauth.oauthmanager.WBOauthManager.do_get")
def test_weibo_authoriz_login_without_email(self, mock_do_get, mock_do_post):
+ """
+ 测试不带邮箱的微博授权登录完整流程
+ 验证需要补充邮箱的场景,包括邮箱表单提交和邮箱确认流程。
+ """
+ # 设置模拟返回值 - 令牌获取响应
mock_do_post.return_value = json.dumps({"access_token": "access_token",
"uid": "uid"
})
+ # 设置模拟用户信息(不包含邮箱)
mock_user_info = {
"avatar_large": "avatar_large",
"screen_name": "screen_name1",
@@ -211,28 +361,34 @@ class OauthLoginTest(TestCase):
}
mock_do_get.return_value = json.dumps(mock_user_info)
+ # 第一步:发起OAuth登录请求
response = self.client.get('/oauth/oauthlogin?type=weibo')
self.assertEqual(response.status_code, 302)
self.assertTrue("api.weibo.com" in response.url)
+ # 第二步:模拟授权回调(应该重定向到邮箱补充页面)
response = self.client.get('/oauth/authorize?type=weibo&code=code')
-
self.assertEqual(response.status_code, 302)
+ # 解析OAuth用户ID从重定向URL中
oauth_user_id = int(response.url.split('/')[-1].split('.')[0])
self.assertEqual(response.url, f'/oauth/requireemail/{oauth_user_id}.html')
+ # 第三步:提交邮箱表单
response = self.client.post(response.url, {'email': 'test@gmail.com', 'oauthid': oauth_user_id})
-
self.assertEqual(response.status_code, 302)
+
+ # 生成邮箱确认签名
sign = get_sha256(settings.SECRET_KEY +
str(oauth_user_id) + settings.SECRET_KEY)
+ # 验证重定向到绑定成功页面
url = reverse('oauth:bindsuccess', kwargs={
'oauthid': oauth_user_id,
})
self.assertEqual(response.url, f'{url}?type=email')
+ # 第四步:模拟邮箱确认链接点击
path = reverse('oauth:email_confirm', kwargs={
'id': oauth_user_id,
'sign': sign
@@ -240,10 +396,12 @@ class OauthLoginTest(TestCase):
response = self.client.get(path)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, f'/oauth/bindsuccess/{oauth_user_id}.html?type=success')
+
+ # 验证最终用户认证状态和关联信息
user = auth.get_user(self.client)
from oauth.models import OAuthUser
oauth_user = OAuthUser.objects.get(author=user)
- self.assertTrue(user.is_authenticated)
- self.assertEqual(user.username, mock_user_info['screen_name'])
- self.assertEqual(user.email, 'test@gmail.com')
- self.assertEqual(oauth_user.pk, oauth_user_id)
+ self.assertTrue(user.is_authenticated) # 验证用户已认证
+ self.assertEqual(user.username, mock_user_info['screen_name']) # 验证用户名
+ self.assertEqual(user.email, 'test@gmail.com') # 验证补充的邮箱
+ self.assertEqual(oauth_user.pk, oauth_user_id) # 验证OAuth用户关联正确
\ No newline at end of file
diff --git a/src/DjangoBlog/oauth/urls.py b/src/DjangoBlog/oauth/urls.py
index c4a12a0..fce2abb 100644
--- a/src/DjangoBlog/oauth/urls.py
+++ b/src/DjangoBlog/oauth/urls.py
@@ -1,25 +1,53 @@
+"""
+OAuth 认证URL路由配置模块
+
+该模块定义了OAuth认证系统的所有URL路由,包括授权回调、邮箱验证、绑定成功等端点。
+这些路由处理第三方登录的完整流程,从初始授权到最终的用户绑定。
+"""
+
+# 导入Django URL路由相关模块
from django.urls import path
+# 导入当前应用的视图模块
from . import views
+# 定义应用命名空间,用于URL反向解析
app_name = "oauth"
+
+# 定义URL模式列表,将URL路径映射到对应的视图处理函数
urlpatterns = [
+ # OAuth授权回调端点 - 处理第三方平台返回的授权码
path(
- r'oauth/authorize',
- views.authorize),
+ r'oauth/authorize', # URL路径:/oauth/authorize
+ views.authorize, # 对应的视图函数
+ # 名称:未指定,使用默认(可通过views.authorize.__name__访问)
+ ),
+
+ # 邮箱补充页面 - 当第三方登录未提供邮箱时显示
path(
- r'oauth/requireemail/.html',
- views.RequireEmailView.as_view(),
- name='require_email'),
+ r'oauth/requireemail/.html', # URL路径,包含OAuth用户ID参数
+ views.RequireEmailView.as_view(), # 类视图,需要调用as_view()方法
+ name='require_email' # URL名称,用于反向解析
+ ),
+
+ # 邮箱确认端点 - 验证用户提交的邮箱地址
path(
- r'oauth/emailconfirm//.html',
- views.emailconfirm,
- name='email_confirm'),
+ r'oauth/emailconfirm//.html', # URL路径,包含用户ID和签名参数
+ views.emailconfirm, # 对应的视图函数
+ name='email_confirm' # URL名称,用于反向解析
+ ),
+
+ # 绑定成功页面 - 显示OAuth账号绑定成功信息
path(
- r'oauth/bindsuccess/.html',
- views.bindsuccess,
- name='bindsuccess'),
+ r'oauth/bindsuccess/.html', # URL路径,包含OAuth用户ID参数
+ views.bindsuccess, # 对应的视图函数
+ name='bindsuccess' # URL名称,用于反向解析
+ ),
+
+ # OAuth登录入口 - 初始化第三方登录流程
path(
- r'oauth/oauthlogin',
- views.oauthlogin,
- name='oauthlogin')]
+ r'oauth/oauthlogin', # URL路径:/oauth/oauthlogin
+ views.oauthlogin, # 对应的视图函数
+ name='oauthlogin' # URL名称,用于反向解析
+ )
+]
\ No newline at end of file
diff --git a/src/DjangoBlog/oauth/views.py b/src/DjangoBlog/oauth/views.py
index 12e3a6e..f34a9e4 100644
--- a/src/DjangoBlog/oauth/views.py
+++ b/src/DjangoBlog/oauth/views.py
@@ -1,119 +1,223 @@
+"""
+OAuth 认证视图模块
+
+该模块实现了OAuth认证系统的核心视图逻辑,处理第三方登录的完整流程。
+包括授权初始化、回调处理、邮箱验证、用户绑定等功能。
+"""
+
import logging
-# Create your views here.
-from urllib.parse import urlparse
+# 导入日志模块
+from urllib.parse import urlparse # 导入URL解析工具
+# 导入Django核心模块
from django.conf import settings
-from django.contrib.auth import get_user_model
-from django.contrib.auth import login
-from django.core.exceptions import ObjectDoesNotExist
-from django.db import transaction
-from django.http import HttpResponseForbidden
-from django.http import HttpResponseRedirect
-from django.shortcuts import get_object_or_404
-from django.shortcuts import render
-from django.urls import reverse
-from django.utils import timezone
-from django.utils.translation import gettext_lazy as _
-from django.views.generic import FormView
-
-from djangoblog.blog_signals import oauth_user_login_signal
-from djangoblog.utils import get_current_site
-from djangoblog.utils import send_email, get_sha256
-from oauth.forms import RequireEmailForm
-from .models import OAuthUser
-from .oauthmanager import get_manager_by_type, OAuthAccessTokenException
+from django.contrib.auth import get_user_model # 获取用户模型
+from django.contrib.auth import login # 用户登录功能
+from django.core.exceptions import ObjectDoesNotExist # 对象不存在异常
+from django.db import transaction # 数据库事务
+from django.http import HttpResponseForbidden # 403禁止访问响应
+from django.http import HttpResponseRedirect # 重定向响应
+from django.shortcuts import get_object_or_404 # 获取对象或404
+from django.shortcuts import render # 模板渲染
+from django.urls import reverse # URL反向解析
+from django.utils import timezone # 时区工具
+from django.utils.translation import gettext_lazy as _ # 国际化翻译
+from django.views.generic import FormView # 表单视图基类
+
+# 导入项目自定义模块
+from djangoblog.blog_signals import oauth_user_login_signal # 信号量
+from djangoblog.utils import get_current_site # 获取当前站点
+from djangoblog.utils import send_email, get_sha256 # 邮件发送和加密工具
+from oauth.forms import RequireEmailForm # 邮箱表单
+from .models import OAuthUser # OAuth用户模型
+from .oauthmanager import get_manager_by_type, OAuthAccessTokenException # OAuth管理器
+# 获取当前模块的日志记录器
logger = logging.getLogger(__name__)
def get_redirecturl(request):
+ """
+ 获取安全的重定向URL
+
+ 验证next_url参数的安全性,防止开放重定向漏洞。
+
+ Args:
+ request: HttpRequest对象
+
+ Returns:
+ str: 安全的跳转URL,默认为首页
+ """
+ # 从请求参数获取跳转URL
nexturl = request.GET.get('next_url', None)
+
+ # 如果nexturl为空或是登录页面,则重定向到首页
if not nexturl or nexturl == '/login/' or nexturl == '/login':
nexturl = '/'
return nexturl
+
+ # 解析URL以验证安全性
p = urlparse(nexturl)
+
+ # 检查URL是否指向外部域名(防止开放重定向攻击)
if p.netloc:
site = get_current_site().domain
+ # 比较域名(忽略www前缀),如果不匹配则视为非法URL
if not p.netloc.replace('www.', '') == site.replace('www.', ''):
logger.info('非法url:' + nexturl)
- return "/"
+ return "/" # 重定向到首页
+
return nexturl
def oauthlogin(request):
+ """
+ OAuth登录入口视图
+
+ 根据平台类型初始化第三方登录流程,重定向到对应平台的授权页面。
+
+ Args:
+ request: HttpRequest对象
+
+ Returns:
+ HttpResponseRedirect: 重定向到第三方授权页面或首页
+ """
+ # 从请求参数获取OAuth平台类型
type = request.GET.get('type', None)
if not type:
- return HttpResponseRedirect('/')
+ return HttpResponseRedirect('/') # 类型为空则重定向到首页
+
+ # 获取对应平台的OAuth管理器
manager = get_manager_by_type(type)
if not manager:
- return HttpResponseRedirect('/')
+ return HttpResponseRedirect('/') # 管理器不存在则重定向到首页
+
+ # 获取安全的跳转URL
nexturl = get_redirecturl(request)
+
+ # 生成第三方平台的授权URL
authorizeurl = manager.get_authorization_url(nexturl)
+
+ # 重定向到第三方授权页面
return HttpResponseRedirect(authorizeurl)
def authorize(request):
+ """
+ OAuth授权回调视图
+
+ 处理第三方平台返回的授权码,获取访问令牌和用户信息,
+ 完成用户认证或引导用户补充信息。
+
+ Args:
+ request: HttpRequest对象
+
+ Returns:
+ HttpResponseRedirect: 重定向到相应页面
+ """
+ # 从请求参数获取OAuth平台类型
type = request.GET.get('type', None)
if not type:
return HttpResponseRedirect('/')
+
+ # 获取对应平台的OAuth管理器
manager = get_manager_by_type(type)
if not manager:
return HttpResponseRedirect('/')
+
+ # 从请求参数获取授权码
code = request.GET.get('code', None)
+
try:
+ # 使用授权码获取访问令牌和用户信息
rsp = manager.get_access_token_by_code(code)
except OAuthAccessTokenException as e:
+ # 处理令牌获取异常
logger.warning("OAuthAccessTokenException:" + str(e))
return HttpResponseRedirect('/')
except Exception as e:
+ # 处理其他异常
logger.error(e)
rsp = None
+
+ # 获取安全的跳转URL
nexturl = get_redirecturl(request)
+
+ # 如果获取用户信息失败,重新跳转到授权页面
if not rsp:
return HttpResponseRedirect(manager.get_authorization_url(nexturl))
+
+ # 获取OAuth用户信息
user = manager.get_oauth_userinfo()
+
if user:
+ # 处理昵称为空的情况,生成默认昵称
if not user.nickname or not user.nickname.strip():
user.nickname = "djangoblog" + timezone.now().strftime('%y%m%d%I%M%S')
+
try:
+ # 检查是否已存在相同平台和OpenID的用户
temp = OAuthUser.objects.get(type=type, openid=user.openid)
+ # 更新现有用户信息
temp.picture = user.picture
temp.metadata = user.metadata
temp.nickname = user.nickname
user = temp
except ObjectDoesNotExist:
+ # 用户不存在,使用新用户对象
pass
- # facebook的token过长
+
+ # Facebook的token过长,清空存储
if type == 'facebook':
user.token = ''
+
+ # 如果用户有邮箱,直接完成登录流程
if user.email:
- with transaction.atomic():
+ with transaction.atomic(): # 使用事务保证数据一致性
author = None
try:
+ # 尝试获取已关联的本地用户
author = get_user_model().objects.get(id=user.author_id)
except ObjectDoesNotExist:
pass
+
+ # 如果没有关联的本地用户
if not author:
+ # 根据邮箱获取或创建本地用户
result = get_user_model().objects.get_or_create(email=user.email)
author = result[0]
+
+ # 如果是新创建的用户
if result[1]:
try:
+ # 检查昵称是否已被使用
get_user_model().objects.get(username=user.nickname)
except ObjectDoesNotExist:
+ # 昵称可用,设置为用户名
author.username = user.nickname
else:
+ # 昵称已被使用,生成唯一用户名
author.username = "djangoblog" + timezone.now().strftime('%y%m%d%I%M%S')
+
+ # 设置用户来源和保存
author.source = 'authorize'
author.save()
+ # 关联OAuth用户和本地用户
user.author = author
user.save()
+ # 发送用户登录信号
oauth_user_login_signal.send(
sender=authorize.__class__, id=user.id)
+
+ # 登录用户
login(request, author)
+
+ # 重定向到目标页面
return HttpResponseRedirect(nexturl)
else:
+ # 用户没有邮箱,保存用户信息并跳转到邮箱补充页面
user.save()
url = reverse('oauth:require_email', kwargs={
'oauthid': user.id
@@ -121,35 +225,68 @@ def authorize(request):
return HttpResponseRedirect(url)
else:
+ # 获取用户信息失败,重定向到目标页面
return HttpResponseRedirect(nexturl)
def emailconfirm(request, id, sign):
+ """
+ 邮箱确认视图
+
+ 验证邮箱确认链接的签名,完成OAuth用户与本地用户的绑定。
+
+ Args:
+ request: HttpRequest对象
+ id: OAuth用户ID
+ sign: 安全签名
+
+ Returns:
+ HttpResponseRedirect: 重定向到绑定成功页面
+ """
+ # 验证签名是否存在
if not sign:
return HttpResponseForbidden()
+
+ # 验证签名是否正确
if not get_sha256(settings.SECRET_KEY +
str(id) +
settings.SECRET_KEY).upper() == sign.upper():
return HttpResponseForbidden()
+
+ # 获取OAuth用户对象
oauthuser = get_object_or_404(OAuthUser, pk=id)
- with transaction.atomic():
+
+ with transaction.atomic(): # 使用事务保证数据一致性
+ # 处理用户关联
if oauthuser.author:
+ # 已有关联用户,直接获取
author = get_user_model().objects.get(pk=oauthuser.author_id)
else:
+ # 没有关联用户,根据邮箱创建或获取用户
result = get_user_model().objects.get_or_create(email=oauthuser.email)
author = result[0]
+
+ # 如果是新创建的用户
if result[1]:
- author.source = 'emailconfirm'
+ author.source = 'emailconfirm' # 设置用户来源
+ # 设置用户名(使用昵称或生成唯一用户名)
author.username = oauthuser.nickname.strip() if oauthuser.nickname.strip(
) else "djangoblog" + timezone.now().strftime('%y%m%d%I%M%S')
author.save()
+
+ # 保存用户关联关系
oauthuser.author = author
oauthuser.save()
+
+ # 发送用户登录信号
oauth_user_login_signal.send(
sender=emailconfirm.__class__,
id=oauthuser.id)
+
+ # 登录用户
login(request, author)
+ # 准备邮件内容
site = 'http://' + get_current_site().domain
content = _('''
Congratulations, you have successfully bound your email address. You can use
@@ -162,7 +299,10 @@ def emailconfirm(request, id, sign):
%(site)s
''') % {'oauthuser_type': oauthuser.type, 'site': site}
+ # 发送绑定成功邮件
send_email(emailto=[oauthuser.email, ], title=_('Congratulations on your successful binding!'), content=content)
+
+ # 重定向到绑定成功页面
url = reverse('oauth:bindsuccess', kwargs={
'oauthid': id
})
@@ -171,49 +311,96 @@ def emailconfirm(request, id, sign):
class RequireEmailView(FormView):
- form_class = RequireEmailForm
- template_name = 'oauth/require_email.html'
+ """
+ 邮箱补充表单视图
+
+ 当第三方登录未提供邮箱时,显示表单让用户输入邮箱地址。
+ """
+
+ form_class = RequireEmailForm # 使用的表单类
+ template_name = 'oauth/require_email.html' # 模板名称
def get(self, request, *args, **kwargs):
- oauthid = self.kwargs['oauthid']
+ """
+ GET请求处理
+
+ 检查OAuth用户是否已有邮箱,如有则跳过此步骤。
+ """
+ oauthid = self.kwargs['oauthid'] # 获取OAuth用户ID
oauthuser = get_object_or_404(OAuthUser, pk=oauthid)
+
+ # 如果用户已有邮箱,理论上应该跳过此步骤
if oauthuser.email:
pass
- # return HttpResponseRedirect('/')
+ # 这里可以添加重定向逻辑:return HttpResponseRedirect('/')
return super(RequireEmailView, self).get(request, *args, **kwargs)
def get_initial(self):
+ """
+ 设置表单初始值
+
+ Returns:
+ dict: 包含初始值的字典
+ """
oauthid = self.kwargs['oauthid']
return {
- 'email': '',
- 'oauthid': oauthid
+ 'email': '', # 邮箱初始值为空
+ 'oauthid': oauthid # 隐藏的OAuth用户ID
}
def get_context_data(self, **kwargs):
+ """
+ 添加上下文数据
+
+ 将OAuth用户的头像URL添加到模板上下文。
+ """
oauthid = self.kwargs['oauthid']
oauthuser = get_object_or_404(OAuthUser, pk=oauthid)
+
+ # 如果用户有头像,添加到上下文
if oauthuser.picture:
kwargs['picture'] = oauthuser.picture
+
return super(RequireEmailView, self).get_context_data(**kwargs)
def form_valid(self, form):
+ """
+ 表单验证通过后的处理
+
+ 保存用户邮箱,发送确认邮件。
+
+ Args:
+ form: 验证通过的表单实例
+
+ Returns:
+ HttpResponseRedirect: 重定向到邮件发送提示页面
+ """
+ # 获取表单数据
email = form.cleaned_data['email']
oauthid = form.cleaned_data['oauthid']
+
+ # 获取OAuth用户并更新邮箱
oauthuser = get_object_or_404(OAuthUser, pk=oauthid)
oauthuser.email = email
oauthuser.save()
+
+ # 生成安全签名
sign = get_sha256(settings.SECRET_KEY +
str(oauthuser.id) + settings.SECRET_KEY)
+
+ # 构建确认链接
site = get_current_site().domain
if settings.DEBUG:
- site = '127.0.0.1:8000'
+ site = '127.0.0.1:8000' # 调试模式使用本地地址
+
path = reverse('oauth:email_confirm', kwargs={
'id': oauthid,
'sign': sign
})
url = "http://{site}{path}".format(site=site, path=path)
+ # 准备邮件内容
content = _("""
Please click the link below to bind your email
@@ -225,29 +412,52 @@ class RequireEmailView(FormView):
%(url)s
""") % {'url': url}
+
+ # 发送确认邮件
send_email(emailto=[email, ], title=_('Bind your email'), content=content)
+
+ # 重定向到提示页面
url = reverse('oauth:bindsuccess', kwargs={
'oauthid': oauthid
})
- url = url + '?type=email'
+ url = url + '?type=email' # 添加类型参数
return HttpResponseRedirect(url)
def bindsuccess(request, oauthid):
+ """
+ 绑定成功页面视图
+
+ 根据绑定状态显示不同的成功信息。
+
+ Args:
+ request: HttpRequest对象
+ oauthid: OAuth用户ID
+
+ Returns:
+ HttpResponse: 渲染的绑定成功页面
+ """
+ # 获取绑定类型
type = request.GET.get('type', None)
oauthuser = get_object_or_404(OAuthUser, pk=oauthid)
+
+ # 根据类型设置不同的显示内容
if type == 'email':
+ # 邮箱已发送状态
title = _('Bind your email')
content = _(
'Congratulations, the binding is just one step away. '
'Please log in to your email to check the email to complete the binding. Thank you.')
else:
+ # 绑定完成状态
title = _('Binding successful')
content = _(
"Congratulations, you have successfully bound your email address. You can use %(oauthuser_type)s"
" to directly log in to this website without a password. You are welcome to continue to follow this site." % {
'oauthuser_type': oauthuser.type})
+
+ # 渲染绑定成功页面
return render(request, 'oauth/bindsuccess.html', {
'title': title,
'content': content
- })
+ })
\ No newline at end of file