Django项目实践(商城):九、QQ登录

在这里插入图片描述

(根据居然老师直播课内容整理)
  • QQ登录:即我们所说的第三方登录,是指用户可以不在本项目中输入密码,而直接通过第三方的验证,成功登录本项目。
  • 一般QQ登录成功就直接进入系统,本项目还需要绑定用户
  • 如果用户已注册,直接绑定用户
  • 如果没有注册,还需要分配用户信息

一、QQ登录开发文档

1、QQ互联开发者申请步骤

若想实现QQ登录,需要成为QQ互联的开发者,审核通过才可实现。
相关连接:http://wiki.connect.qq.com/%E6%88%90%E4%B8%BA%E5%BC%80%E5%8F%91%E8%80%85

2、QQ互联应用申请步骤

成为QQ互联开发者后,还需创建应用,即获取本项目对应与QQ互联的应用ID。
相关连接:http://wiki.connect.qq.com/__trashed-2

3、网站对接QQ登录步骤

QQ互联提供有开发文档,帮助开发者实现QQ登录。
相关连接:http://wiki.connect.qq.com/%E5%87%86%E5%A4%87%E5%B7%A5%E4%BD%9C_oauth2-0

  • QQ登录开发流程:(以下内容截取了2021.2.12QQ说明文档)

3.1 准备工作_OAuth2.0

  • 本步骤的作用:
    接入QQ登录前,网站需首先进行申请,获得对应的appid与appkey,以保证后续流程中可正确对网站与用户进行验证与授权。
  • 本步骤在整个流程中的位置:
    在这里插入图片描述

3.1.1 申请appid和appkey

  • 申请appid和appkey的用途

    • appid:应用的唯一标识。在OAuth2.0认证过程中,appid的值即为oauth_consumer_key的值。
    • appkey:appid对应的密钥,访问用户资源时用来验证应用的合法性。在OAuth2.0认证过程中,appkey的值即为oauth_consumer_secret的值。
  • 申请地址
    https://connect.qq.com/manage.html#/

  • 申请流程

    • 1.开发者资质审核
      • 参考文章:开发者注册流程
    • 2.申请appid(oauth_consumer_key/client_id)和appkey(auth_consumer_secret/client_secret);
      • (1)进入https://connect.qq.com/manage.html#/页面,点击“创建应用”,在弹出的对话框中填写网站或应用的详细资料(名称,域名,回调地址);
      • (2)点击“确定”按钮,提交资料后,获取appid和appkey。
      • 注意:申请appid时,登录的QQ号码将与申请到的appid绑定,后续维护均需要使用该号码。
      • 注意:对appid和appkey信息进行保密,不要随意泄漏。

3.1.2 保证连接畅通

  • 接入QQ登录时,网站需要不停的和Qzone进行交互,发送请求和接受响应。
    • 1.对于PC网站:
      • 请在你的服务器上ping graph.qq.com ,保证连接畅通。
    • 2.移动应用无需此步骤

3.2 放置“QQ登录”按钮_OAuth2.0

  • 本步骤的作用:
    在网站页面上放置“QQ登录”按钮,并为按钮添加前台代码,实现点击按钮即弹出QQ登录对话框。
  • 本步骤在整个流程中的位置:
    在这里插入图片描述

3.2.1 下载“QQ登录”按钮图片,并将按钮放置在页面合适的位置

  • 按钮图片下载: 点击这里下载
  • 按照UI规范,将按钮放置在页面合适的位置:点击这里查看

3.2.2 为“QQ登录”按钮添加前台代码

  • 1.效果演示
    • 用户在页面上点击“QQ登录”按钮,将触发QQ登录对话框,效果如下图所示:
      在这里插入图片描述
  • 2.前台代码
    • 为了实现上述效果,应该为“QQ登录”按钮图片添加如下前台代码:
<img src=QQ登录图标文件在服务器上的地址 onclick=按钮点击事件>
  • 3.代码示例
    • 写一个函数“toLogin()”,该函数通过调用“index.php”中的qq_login函数来实现将页面跳转到QQ登录页面。
      (示例中的oauth/index.php,请参见从SDK下载页面下载PHP SDK,在Connect2.1文件夹下的index.php文件。)
