Django之第三方平台QQ授权登录的实现
接入指南:https://wiki.connect.qq.com/成为开发者
准备工作
成为开发者
首先要有一个开发者账号,https://connect.qq.com/
登录后点击用户头像,修改个人信息
修改完信息后会提交系统审核,点击应用管理,可以看到审核状态,审核完毕后就可以创建应用了。(两三天的审核时间)
创建应用
https://wiki.connect.qq.com/__trashed-2
需要审核通过。应用图标必须是100px*100px大小的,否则不予通过。
功能实现
创建应用模块
创建一个新的应用oauth,用来实现QQ第三方认证登录的代码编写。
python manage.py startapp oauth
在settings.py
中注册应用
INSTALLED_APPS = [
# ...
"oauth",
]
设置总路由urls.py
path('api/oauth/', include(('oauth.urls', 'oauth'), namespace='oauth')),
定义QQ登录模型类
在oauth/models.py中定义QQ身份(openid)与用户模型类User的关联关系
from django.db import models
class BaseModel(models.Model):
is_deleted = models.BooleanField(default=False, verbose_name="是否删除")
created_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
updated_time = models.DateTimeField(auto_now=True, verbose_name="更新时间")
class Meta:
# 不创建该表(抽象模型类)
abstract = True
class OAuthQQ(BaseModel):
user = models.ForeignKey("users.UserInfo", verbose_name="用户", on_delete=models.CASCADE)
openid = models.CharField(verbose_name="openid", max_length=64, db_index=True)
class Meta:
db_table = "lg_oauth_qq"
verbose_name = "请求登录"
verbose_name_plural = verbose_name
执行迁移
执行迁移操作,生成QQ登录模型类对应的数据库表
python manage.py makemigrations
python manage.py migrate
QQLoginTool库
安装QQLoginTool
pip install QQLoginTool
导入
from QQLoginTool.QQtool import OAuthQQ
初始化OAuthQQ对象
oauth = OAuthQQ(client_id=settings.QQ_CLIENT_ID, client_secret=settings.QQ_CLIENT_SECRET, redirect_uri=settings.QQ_REDIRECT_URI, state=next)
获取QQ登录扫码页面,扫码后得到Authorization Code
login_url = oauth.get_qq_url()
通过Authorization Code获取Access Token
access_token = oauth.get_access_token(code)
通过Access Token获取OpenID
openid = oauth.get_open_id(access_token)
在settings.py配置QQ登录参数
# QQ登录相关
QQ_CLIENT_ID = '102060113'
QQ_CLIENT_SECRET = '**********'
QQ_REDIRECT_URI = 'http://www.***.com/api/oauth/qq/callback/'
报错解决:
# 如果有以下报错,直接把源码放到项目里面即可
# ModuleNotFoundError: No module named 'QQLoginTool'
QQ登录扫码页面
from django.urls import path, re_path
from . import views
urlpatterns = [
# qq登录地址
path("qq/login/", views.OAuthQQUrlView.as_view()),
]
from django.shortcuts import render
from django.views import View
from django import http
from django.conf import settings
from QQLoginTool.QQtool import OAuthQQ
from response_code import RETCODE, err_msg
class OAuthQQUrlView(View):
def get(self, request):
# next: 从哪个页面进入到的登录页面,登录成功后自动回到那个页面
next_url = request.GET.get('next')
# 获取QQ登录页面网址
oauth = OAuthQQ(client_id=settings.QQ_CLIENT_ID, client_secret=settings.QQ_CLIENT_SECRET,
redirect_uri=settings.QQ_REDIRECT_URI, state=next_url)
login_url = oauth.get_qq_url()
return http.JsonResponse({"code": RETCODE.OK, "msg": "成功", "login_url": login_url})
认证获取openid
1.用户在QQ登录成功后,QQ会将用户重定向到配置的回调网址,同时会传递一个Authorization Code
2.拿到Authorization Code并完成OAuth2.0认证获取openid
注意:回调网址在申请QQ登录开发资质时进行配置
from django.urls import path, re_path
from . import views
urlpatterns = [
# qq登录地址
path("qq/login/", views.OAuthQQUrlView.as_view()),
# qq回调地址
path("qq/callback/", views.OAuthQQCallbacklView.as_view()),
]
使用code向QQ服务器请求,获取access_token
使用access_token向QQ服务器请求获取openid
"""用户扫码登录的回调处理"""
class QQAuthUserView(View):
def get(self, request):
"""Oauth2.0认证"""
# 提取code请求参数
code = request.GET.get('code')
if not code:
return http.HttpResponseBadRequest('缺少code')
# 创建oauth 对象
oauth = OAuthQQ(client_id=settings.QQ_CLIENT_ID, client_secret=settings.QQ_CLIENT_SECRET,
redirect_uri=settings.QQ_REDIRECT_URI)
try:
# 使用code向QQ服务器请求access_token
access_token = oauth.get_access_token(code)
# 使用access_token向QQ服务器请求openid
openid = oauth.get_open_id(access_token)
except Exception as e:
logger.error(e)
return http.HttpResponseServerError('OAuth2.0认证失败')
openid的判断处理
openid是否绑定过用户
判断openid是否绑定过用户,只需要使用openid查询该QQ用户是否绑定过用户即可。
oauth_user = OAuthQQUser.objects.get(openid=openid)
openid已绑定用户
如果openid已绑定用户,直接生成状态保持信息,登录成功,并重定向到首页。
from django.shortcuts import render, redirect
from django.views import View
from django import http
from django.conf import settings
from django.contrib.auth import login
from QQLoginTool.QQtool import OAuthQQ
from response_code import RETCODE, err_msg
from logger import log
from .models import OAuthQQ as OAuthQQUser
try:
oauth_user = OAuthQQUser.objects.get(openid=openid)
except OAuthQQUser.DoesNotExist:
# 如果openid没有绑定用户
pass
else:
# 如果openid已绑定用户
# 登录
login(request, oauth_user.user)
# 响应结果
next_url = request.GET.get("next")
# 页面跳转
response = redirect(next_url)
# 状态保持
response.set_cookie('username', oauth_user.user.username, settings.SESSION_COOKIE_AGE)
return response
openid未绑定用户
openid属于用户隐私信息,在后续的绑定用户操作中前端会使用openid,因此需要将openid签名处理,避免暴露。
from authlib_jwt import generate_access_token
try:
oauth_user = OAuthQQUser.objects.get(openid=openid)
except OAuthQQUser.DoesNotExist:
# 如果openid没绑定用户 generate_eccess_token:对openid签名
access_token = generate_eccess_token(openid)
context = {'access_token_openid': access_token.decode()}
return render(request, 'oauthCallback.html', context)
else:
qq_user = oauth_user.user
login(request, qq_user)
response = redirect(reverse('contents:index'))
response.set_cookie('username', qq_user.username, max_age=3600 * 24 * 15)
return response
oauthCallback.html中渲染access_token
<input type="hidden" name="access_token" value="{{ access_token }}">
openid签名处理
签名处理可以使用authlib库
pip install authlib
from authlib.jose import jwt, JoseError
def generate_access_token(openid):
"""
加密函数
:param openid: 加密的数据
:return: 加密后的数据
"""
# 签名算法
header = {'alg': 'HS256'}
# 待签名的数据负载
data = {"openid": openid}
# 生成token
token = jwt.encode(header=header, payload=data, key=settings.SECRET_KEY)
return token
def check_access_token(openid):
"""
校验authlib签名函数
:param openid: 加密后的token
:return: user对象或None
"""
try:
data = jwt.decode(openid, settings.SECRET_KEY)
except JoseError:
return None
else:
# 拿到解密后的数据
return data.get("openid")
openid绑定用户
openid绑定用户的过程类似于用户注册的业务逻辑
import re
from django.shortcuts import render, redirect
from django.views import View
from django import http
from django.conf import settings
from django.contrib.auth import login
from django_redis import get_redis_connection
from QQLoginTool.QQtool import OAuthQQ
from response_code import RETCODE
from logger import log
from .models import OAuthQQ as OAuthQQUser
from users.models import UserInfo
from authlib_jwt import generate_access_token, check_access_token
class QQAuthUserView(View):
"""用户扫码登录的回调处理"""
def get(self, request):
"""Oauth2.0认证"""
......
def post(self, request):
"""用户绑定openid"""
# 接收参数
mobile = request.POST.get('mobile')
password = request.POST.get('password')
sms_code_client = request.POST.get('sms_code')
access_token = request.POST.get('access_token_openid')
# 判断参数是否齐全
if not all([mobile, password, sms_code_client]):
return http.HttpResponseBadRequest('缺少必传参数')
# 判断手机号是否合法
if not re.match(r'^1[3-9]\d{9}$', mobile):
return http.HttpResponseBadRequest('请输入正确的手机号码')
# 判断密码是否合格
if not re.match(r'^[0-9A-Za-z]{8,20}$', password):
return http.HttpResponseBadRequest('请输入8-20位的密码')
# 判断短信验证码是否一致
redis_conn = get_redis_connection('verifications')
sms_code_server = redis_conn.get('sms_code_%s' % mobile)
if sms_code_server is None:
return render(request, 'oauth_callback.html', {'msg': '无效的短信验证码'})
if sms_code_client != sms_code_server.decode():
return render(request, 'oauth_callback.html', {'msg': '输入短信验证码有误'})
# 判断openid是否有效
openid = check_access_token(access_token)
if not openid:
return render(request, 'oauth_callback.html', {'msg': '无效的openid'})
# 保存注册数据
try:
user = UserInfo.objects.get(mobile=mobile)
except UserInfo.DoesNotExist:
# 不存在,则创建新用户
try:
user = UserInfo.objects.create_user(username=mobile, mobile=mobile, password=password)
except Exception as e:
log.error(e)
return render(request, 'oauth_callback.html', {'msg': '创建用户失败.'})
else:
if not user.check_password(password):
return render(request, 'oauth_callback.html', {'msg': '密码错误.'})
# 将用户绑定openid
try:
OAuthQQUser.objects.create(user=user, openid=openid)
except Exception as e:
log.error(e)
return render(request, 'oauth_callback.html', {'msg': 'QQ登录失败.'})
# 实现状态保持
login(request, user)
# 响应绑定结果
state = request.GET.get('state')
# 页面跳转
response = redirect(state)
# 写入cookie
response.set_cookie('username', user.username, settings.SESSION_COOKIE_AGE)
return response