.html',
+ views.bindsuccess,
+ name='bindsuccess'),
+
+ # OAuth登录入口
+ # 发起第三方登录请求的端点
+ path(
+ r'oauth/oauthlogin',
+ views.oauthlogin,
+ name='oauthlogin')
+]
\ No newline at end of file
diff --git a/src/oauth/views.py b/src/oauth/views.py
new file mode 100644
index 0000000..6dd92a9
--- /dev/null
+++ b/src/oauth/views.py
@@ -0,0 +1,313 @@
+import logging # 导入日志模块,用于记录系统运行日志
+# Create your views here.
+from urllib.parse import urlparse # 导入URL解析工具,用于验证跳转URL的合法性
+
+from django.conf import settings # 导入Django项目配置
+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 # 导入OAuth用户登录信号
+from djangoblog.utils import get_current_site # 导入获取当前站点信息的工具
+from djangoblog.utils import send_email, get_sha256 # 导入发送邮件和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的验证和清洗,确保跳转地址合法
+ :param request: HTTP请求对象
+ :return: 清洗后的合法跳转URL
+ """
+ nexturl = request.GET.get('next_url', None) # 从请求参数中获取跳转URL
+ # 处理默认情况:无next_url或指向登录页时,默认跳转到首页
+ if not nexturl or nexturl == '/login/' or nexturl == '/login':
+ nexturl = '/'
+ return nexturl
+ # 解析URL,验证域名合法性(防止跳转到外部恶意网站)
+ p = urlparse(nexturl)
+ if p.netloc: # 如果URL包含域名(非相对路径)
+ site = get_current_site().domain # 获取当前网站域名
+ # 比较跳转URL的域名与当前网站域名(忽略www.前缀)
+ if not p.netloc.replace('www.', '') == site.replace('www.', ''):
+ logger.info('非法url:' + nexturl) # 记录非法URL日志
+ return "/" # 非法URL时跳转到首页
+ return nexturl # 返回合法的跳转URL
+
+
+def oauthlogin(request):
+ """
+ 处理第三方登录请求,生成并跳转到第三方平台的授权页面
+ :param request: HTTP请求对象
+ :return: 重定向到第三方授权页面的响应
+ """
+ type = request.GET.get('type', None) # 获取第三方平台类型(如weibo、github等)
+ if not type: # 无平台类型时跳转到首页
+ return HttpResponseRedirect('/')
+ # 根据平台类型获取对应的OAuth管理器(如微博管理器、GitHub管理器)
+ manager = get_manager_by_type(type)
+ if not manager: # 管理器不存在时跳转到首页
+ return HttpResponseRedirect('/')
+ nexturl = get_redirecturl(request) # 获取并验证跳转URL
+ # 通过管理器生成第三方平台的授权URL(包含回调地址、state等参数)
+ authorizeurl = manager.get_authorization_url(nexturl)
+ return HttpResponseRedirect(authorizeurl) # 重定向到第三方授权页面
+
+
+def authorize(request):
+ """
+ 处理第三方平台的授权回调,验证授权码并完成用户登录/绑定流程
+ :param request: HTTP请求对象
+ :return: 重定向到目标页面或邮箱填写页的响应
+ """
+ type = request.GET.get('type', None) # 获取第三方平台类型
+ if not type: # 无平台类型时跳转到首页
+ return HttpResponseRedirect('/')
+ manager = get_manager_by_type(type) # 获取对应的OAuth管理器
+ if not manager: # 管理器不存在时跳转到首页
+ return HttpResponseRedirect('/')
+ code = request.GET.get('code', None) # 获取第三方平台返回的授权码
+ try:
+ # 使用授权码获取访问令牌(access token)
+ 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
+ nexturl = get_redirecturl(request) # 获取验证后的跳转URL
+ if not rsp: # 令牌获取失败时,重新生成授权URL并跳转
+ return HttpResponseRedirect(manager.get_authorization_url(nexturl))
+
+ # 通过令牌获取第三方用户信息(如昵称、头像、邮箱等)
+ 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:
+ # 查找是否已存在该第三方用户的关联记录
+ 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(因过长可能导致存储问题,此处清空)
+ if type == 'facebook':
+ user.token = ''
+
+ if user.email: # 若第三方用户信息包含邮箱(可直接绑定/创建本地用户)
+ with transaction.atomic(): # 使用数据库事务确保操作原子性
+ author = None
+ try:
+ # 尝试通过关联ID获取本地用户
+ 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]
+ # 若为新创建的用户(result[1]为True)
+ 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' # 标记用户来源为OAuth授权
+ author.save() # 保存本地用户信息
+
+ # 关联第三方用户记录与本地用户
+ user.author = author
+ user.save() # 保存第三方用户记录
+
+ # 发送OAuth用户登录信号(供其他模块监听处理)
+ oauth_user_login_signal.send(
+ sender=authorize.__class__, id=user.id)
+ login(request, author) # 登录本地用户
+ return HttpResponseRedirect(nexturl) # 重定向到目标页面
+ else: # 第三方用户信息不含邮箱(需要用户补充邮箱)
+ user.save() # 先保存第三方用户记录(无关联本地用户)
+ # 生成邮箱填写页的URL
+ url = reverse('oauth:require_email', kwargs={'oauthid': user.id})
+ return HttpResponseRedirect(url) # 重定向到邮箱填写页
+ else: # 未获取到用户信息时,跳转到目标页面
+ return HttpResponseRedirect(nexturl)
+
+
+def emailconfirm(request, id, sign):
+ """
+ 处理邮箱验证请求,完成第三方用户与本地用户的绑定
+ :param request: HTTP请求对象
+ :param id: OAuthUser记录ID
+ :param sign: 验证签名(防止恶意请求)
+ :return: 重定向到绑定成功页或403禁止访问
+ """
+ if not sign: # 无签名时返回403
+ return HttpResponseForbidden()
+ # 验证签名合法性(使用SECRET_KEY和ID生成SHA256比对)
+ if not get_sha256(settings.SECRET_KEY + str(id) + settings.SECRET_KEY).upper() == sign.upper():
+ return HttpResponseForbidden() # 签名不匹配时返回403
+
+ # 获取对应的第三方用户记录(不存在则返回404)
+ oauthuser = get_object_or_404(OAuthUser, pk=id)
+ 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.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用户登录信号
+ 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
+ %(oauthuser_type)s to directly log in to this website without a password.
+ You are welcome to continue to follow this site, the address is
+ %(site)s
+ Thank you again!
+
+ If the link above cannot be opened, please copy this link to your browser.
+ %(site)s
+ ''') % {'oauthuser_type': oauthuser.type, 'site': site}
+
+ # 发送绑定成功邮件
+ send_email(emailto=[oauthuser.email, ], title=_('Congratulations on your successful binding!'), content=content)
+ # 生成绑定成功页URL
+ url = reverse('oauth:bindsuccess', kwargs={'oauthid': id})
+ url = url + '?type=success' # 添加成功标识参数
+ return HttpResponseRedirect(url) # 重定向到绑定成功页
+
+
+class RequireEmailView(FormView):
+ """处理邮箱填写的表单视图,继承自Django的FormView"""
+ form_class = RequireEmailForm # 指定使用的表单类
+ template_name = 'oauth/require_email.html' # 指定渲染的模板
+
+ def get(self, request, *args, **kwargs):
+ """处理GET请求:获取第三方用户记录,若已填写邮箱则跳转(注释中逻辑)"""
+ oauthid = self.kwargs['oauthid'] # 从URL参数中获取第三方用户ID
+ oauthuser = get_object_or_404(OAuthUser, pk=oauthid) # 获取第三方用户记录
+ if oauthuser.email:
+ pass # 若已填写邮箱,此处可添加跳转逻辑(当前注释)
+ # 调用父类的GET方法,渲染表单页面
+ return super(RequireEmailView, self).get(request, *args, **kwargs)
+
+ def get_initial(self):
+ """设置表单初始值:预填第三方用户ID"""
+ oauthid = self.kwargs['oauthid']
+ return {'email': '', 'oauthid': oauthid} # 邮箱为空,ID为URL参数中的值
+
+ def get_context_data(self, **kwargs):
+ """扩展上下文数据:添加第三方用户的头像(用于模板显示)"""
+ 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):
+ """处理合法的表单提交:保存邮箱并发送验证邮件"""
+ email = form.cleaned_data['email'] # 获取清洗后的邮箱
+ oauthid = form.cleaned_data['oauthid'] # 获取第三方用户ID
+ 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)
+ # 获取当前站点域名(开发环境使用127.0.0.1:8000)
+ site = get_current_site().domain
+ if settings.DEBUG:
+ 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
+
+ %(url)s
+
+ Thank you again!
+
+ If the link above cannot be opened, please copy this link to your browser.
+
+ %(url)s
+ """) % {'url': url}
+ # 发送验证邮件
+ send_email(emailto=[email, ], title=_('Bind your email'), content=content)
+ # 生成绑定成功页URL(带邮件发送标识)
+ url = reverse('oauth:bindsuccess', kwargs={'oauthid': oauthid})
+ url = url + '?type=email'
+ return HttpResponseRedirect(url) # 重定向到绑定成功页
+
+
+def bindsuccess(request, oauthid):
+ """
+ 显示绑定成功页面(根据类型显示不同内容)
+ :param request: HTTP请求对象
+ :param oauthid: 第三方用户ID
+ :return: 渲染绑定成功页面的响应
+ """
+ type = request.GET.get('type', None) # 获取类型参数(email或success)
+ 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