<script>
 function toLogin()
 {
   //以下为按钮点击事件的逻辑。注意这里要重新打开窗口
   //否则后面跳转到QQ登录,授权页面时会直接缩小当前浏览器的窗口,而不是打开新窗口
   var A=window.open("oauth/index.php","TencentLogin",
   "width=450,height=320,menubar=0,scrollbars=1,
   resizable=1,status=1,titlebar=0,toolbar=0,location=1");
 }
</script>
  • 为按钮添加“toLogin()”事件:
 <a href="#" onclick='toLogin()'>
 <img src="img/qq_login.png"></a>

3.3 使用Authorization_Code获取Access_Token

  • 本步骤的作用:
    通过用户验证登录和授权,获取Access Token,为下一步获取用户的OpenID做准备;
    同时,Access Token是应用在调用OpenAPI访问和修改用户数据时必须传入的参数。
    移动端应用可以直接获得AccessToken,请参考使用Implicit_Grant方式获取Access_Token
  • 本步骤在整个流程中的位置:
    在这里插入图片描述
  • 对于应用而言,需要进行两步:
    • 1.获取Authorization Code;
    • 2.通过Authorization Code获取Access Token
  • 过程详解

Step1:获取Authorization Code(提供扫码页面)

  • 请求地址:PC网站:https://graph.qq.com/oauth2.0/authorize
  • 请求方法:GET
  • 请求参数:请求参数请包含如下内容:
参数 是否必须 含义
response_type 必须 授权类型,此值固定为“code”。
client_id 必须 申请QQ登录成功后,分配给应用的appid。
redirect_uri 必须 成功授权后的回调地址,必须是注册appid时填写的主域名下的地址,建议设置为网站首页或网站的用户中心。注意需要将url进行URLEncode。
state 必须 client端的状态值。用于第三方应用防止CSRF攻击,成功授权后回调时会原样带回。请务必严格按照流程检查用户与state参数状态的绑定。

返回说明:

  1. 如果用户成功登录并授权,则会跳转到指定的回调地址,并在redirect_uri地址后带上Authorization Code和原始的state值。如:
    PC网站:http://graph.qq.com/demo/index.jsp?code=9A5F************************06AF&state=test
    注意:此code会在10分钟内过期。
  2. 如果用户在登录授权过程中取消登录流程,对于PC网站,登录页面直接关闭;
    错误码说明:
    接口调用有错误时,会返回code和msg字段,以url参数对的形式返回,value部分会进行url编码(UTF-8)。
    PC网站接入时,错误码详细信息请参见:100000-100031:PC网站接入时的公共返回码。

Step2:通过Authorization Code获取Access Token

  • 请求地址:PC网站:https://graph.qq.com/oauth2.0/token

  • 请求方法:GET

  • 请求参数:请求参数请包含如下内容:
    在这里插入图片描述

  • 返回说明:

    • 如果成功返回,即可在返回包中获取到Access Token。 如(不指定fmt时):

    • access_token=FE04************************CCE2&expires_in=7776000&refresh_token=88E4************************BE14
      在这里插入图片描述

  • 错误码说明:
    接口调用有错误时,会返回code和msg字段,以url参数对的形式返回,value部分会进行url编码(UTF-8)。
    PC网站接入时,错误码详细信息请参见:100000-100031:PC网站接入时的公共返回码。

Step3:(可选)权限自动续期,获取Access Token

  • Access_Token的有效期默认是3个月,过期后需要用户重新授权才能获得新的Access_Token。本步骤可以实现授权自动续期,避免要求用户再次授权的操作,提升用户体验。
  • 请求地址:PC网站:https://graph.qq.com/oauth2.0/token
  • 请求方法:GET
  • 请求参数:请求参数请包含如下内容:
    在这里插入图片描述
  • 返回说明:
    如果成功返回,即可在返回包中获取到Access Token。 如(不指定fmt时):
    access_token=FE04************************CCE2&expires_in=7776000&refresh_token=88E4************************BE14。
    在这里插入图片描述
  • 错误码说明:
    接口调用有错误时,会返回code和msg字段,以url参数对的形式返回,value部分会进行url编码(UTF-8)。
    PC网站接入时,错误码详细信息请参见:100000-100031:PC网站接入时的公共返回码。

3.4 获取用户OpenID_OAuth2.0

  • 本步骤的作用:
    通过输入在上一步获取的Access Token,得到对应用户身份的OpenID。
    OpenID是此网站上或应用中唯一对应用户身份的标识,网站或应用可将此ID进行存储,便于用户下次登录时辨识其身份,或将其与用户在网站上或应用中的原有账号进行绑定。
  • 本步骤在整个流程中的位置:
    在这里插入图片描述
  • 1 请求地址:PC网站:https://graph.qq.com/oauth2.0/me
  • 2 请求方法:GET
  • 3 请求参数:请求参数请包含如下内容:
参数 是否必须 含义
access_token 必须 在Step1中获取到的access token。
  • 4 返回说明
    PC网站接入时,获取到用户OpenID,返回包如下(如果fmt参数未指定):
callback( {"client_id":"YOUR_APPID","openid":"YOUR_OPENID"} );

openid是此网站上唯一对应用户身份的标识,网站可将此ID进行存储便于用户下次登录时辨识其身份,或将其与用户在网站上的原有账号进行绑定。

  • 5 错误码说明
    接口调用有错误时,会返回code和msg字段,以url参数对的形式返回,value部分会进行url编码(UTF-8)。
    PC网站接入时,错误码详细信息请参见:100000-100031:PC网站接入时的公共返回码。

4、QQ登录流程分析

在这里插入图片描述

二、QQ登录工具QQLoginTool

  • 上面的登录流程可以使用QQLoginTool实现
  • 该工具封装了QQ登录时对接QQ互联接口的请求操作。可用于快速实现QQ登录。
  • QQLoginTool安装:pip install QQLoginTool
  • 使用说明
    • 1.导入
from QQLoginTool.QQtool import OAuthQQ
  • 2.初始化OAuthQQ对象
oauth = OAuthQQ(client_id=settings.QQ_CLIENT_ID, client_secret=settings.QQ_CLIENT_SECRET, redirect_uri=settings.QQ_REDIRECT_URI, state=next)
  • 3.获取QQ登录扫码页面,扫码后得到Authorization Code
login_url = oauth.get_qq_url()
  • 4.通过Authorization Code获取Access Token
access_token = oauth.get_access_token(code)
  • 5.通过Access Token获取OpenID
openid = oauth.get_open_id(access_token)

三、定义QQ登录模型类

  • QQ登录成功后,我们需要将QQ用户和商场用户关联到一起,方便下次QQ登录时使用,所以我们选择使用MySQL数据库进行存储。
  • 保存字段主要有:创建时间、更新时间 、QQ的openid和用户id
    在这里插入图片描述
  • 此功能可以不创建新的app,直接在uses的modles.py里创建,也可以创建新的app,本次项目就创建新的app,方便以后写微信、支付宝等其它第三方登录
    在这里插入图片描述
python ../manage.py startapp oauth

1、创建数据模型基类

  • 创建时间、更新时间这两个字段在很多表中都需要,固需要创建成基类,其它数据模型在其基础上继承
  • 此两字段不需要单独创建表,故定义为抽象模型类 abstract = True,数据库迁移时不创建新表
from django.db import models

class BaseModel(models.Model):
    create_time=models.DateTimeField(auto_now_add=True,verbose_name="创建时间")
    update_time=models.DateTimeField(auto_now=True,verbose_name="更新时间")

    class Meta:
        # 数据库迁移的时候不创建BaseModel这个表
        abstract = True

2、定义QQ登录模型类

  • 继承BaseModel类,用户id是外健,users.User类
from django.db import models
from utils.models import BaseModel


class OAuthQQUser(BaseModel):
    """QQ登录用户数据"""
    user = models.ForeignKey('users.User', on_delete=models.CASCADE, verbose_name='用户')
    openid = models.CharField(max_length=64, verbose_name='openid')

    class Meta:
        db_table = 'tb_oauth_qq'
        verbose_name = 'QQ登录用户数据'
        verbose_name_plural = verbose_name

3、注册应用

在这里插入图片描述

4、数据库迁移

在这里插入图片描述

python manage.py makemigrations
python manage.py migrate

四、OAuth2.0认证获取openid

  • 首先前端发起QQ登录请求,需要后端返回一个QQ登录界面的链接
  • 前端接收到QQ登录界面的链接后,打开并展示QQ登录界面(由QQ服务器返回QQ登录界面及相关参数)
  • 用户扫描QQ登录链,完成QQ录登验证(QQ服务器处理)
  • 如果验证成功,QQ服务器会将处理结果回调信息传给后端,后端根据接口参数完成系统登录处理

1、获取QQ登录扫码页面

1.1 接口设计

  • 用户点击QQ登录图标,前端即会发起QQ登录的ajax请求
  • 后端接收到请求,接收参数,
  • 利用QQLoginTools工具得到QQ登录的url
  • 返回用JSON方式,发送给前端

1.1.1 请求方式

选项 方案
请求方法 GET
请求地址 /qq/login/

1.1.2 请求参数:查询参数

参数名 类型 是否必传 说明
next string 用于记录QQ登录成功后进入的网址

1.1.3 响应结果:JSON

字段 说明
code 状态码
errmsg 错误信息
login_url QQ登录扫码页面链接

1.2 QQ登录参数

  • QQ互联开发者申请后,会有以下3个参数:QQ_CLIENT_ID、QQ_CLIENT_SECRET、QQ_REDIRECT_URI
  • 一般公司会有专人负责申请,直接向公司领用此3个参数即可
  • 建议将此参数定义在配置文件中

1.3 后端逻辑实现

# ./apps/oauth/views.py
from django.shortcuts import render
from django.views import View
from QQLoginTool.QQtool import OAuthQQ
from django.conf import settings
from django import http
from utils.response_code import RETCODE

# Create your views here.

class QQAuthURLView(View):

    def get(self,request):
        # 获取参数
        next=request.GET.get("next")
        print(next)

        # 创建工具对象
        oauth = OAuthQQ(client_id=settings.QQ_CLIENT_ID, client_secret=settings.QQ_CLIENT_SECRET,
                        redirect_uri=settings.QQ_REDIRECT_URI, state=next)

        # 生成扫描链接地址
        login_url = oauth.get_qq_url()

        return http.JsonResponse({'code': RETCODE.OK, 'errmsg': 'OK', 'login_url': login_url})

1.3.2 注册路由

#  注册子路由
# ./apps/oauth/urls.py
from django.urls import path
from . import views

urlpatterns = [
    # 提供QQ登录扫描页面
    path('qq/login/', views.QQAuthURLView.as_view()),

]

# 注册主路由
# ./lgshop/urls.py 
from django.contrib import admin
from django.urls import path,include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('users/', include('users.urls')),
    path('', include('contents.urls')),
    path('',include("verifications.urls")),
    path("",include("oauth.urls"))
]

