|
|
|
|
@ -0,0 +1,306 @@
|
|
|
|
|
import logging
|
|
|
|
|
from django.utils.translation import gettext_lazy as _
|
|
|
|
|
from django.conf import settings
|
|
|
|
|
from django.contrib import auth
|
|
|
|
|
from django.contrib.auth import REDIRECT_FIELD_NAME
|
|
|
|
|
from django.contrib.auth import get_user_model
|
|
|
|
|
from django.contrib.auth import logout
|
|
|
|
|
from django.contrib.auth.forms import AuthenticationForm
|
|
|
|
|
from django.contrib.auth.hashers import make_password
|
|
|
|
|
from django.http import HttpResponseRedirect, HttpResponseForbidden
|
|
|
|
|
from django.http.request import HttpRequest
|
|
|
|
|
from django.http.response import HttpResponse
|
|
|
|
|
from django.shortcuts import get_object_or_404
|
|
|
|
|
from django.shortcuts import render
|
|
|
|
|
from django.urls import reverse
|
|
|
|
|
from django.utils.decorators import method_decorator
|
|
|
|
|
from django.utils.http import url_has_allowed_host_and_scheme
|
|
|
|
|
from django.views import View
|
|
|
|
|
from django.views.decorators.cache import never_cache
|
|
|
|
|
from django.views.decorators.csrf import csrf_protect
|
|
|
|
|
from django.views.decorators.debug import sensitive_post_parameters
|
|
|
|
|
from django.views.generic import FormView, RedirectView
|
|
|
|
|
|
|
|
|
|
# 导入项目公共工具函数:邮件发送、加密、站点信息、验证码生成、缓存删除
|
|
|
|
|
from djangoblog.utils import send_email, get_sha256, get_current_site, generate_code, delete_sidebar_cache
|
|
|
|
|
from . import utils # 导入accounts应用自定义工具(验证码发送/存储)
|
|
|
|
|
from .forms import RegisterForm, LoginForm, ForgetPasswordForm, ForgetPasswordCodeForm # 导入用户交互表单
|
|
|
|
|
from .models import BlogUser # 导入自定义用户模型
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__) # 初始化日志对象,用于记录视图层操作日志
|
|
|
|
|
|
|
|
|
|
# 模块级注释——accounts应用的核心视图文件,
|
|
|
|
|
# 包含用户注册、登录、退出、邮箱验证、密码重置等核心业务的视图实现,
|
|
|
|
|
# 基于Django类视图(FormView/RedirectView/View)和函数视图,
|
|
|
|
|
# 整合表单验证、邮件发送、缓存操作、权限控制等逻辑,处理用户交互的全流程
|
|
|
|
|
# Create your views here.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class RegisterView(FormView):
|
|
|
|
|
"""
|
|
|
|
|
用户注册视图,继承Django FormView(专门处理表单提交的类视图)
|
|
|
|
|
核心功能:接收注册表单数据、验证合法性、创建未激活用户、发送邮箱验证链接、重定向到结果页
|
|
|
|
|
"""
|
|
|
|
|
form_class = RegisterForm # 关联注册表单类(处理用户名、邮箱、密码验证)
|
|
|
|
|
template_name = 'account/registration_form.html' # 注册页面模板路径
|
|
|
|
|
|
|
|
|
|
@method_decorator(csrf_protect)
|
|
|
|
|
def dispatch(self, *args, **kwargs):
|
|
|
|
|
"""
|
|
|
|
|
重写分发方法,添加CSRF防护装饰器
|
|
|
|
|
防止跨站请求伪造攻击,确保注册请求来自本网站合法表单
|
|
|
|
|
"""
|
|
|
|
|
return super(RegisterView, self).dispatch(*args, **kwargs)
|
|
|
|
|
|
|
|
|
|
def form_valid(self, form):
|
|
|
|
|
"""
|
|
|
|
|
表单验证通过后的核心处理逻辑(FormView的核心方法)
|
|
|
|
|
流程:创建未激活用户 → 生成邮箱验证链接 → 发送验证邮件 → 重定向到注册结果页
|
|
|
|
|
"""
|
|
|
|
|
if form.is_valid():
|
|
|
|
|
# 1. 表单验证通过,先不提交到数据库(commit=False),后续补充字段
|
|
|
|
|
user = form.save(False)
|
|
|
|
|
user.is_active = False # 默认设置用户为未激活状态(需邮箱验证后激活)
|
|
|
|
|
user.source = 'Register' # 标记用户创建来源:前台注册(区别于后台创建/第三方登录)
|
|
|
|
|
user.save(True) # 最终保存用户数据到数据库
|
|
|
|
|
|
|
|
|
|
# 2. 生成邮箱验证链接(包含站点域名、用户ID、加密签名,确保链接安全性)
|
|
|
|
|
site = get_current_site().domain # 获取当前站点域名(如"example.com")
|
|
|
|
|
# 双重SHA256加密:结合SECRET_KEY和用户ID,防止链接被篡改
|
|
|
|
|
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
|
|
|
|
|
|
|
|
|
|
# 开发环境适配:DEBUG模式下使用本地测试域名(127.0.0.1:8000)
|
|
|
|
|
if settings.DEBUG:
|
|
|
|
|
site = '127.0.0.1:8000'
|
|
|
|
|
path = reverse('account:result') # 验证结果页路由
|
|
|
|
|
# 拼接完整的验证链接(HTTP协议,开发环境可用;生产环境建议改为HTTPS)
|
|
|
|
|
url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format(
|
|
|
|
|
site=site, path=path, id=user.id, sign=sign)
|
|
|
|
|
|
|
|
|
|
# 3. 构建验证邮件内容(HTML格式,包含验证链接)
|
|
|
|
|
content = """
|
|
|
|
|
<p>请点击下面链接验证您的邮箱</p>
|
|
|
|
|
|
|
|
|
|
<a href="{url}" rel="bookmark">{url}</a>
|
|
|
|
|
|
|
|
|
|
再次感谢您!
|
|
|
|
|
<br />
|
|
|
|
|
如果上面链接无法打开,请将此链接复制至浏览器。
|
|
|
|
|
{url}
|
|
|
|
|
""".format(url=url)
|
|
|
|
|
# 发送验证邮件到用户注册邮箱
|
|
|
|
|
send_email(
|
|
|
|
|
emailto=[user.email], # 收件人(注册时填写的邮箱)
|
|
|
|
|
title='验证您的电子邮箱', # 邮件标题
|
|
|
|
|
content=content) # 邮件HTML内容
|
|
|
|
|
|
|
|
|
|
# 4. 重定向到注册结果页(告知用户验证邮件已发送)
|
|
|
|
|
url = reverse('accounts:result') + '?type=register&id=' + str(user.id)
|
|
|
|
|
return HttpResponseRedirect(url)
|
|
|
|
|
else:
|
|
|
|
|
# 表单验证失败(如用户名已存在、密码不匹配),重新渲染注册页面并显示错误
|
|
|
|
|
return self.render_to_response({'form': form})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class LogoutView(RedirectView):
|
|
|
|
|
"""
|
|
|
|
|
用户退出登录视图,继承Django RedirectView(专门处理重定向的类视图)
|
|
|
|
|
核心功能:执行退出登录逻辑、清理缓存、重定向到登录页
|
|
|
|
|
"""
|
|
|
|
|
url = '/login/' # 退出后重定向的目标URL(登录页)
|
|
|
|
|
|
|
|
|
|
@method_decorator(never_cache)
|
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
|
|
|
"""
|
|
|
|
|
重写分发方法,添加never_cache装饰器
|
|
|
|
|
禁止浏览器缓存退出页,避免用户后退到已登录状态
|
|
|
|
|
"""
|
|
|
|
|
return super(LogoutView, self).dispatch(request, *args, **kwargs)
|
|
|
|
|
|
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
|
|
|
"""
|
|
|
|
|
处理GET请求(退出登录请求)
|
|
|
|
|
流程:执行退出登录 → 清理侧边栏缓存 → 重定向到登录页
|
|
|
|
|
"""
|
|
|
|
|
logout(request) # Django内置logout函数:清除用户会话,销毁登录状态
|
|
|
|
|
delete_sidebar_cache() # 清除侧边栏缓存(避免缓存中保留用户相关信息)
|
|
|
|
|
return super(LogoutView, self).get(request, *args, **kwargs)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class LoginView(FormView):
|
|
|
|
|
"""
|
|
|
|
|
用户登录视图,继承Django FormView
|
|
|
|
|
核心功能:接收登录表单数据、验证合法性、执行登录逻辑、处理"记住我"、重定向到目标页面
|
|
|
|
|
"""
|
|
|
|
|
form_class = LoginForm # 关联自定义登录表单(适配前端样式)
|
|
|
|
|
template_name = 'account/login.html' # 登录页面模板路径
|
|
|
|
|
success_url = '/' # 登录成功后的默认重定向URL(网站根目录)
|
|
|
|
|
redirect_field_name = REDIRECT_FIELD_NAME # 重定向字段名(默认'redirect_to',用于跳转前的目标页面)
|
|
|
|
|
login_ttl = 2626560 # "记住我"的会话有效期:2626560秒 ≈ 1个月
|
|
|
|
|
|
|
|
|
|
@method_decorator(sensitive_post_parameters('password'))
|
|
|
|
|
@method_decorator(csrf_protect)
|
|
|
|
|
@method_decorator(never_cache)
|
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
|
|
|
"""
|
|
|
|
|
重写分发方法,添加3个核心装饰器:
|
|
|
|
|
1. sensitive_post_parameters('password'):保护密码字段,避免在错误报告中泄露
|
|
|
|
|
2. csrf_protect:防跨站请求伪造攻击
|
|
|
|
|
3. never_cache:禁止浏览器缓存登录页,确保每次请求都是最新状态
|
|
|
|
|
"""
|
|
|
|
|
return super(LoginView, self).dispatch(request, *args, **kwargs)
|
|
|
|
|
|
|
|
|
|
def get_context_data(self, **kwargs):
|
|
|
|
|
"""
|
|
|
|
|
补充模板上下文数据(传递重定向地址给模板)
|
|
|
|
|
用于登录成功后跳转到登录前的目标页面(如访问需要登录的页面时,先跳转登录,登录后返回原页面)
|
|
|
|
|
"""
|
|
|
|
|
# 从GET请求中获取重定向地址(如?redirect_to=/article/1/)
|
|
|
|
|
redirect_to = self.request.GET.get(self.redirect_field_name)
|
|
|
|
|
if redirect_to is None:
|
|
|
|
|
redirect_to = '/' # 无重定向地址时,默认跳转到根目录
|
|
|
|
|
kwargs['redirect_to'] = redirect_to # 将重定向地址添加到上下文
|
|
|
|
|
return super(LoginView, self).get_context_data(** kwargs)
|
|
|
|
|
|
|
|
|
|
def form_valid(self, form):
|
|
|
|
|
"""
|
|
|
|
|
表单验证通过后的核心登录逻辑
|
|
|
|
|
流程:验证表单 → 清理缓存 → 执行登录 → 处理"记住我" → 重定向
|
|
|
|
|
"""
|
|
|
|
|
# 重新初始化Django内置AuthenticationForm(确保使用正确的认证逻辑)
|
|
|
|
|
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())
|
|
|
|
|
# 处理"记住我"选项:若勾选,则设置会话有效期为1个月
|
|
|
|
|
if self.request.POST.get("remember"):
|
|
|
|
|
self.request.session.set_expiry(self.login_ttl)
|
|
|
|
|
# 调用父类form_valid方法,执行重定向
|
|
|
|
|
return super(LoginView, self).form_valid(form)
|
|
|
|
|
else:
|
|
|
|
|
# 表单验证失败(如用户名/密码错误),重新渲染登录页并显示错误
|
|
|
|
|
return self.render_to_response({'form': form})
|
|
|
|
|
|
|
|
|
|
def get_success_url(self):
|
|
|
|
|
"""
|
|
|
|
|
自定义登录成功后的重定向URL
|
|
|
|
|
核心:校验重定向地址的合法性,避免恶意重定向攻击
|
|
|
|
|
"""
|
|
|
|
|
# 从POST请求中获取目标重定向地址(登录表单中隐藏字段传递)
|
|
|
|
|
redirect_to = self.request.POST.get(self.redirect_field_name)
|
|
|
|
|
# 校验重定向地址是否合法(仅允许本网站域名下的地址)
|
|
|
|
|
if not url_has_allowed_host_and_scheme(
|
|
|
|
|
url=redirect_to, allowed_hosts=[self.request.get_host()]):
|
|
|
|
|
redirect_to = self.success_url # 非法地址则使用默认重定向地址(根目录)
|
|
|
|
|
return redirect_to
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def account_result(request):
|
|
|
|
|
"""
|
|
|
|
|
账号操作结果展示函数视图
|
|
|
|
|
处理两种场景:1. 注册成功后的提示 2. 邮箱验证后的激活与提示
|
|
|
|
|
核心:根据URL参数区分场景,验证链接合法性,激活用户,渲染结果页面
|
|
|
|
|
"""
|
|
|
|
|
# 从GET请求中获取操作类型(register/validation)和用户ID
|
|
|
|
|
type = request.GET.get('type')
|
|
|
|
|
id = request.GET.get('id')
|
|
|
|
|
|
|
|
|
|
# 根据用户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':
|
|
|
|
|
# 场景1:注册成功提示(告知用户验证邮件已发送)
|
|
|
|
|
content = '''
|
|
|
|
|
恭喜您注册成功,一封验证邮件已经发送到您的邮箱,请验证您的邮箱后登录本站。
|
|
|
|
|
'''
|
|
|
|
|
title = '注册成功'
|
|
|
|
|
else:
|
|
|
|
|
# 场景2:邮箱验证(校验链接签名合法性,激活用户)
|
|
|
|
|
# 重新计算签名(与注册时的加密规则一致)
|
|
|
|
|
c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
|
|
|
|
|
sign = request.GET.get('sign') # 从URL中获取传递的签名
|
|
|
|
|
|
|
|
|
|
# 签名不匹配则返回403禁止访问(防止恶意篡改链接)
|
|
|
|
|
if sign != c_sign:
|
|
|
|
|
return HttpResponseForbidden()
|
|
|
|
|
|
|
|
|
|
# 签名匹配,激活用户(设置is_active为True)
|
|
|
|
|
user.is_active = True
|
|
|
|
|
user.save()
|
|
|
|
|
# 验证成功提示
|
|
|
|
|
content = '''
|
|
|
|
|
恭喜您已经成功的完成邮箱验证,您现在可以使用您的账号来登录本站。
|
|
|
|
|
'''
|
|
|
|
|
title = '验证成功'
|
|
|
|
|
# 渲染结果页面,传递标题和内容
|
|
|
|
|
return render(request, 'account/result.html', {
|
|
|
|
|
'title': title,
|
|
|
|
|
'content': content
|
|
|
|
|
})
|
|
|
|
|
else:
|
|
|
|
|
# 非法操作类型,重定向到首页
|
|
|
|
|
return HttpResponseRedirect('/')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ForgetPasswordView(FormView):
|
|
|
|
|
"""
|
|
|
|
|
忘记密码重置视图,继承Django 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()
|
|
|
|
|
# 哈希处理新密码(make_password自动使用Django配置的哈希算法,避免明文存储)
|
|
|
|
|
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):
|
|
|
|
|
"""
|
|
|
|
|
忘记密码验证码发送视图,继承Django View
|
|
|
|
|
核心功能:接收邮箱 → 验证邮箱合法性 → 生成验证码 → 发送验证邮件 → 存储验证码到缓存
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def post(self, request: HttpRequest):
|
|
|
|
|
"""
|
|
|
|
|
处理POST请求(接收邮箱,发送验证码)
|
|
|
|
|
"""
|
|
|
|
|
# 初始化忘记密码验证码表单,验证请求数据
|
|
|
|
|
form = ForgetPasswordCodeForm(request.POST)
|
|
|
|
|
if not form.is_valid():
|
|
|
|
|
return HttpResponse("错误的邮箱") # 表单验证失败(如邮箱格式错误),返回错误提示
|
|
|
|
|
|
|
|
|
|
# 表单验证通过,获取目标邮箱
|
|
|
|
|
to_email = form.cleaned_data["email"]
|
|
|
|
|
|
|
|
|
|
# 生成随机验证码(项目工具函数)
|
|
|
|
|
code = generate_code()
|
|
|
|
|
# 发送验证码邮件(调用accounts应用自定义工具函数)
|
|
|
|
|
utils.send_verify_email(to_email, code)
|
|
|
|
|
# 存储验证码到缓存(key=邮箱,value=验证码,有效期5分钟,由utils模块的_code_ttl控制)
|
|
|
|
|
utils.set_code(to_email, code)
|
|
|
|
|
|
|
|
|
|
return HttpResponse("ok") # 验证码发送成功,返回"ok"提示n
|