Django之第三方平台QQ授权登录的实现

发布时间 2023-08-17 10:45:56作者: hkwJsxl

接入指南:https://wiki.connect.qq.com/成为开发者

准备工作

成为开发者

首先要有一个开发者账号,https://connect.qq.com/

登录后点击用户头像,修改个人信息

image-20230805173100784

image-20230805173124743

修改完信息后会提交系统审核,点击应用管理,可以看到审核状态,审核完毕后就可以创建应用了。(两三天的审核时间)

image-20230805173239654

创建应用

https://wiki.connect.qq.com/__trashed-2

需要审核通过。应用图标必须是100px*100px大小的,否则不予通过。

image-20230813164627205

功能实现

创建应用模块

创建一个新的应用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': access_token}
    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