1.4 前端

在这里插入图片描述

  • ./templates/login.html
    在这里插入图片描述
  • ./static/login.js
    在这里插入图片描述

2、用户用手机QQ扫描QQ登录二维码后,并确认登录后,出现下面错误界面

在这里插入图片描述

  • 返回址址:http://www.meiduo.site:8000/oauth_callback?code=62168281346F9B7CBF5964694C5B26F2&state=%2F

  • 根据错误提示,将www.meiduo.site 添加到允许访问的地址列表中,同时还要将127.0.0.1也添加到允许访问的地址列表中
    在这里插入图片描述

  • 在开发环境下,服务都跑在127.0.0.1上,所以要将本机绑定www.meiduo.site域名

    • Windows系统:编辑 C:\Windows\System32\drivers\etc\hosts
      在这里插入图片描述
    • 在linux ubuntu系统或者Mac系统:编辑 /etc/hosts

3、接收Authorization Codece,获取openid

  • 用户在QQ登录成功后,QQ会将用户重定向到我们配置的回调网址。
  • 在QQ重定向到回调网址时,会传给我们一个Authorization Code。
  • 我们需要拿到Authorization Code并完成OAuth2.0认证获取openid。
  • 在本项目中,我们申请QQ登录开发资质时配置的回调网址为:
    • http://www.meiduo.site:8000/oauth_callback
    • QQ互联重定向的完整网址为:http://www.meiduo.site:8000/oauth_callback/?code=AE263F12675FA79185B54870D79730A7&state=%2F
  • 使用access_token向QQ服务器请求openid

