.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