2.1 view实现

  • 通过QQ回调url,拿到code
  • 通过code ,向QQ服务器申请access_token
  • 通过QQ服务器返回的access_token,再向QQ服务器申请openid
  • 当获得QQ的openid后,就完成QQ登录的验证部分
  • 根据项目需要,还需要将openid与用户名绑定(相关讲解详见后面的4、openid绑定用户的处理)

2.2 路由设置

在这里插入图片描述

4、openid是否绑定用户的处理

  • 得到openid后,需要检查openid是否绑定用户,即查询表tb_oauth_qq中是否有openid
    • 得到oauth_user对象,但user对象是它的外键
  • 如果存在,即openid与用户进行了绑定,使用用户操作登录需要的步骤
    • 保持登录状态:利用django的login()
    • 需要获得跳转路径: next关建字改为了state
    • 将用户设置到cookie中
    • 返回前端
  • 如果不存在,即openid未与用户进行了绑定过,需要显示绑定页面
    • 绑定界面:用户名、手机号、密码、图片验证、短信验证、隐藏的openid
      • 隐藏的openid需要后端做为参数传递到前端
    • openid不能用明文传输,需要加密,提交后还需要解密还原
      • 可以使用itsdangerous对openid进行加密及解密

4.1 使用itsdangerous加密和解密

  • 安装itsdangerous : pip install itsdangerous
  • 将加密解密的函数,定义到独立的文件utils.py中
  • 加解密需要用到数据证书,建议用项目配置文件中SECRET_KEY,也可以自定义一个
  • 使用TimedJSONWebSignatureSerializer可以生成带有有效期的token

4.1.1 itsdangerous的使用

  • itsdangerous模块的参考资料链接 http://itsdangerous.readthedocs.io/en/latest/
  • 安装:pip install itsdangerous
  • TimedJSONWebSignatureSerializer的使用
    • 使用TimedJSONWebSignatureSerializer可以生成带有有效期的token
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
from django.conf import settings
# serializer = Serializer(秘钥, 有效期秒)
serializer = Serializer(settings.SECRET_KEY, 300)
# serializer.dumps(数据), 返回bytes类型
token = serializer.dumps({'mobile': '185xxxxxxx78'})
token = token.decode()
# 检验token
# 验证失败,会抛出itsdangerous.BadData异常
serializer = Serializer(settings.SECRET_KEY, 300)
try:
    data = serializer.loads(token)
except BadData:
    return None

4.1.2 itsdangerous加密和解密(utils.py)

# .oauth.utils.py 
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
from django.conf import settings
from . import constants


def check_access_token(openid):
    """
    反序列化
    :param openid: openid密文
    :return: openid: 明文
    """
    serializer = Serializer(settings.SECRET_KEY, constants.ACCESS_TOKEN_EXPIRES)
    # constants.ACCESS_TOKEN_EXPIRES=600

    try:
        data = serializer.loads(openid)
    except Exception as e:
        return None
    else:
        return data.get('openid')


def generate_access_token(openid):
    """
    签名、序列化openid
    :param openid: 明文
    :return: openid密文
    """

    serializer = Serializer(settings.SECRET_KEY, constants.ACCESS_TOKEN_EXPIRES)

    data = {'openid':  openid}

    # 类型 是字节
    token = serializer.dumps(data)

    return token.decode()

5、views.py代码实现

class QQAuthUserView(View):
    """处理QQ登录回调"""

    def get(self, request):
        """处理QQ登录回调的业务逻辑"""
        code = request.GET.get('code')
        if not code:
            return http.HttpResponseForbidden('获取code失败')

        # 创建工具对象
        oauth = OAuthQQ(client_id=settings.QQ_CLIENT_ID, client_secret=settings.QQ_CLIENT_SECRET,
                            redirect_uri=settings.QQ_REDIRECT_URI)

        try:
            # 使用code获取access_token
            access_token = oauth.get_access_token(code)
            # 使用access_token获取openid
            openid = oauth.get_open_id(access_token)
        except Exception as e:
            logger.error(e)
            return http.HttpResponseServerError('OAuth2.0认证失败')

        # openid QQ用户的ID
        # 使用openid判断该QQ用户是否绑定商城的用户
        try:
            oauth_user = OAuthQQUser.objects.get(openid=openid)
        except OAuthQQUser.DoesNotExist:
            # 如果没有找到记录 openid 未绑定商城用户  展示绑定页面
            # openid 是明文  Sdasdasdasdasy7iyw432 => 明文  可逆 签名的算法
            context = {'access_token_openid': generate_access_token(openid)}
            return render(request, 'oauth_callback.html', context=context)
        else:
            # 找到记录  登录
            # 状态保持
            login(request, oauth_user.user)
            next = request.GET.get('state')
            response = redirect(next)
            response.set_cookie('username', oauth_user.user.username, max_age=3600 * 24)
            # 响应结果 重定向到首页
            return response

6、用户绑定实现

  • 用户绑定实现类似于用户注册的业务逻辑
  • 当用户输入的手机号对应的用户已存在
    • 直接将该已存在用户跟openid绑定
  • 当用户输入的手机号对应的用户不存在
    • 新建一个用户,并跟openid绑定

6.1 请求方式

选项 方案
请求方法 POST
请求地址 /oauth_callback/

6.2 请求参数:表单参数

参数名 类型 是否必传 说明
username string 用户名
password string 密码
mobile string 手机号
sms_code string 短信验证码

6.3 响应结果

响应结果 响应内容
注册失败 响应错误提示
注册成功 重定向到首页

6.4 views实现

  • 接收参数
    • 手机号、密码、短信验证码、密文的openid
  • 校验参数
    • 短信验证码是否过期,如果过期,返回错误信息
    • 短信验证码是否正确,如果错误,返回错误信息
    • 解密openid,如果为空(过期或错误),返回错误信息
    • 检查手机号是否存在(即是否注册过)
    • 如果手机没有注册过,以此手机号为用户名创建新用户(用户名=手机,密码,手机号)
    • 如果手机号注册过,判断密码是否正确,如果错误,返回错误
  • 绑定用户
    • 将用记信息与openid保存到数据库中
  • 保持状态
  • 保存cookie
  • 重定向
    def post(self,request):
        """实现绑定用户的业务逻辑"""
        # 接收参数
        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')  # openid密文

        # 校验参数
        # 判断短信验证码是否一致
        redis_conn = get_redis_connection('verify_code')
        sms_code_server = redis_conn.get('sms_%s' % mobile)

        if sms_code_server is None:
            return render(request, 'oauth_callback.html', {
    
    'sms_code_errmsg': '无效的短信验证码'})

        if sms_code_client != sms_code_server.decode():
            return render(request, 'oauth_callback.html', {
    
    'sms_code_errmsg': '输入短信验证码有误'})

        # 判断openid是否有效
        openid = check_access_token(access_token)
        if not openid:  # openid 过期 或 不对
            return render(request, 'oauth_callback.html', {
    
    'openid_errmsg': 'openid已经失效'})

        # 使用手机号查询对应的用户是否存在
        try:
            user = User.objects.get(mobile=mobile)
        except User.DoesNotExist:
            # 如果不存在, 创建一个新的用户
            user = User.objects.create_user(username=mobile, password=password, mobile=mobile)
        else:
            # 如果存在,校验密码
            if not user.check_password(password):
                return render(request, 'oauth_callback.html', {
    
    'account_errmsg': '账号或者密码错误'})

        # 绑定用户
        # oauth_qq_user = OAuthQQUser(user=user, openid=openid)
        # oauth_qq_user.save()
        try:
            oauth_qq_user = OAuthQQUser.objects.create(user=user, openid=openid)
        except Exception as e:
            return render(request, 'oauth_callback.html', {
    
    'account_errmsg': '账号或者密码错误'})

        # 重定向到首页
        # 状态保持
        login(request, oauth_qq_user.user)

        next = request.GET.get('state')
        # print(next)
        response = redirect(next)

        response.set_cookie('username', oauth_qq_user.user.username, max_age=3600 * 24)
        # 响应结果 重定向到首页
        return response

猜你喜欢

转载自blog.csdn.net/laoluobo76/article/details/113796460
今日推荐