python学习之美多商城(八):用户模块--第三方登录(QQ登录、微博登录)、创建数据模型类基类

一、QQ登录:

QQ登录,亦即我们所说的第三方登录,是指用户可以不在本项目中输入密码,而直接通过第三方的验证,成功登录本项目。

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

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

QQ登录开发文档连接http://wiki.connect.qq.com/%E5%87%86%E5%A4%87%E5%B7%A5%E4%BD%9C_oauth2-0

1.前期准备:

QQ开放平台:https://open.tencent.com/

  • 1.注册成为""QQ开放平台"的的开发者;
  • 2.创建网站应用,提交审核;
  • 3.审核成功,拿到app_id和app_key
  • 4.在前端页面中放置QQ图标;
  • 5.获取Access_token;
  • 6.获取open_id值(open_id是QQ用户身份的唯一标识)

2.创建模型类:

创建一个新的应用oauth,用来实现QQ第三方认证登录。总路由前缀 oauth/
在meiduo/meiduo_mall/utils/models.py文件中创建模型类基类,用于增加数据新建时间和更新时间。

# /meiduo_mall/utils/models.py
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:
        abstract = True  # 说明是抽象模型类, 用于继承使用,数据库迁移时不会创建BaseModel的表

将第三方账号登录功能单独放到一个子应用(oauth)中,

ubuntu:~/Desktop/meiduo/meiduo_mall/meiduo_mall/apps$ python ../../manage.py startapp oauth

在oauth/models.py中定义QQ身份(openid)和微博身份(weibotoken), 与用户模型类User的关联关系。

from django.db import models
from meiduo_mall.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', db_index=True)	# QQ用户标识openid
    weibotoken = models.CharField(max_length=64, verbose_name='weibotoken', db_index=True)	# 微博用户登录Access_token
    
    class Meta:
        db_table = 'tb_oauth'
        verbose_name = '第三方登录登录用户数据'
        verbose_name_plural = verbose_name

进行数据库迁移

python manage.py makemigrations
python manage.py migrate

3.QQ登录SDK使用:

  • QQ官方没有给出响应的python3版本的SDK, 所以, 根据QQ开放平台的官方文档手写一个QQ登录SDK。
  • 除了手写,还可以使用OOLoginTool第三方模块
pip install QQLoginTool

4.自定义QQ登录的SDK:

# oauth/utils.py
import json
from urllib.parse import urlencode, parse_qs

import requests
from django.conf import settings


class OAuthQQ(object):
    """ QQ认证辅助工具类 """
    def __init__(self, client_id = None, client_secret = None, redirect_uri = None, state = None):
        """
        初始化QQ登录对象
        :param client_id:
        :param client_secret:
        :param redirect_uri:
        :param state:
        """
        self.client_id = client_id or settings.QQ_CLIENT_ID
        self.client_secret = client_secret or settings.QQ_CLIENT_SECRET
        self.redirect_uri = redirect_uri or settings.QQ_REDIRECT_URI
        self.state = state or settings.QQ_STATE  # 用于保存登录成功后的跳转页面路径


    def get_qq_url(self):
        """
        获取QQ登录链接
        :return:
        """
        data_dict = {
            'response_type': 'code',
            'client_id': self.client_id,
            'redirect_uri': self.redirect_uri,
            'state': self.state
        }
        # 构造QQ登录链接==>查询字符串
        qq_url = 'https://graph.qq.com/oauth2.0/authorize?'+ urlencode(data_dict)
        return qq_url

    def get_access_token(self, code):
        """
        获取access_token值
        :param code:
        :return:
        """
        # 构建参数数据
        data_dict = {
            'grant_type': 'authorization_code',
            'client_id': self.client_id,
            'client_secret': self.client_secret,
            'redirect_uri': self.redirect_uri,
            'code': code
        }

        # 构建url
        access_url = 'https://graph.qq.com/oauth2.0/token?' + urlencode(data_dict)

        # 发送请求
        try:
            response = requests.get(access_url)

            # 提取数据
            # access_token=FE04************************CCE2&expires_in=7776000&refresh_token=88E4************************BE14
            data = response.text

            # 转化为字典
            data = parse_qs(data)
        except:
            raise Exception('qq请求失败')

        # 提取access_token
        access_token = data.get('access_token', None)

        if not access_token:
            raise Exception('access_token获取失败')

        return access_token[0]

    def get_open_id(self, access_token):
        """
        获取openid
        :param access_token:
        :return:
        """
        # 构建请求url
        url = "https://graph.qq.com/oauth2.0/me?access_token=" + access_token

        # 发送请求
        try:
            response = requests.get(url)

            # 提取数据
            # callback( {"client_id":"YOUR_APPID","openid":"YOUR_OPENID"} );
            # code=asdasd&msg=asjdhui  错误的时候返回的结果
            data = response.text
            data = data[10:-3]
        except:
            raise Exception('qq请求失败')
            # 转化为字典
        try:
            data_dict = json.loads(data)
            # 获取openid
            print("data:%s" % data_dict)
            openid = data_dict.get('openid')
        except:
            raise Exception('openid获取失败')

        return openid

5. 返回QQ登录网址的视图:

5.1 后端接口设计:

请求方式: GET /oauth/qq/authorization/
请求参数: 查询字符串

参数名类型是否必须说明
nextstr用户QQ登录成功后进入美多商城的哪个网址

返回数据: JSON

返回值类型是否必传说明
login_urlstrqq登录网址
{
    "login_url": "https://graph.qq.com/oauth2.0/show?which=Login&display=pc&response_type=code&client_id=101474184&redirect_uri=http%3A%2F%2Fwww.meiduo.site%3A8080%2Foauth_callback.html&state=%2F&scope=get_user_info"
}

5.1.2准备配置信息:

在配置文件中添加关于QQ登录的应用开发信息

settings/dev.py
# QQ登录参数
QQ_CLIENT_ID = '10*****84'
QQ_CLIENT_SECRET = 'c6*******************37c'
QQ_REDIRECT_URI = 'http://www.meiduo.site:8080/oauth_callback.html'

5.1.3 后端的逻辑实现:

class QQAuthURLLView(APIView):
    """定义QQ第三方登录的视图类"""
    def get(self, request):
        """
        获取QQ登录的链接
        :param request:
        :return:
        """
        # 1.通过查询字符串
        next = request.query_params.get('state')
        if not next:
            next = "/"

        # 获取QQ登录网页
        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 Response({"login_url": login_url})

5.1.4 前端实现:

修改login.js,在methods中增加qq_login方法

// qq登录
qq_login: function(){
     var next = this.get_query_string('next') || '/';
     axios.get(this.host + '/oauth/qq/authorization/?next=' + next, {
             responseType: 'json'
         })
         .then(response => {
             location.href = response.data.login_url;
         })
         .catch(error => {
             console.log(error.response.data);
         })
 }

6.OAuth2.0认证:

  1. 准备oauth_callback回调页,用于扫码后接受Authorization Code
  2. 通过Authorization Code获取Access Token
  3. 通过Access Token获取OpenID

6.1 oauth_callback回调页

用户在QQ登录成功后,QQ会将用户重定向回我们配置的回调callback网址,在本项目中,我们申请QQ登录开发资质时配置的回调地址为:

http://www.meiduo.site:8080/oauth_callback.html

我们在front_end_pc目录中新建oauth_callback.html文件,用于接收QQ登录成功的用户回调请求。在该页面中,提供了用于用户首次使用QQ登录时需要绑定用户身份的表单信息。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
    <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
    <title>美多商城-绑定用户</title>
    <link rel="stylesheet" type="text/css" href="css/reset.css">
    <link rel="stylesheet" type="text/css" href="css/main.css">
    <script type="text/javascript" src="js/hosts.js"></script>
    <script type="text/javascript" src="js/vue-2.5.16.js"></script>
    <script type="text/javascript" src="js/axios-0.18.0.min.js"></script>
</head>
<body>
    <div id="app">
        <div v-if="is_show_waiting" class="pass_change_finish">请稍后...</div>
        <div v-else>
            <div class="register_con">
                <div class="l_con fl">
                    <a class="reg_logo"><img src="images/logo.png"></a>
                    <div class="reg_slogan">商品美 · 种类多 · 欢迎光临</div>
                    <div class="reg_banner"></div>
                </div>

                <div class="r_con fr">
                    <div class="reg_title clearfix">
                        <h1>绑定用户</h1>
                    </div>
                    <div class="reg_form clearfix" v-cloak>
                        <form id="reg_form" v-on:submit.prevent="on_submit">
                        <ul>
                            <li>
                                <label>手机号:</label>
                                <input type="text" v-model="mobile" v-on:blur="check_phone" name="phone" id="phone">
                                <span v-show="error_phone" class="error_tip">{{ error_phone_message }}</span>
                            </li>
                            <li>
                                <label>密码:</label>
                                <input type="password" v-model="password" v-on:blur="check_pwd" name="pwd" id="pwd">
                                <span v-show="error_password" class="error_tip">密码最少8位,最长20位</span>
                            </li>
                            <li>
                                <label>短信验证码:</label>
                                <input type="text" v-model="sms_code" v-on:blur="check_sms_code" name="msg_code" id="msg_code" class="msg_input">
                                <a v-on:click="send_sms_code" class="get_msg_code">{{ sms_code_tip }}</a>
                                <span v-show="error_sms_code" class="error_tip">{{ error_sms_code_message }}</span>
                            </li>
                            <li class="reg_sub">
                                <input type="submit" value="保 存" name="">
                            </li>
                        </ul>
                        </form>
                    </div>
                </div>
            </div>

            <div class="footer no-mp">
                <div class="foot_link">
                    <a href="#">关于我们</a>
                    <span>|</span>
                    <a href="#">联系我们</a>
                    <span>|</span>
                    <a href="#">招聘人才</a>
                    <span>|</span>
                    <a href="#">友情链接</a>
                </div>
                <p>CopyRight © 2016 北京美多商业股份有限公司 All Rights Reserved</p>
                <p>电话:010-****888    京ICP备*******8号</p>
            </div>
        </div>
    </div>
    <script type="text/javascript" src="js/oauth_callback.js"></script>
</body>
</html>

在js目录中新建oauth_callback.js文件

var vm = new Vue({
    el: '#app',
    data: {
        host: host,
        is_show_waiting: true,

        error_password: false,
        error_phone: false,
        error_sms_code: false,
        error_phone_message: '',
        error_sms_code_message: '',

        sms_code_tip: '获取短信验证码',
        sending_flag: false, // 正在发送短信标志

        password: '',
        mobile: '', 
        sms_code: '',
        access_token: ''
    },
    mounted: function(){
        // 从路径中获取qq重定向返回的code
        var code = this.get_query_string('code');
        axios.get(this.host + '/oauth/qq/user/?code=' + code, {
                responseType: 'json',
                withCredentials: true
            })
            .then(response => {
                if (response.data.user_id){
                    // 用户已绑定
                    sessionStorage.clear();
                    localStorage.clear();
                    localStorage.user_id = response.data.user_id;
                    localStorage.username = response.data.username;
                    localStorage.token = response.data.token;

                    // 登录成功后,根据state将用户引导到登录成功后的页面
                    var state = this.get_query_string('state');
                    location.href = state;
                } else {
                    // 用户未绑定
                    this.access_token = response.data.access_token;
                    this.is_show_waiting = false;
                }
            })
            .catch(error => {
                console.log(error.response.data);
                alert('服务器异常');
            })
    },
    methods: {
        // 获取url路径参数    
        get_query_string: function(name){ 
            var reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)', 'i');
            var r = window.location.search.substr(1).match(reg);
            if (r != null) {
                return decodeURI(r[2]);
            }
            return null;
        },
        check_pwd: function (){
            var len = this.password.length;
            if(len<8||len>20){
                this.error_password = true;
            } else {
                this.error_password = false;
            }        
        },
        check_phone: function (){
            var re = /^1[345789]\d{9}$/;
            if(re.test(this.mobile)) {
                this.error_phone = false;
            } else {
                this.error_phone_message = '您输入的手机号格式不正确';
                this.error_phone = true;
            }
        },
        check_sms_code: function(){
            if(!this.sms_code){
                this.error_sms_code_message = '请填写短信验证码';
                this.error_sms_code = true;
            } else {
                this.error_sms_code = false;
            }
        },
        // 发送手机短信验证码
        send_sms_code: function(){

            // 重新发送短信后,隐藏提示信息
            this.error_sms_code = false;

            if (this.sending_flag == true) {
                return;
            } 
            this.sending_flag = true;

            // 校验参数,保证输入框有数据填写
            this.check_phone();

            if (this.error_phone == true) {
                this.sending_flag = false;
                return;
            }

            // 向后端接口发送请求,让后端发送短信验证码
            axios.get(this.host + '/sms_codes/' + this.mobile + '/', {
                    responseType: 'json'
                })
                .then(response => {
                    // 表示后端发送短信成功
                    // 倒计时60秒,60秒后允许用户再次点击发送短信验证码的按钮
                    var num = 60;
                    // 设置一个计时器
                    var t = setInterval(() => {
                        if (num == 1) {
                            // 如果计时器到最后, 清除计时器对象
                            clearInterval(t);
                            // 将点击获取验证码的按钮展示的文本回复成原始文本
                            this.sms_code_tip = '获取短信验证码';
                            // 将点击按钮的onclick事件函数恢复回去
                            this.sending_flag = false;
                        } else {
                            num -= 1;
                            // 展示倒计时信息
                            this.sms_code_tip = num + '秒';
                        }
                    }, 1000, 60)
                })
                .catch(error => {
                    if (error.response.status == 400) {
                        // 展示发送短信错误提示
                        this.error_sms_code = true;
                        this.error_sms_code_message = error.response.data.message;
                    } else {
                        console.log(error.response.data);
                    }
                    this.sending_flag = false;
                })
        },
        // 保存
        on_submit: function(){
            this.check_pwd();
            this.check_phone();
            this.check_sms_code();

            if(this.error_password == false && this.error_phone == false && this.error_sms_code == false) {
                axios.post(this.host + '/oauth/qq/user/', {
                        password: this.password,
                        mobile: this.mobile,
                        sms_code: this.sms_code,
                        access_token: this.access_token
                    }, {
                        responseType: 'json',
                    })
                    .then(response => {
                        // 记录用户登录状态
                        sessionStorage.clear();
                        localStorage.clear();
                        localStorage.token = response.data.token;
                        localStorage.user_id = response.data.user_id;
                        localStorage.username = response.data.username;
                        location.href = this.get_query_string('state');
                    })
                    .catch(error=> {
                        if (error.response.status == 400) {
                            this.error_sms_code_message = error.response.data.message;
                            this.error_sms_code = true;
                        } else {
                            console.log(error.response.data);
                        }
                    })
            }
        }
    }
});

6.2 获取QQ用户的OpenID:

在QQ将用户重定向到此网页的时候,重定向的网址会携带QQ提供的code参数,用于获取用户信息使用,我们需要将这个code参数发送给后端,在后端中使用code参数向QQ请求用户的身份信息,并查询与该QQ用户绑定的用户。

6.2.1 后端接口:

** 请求方式:** GET oauth/qq/user/
** 请求参数:** 查询字符串

参数类型是否必传说明
codestrqq返回的授权凭证code

返回数据:

参数类型是否必传说明
access_tokenstr用户是第一次使用QQ登录时返回,其中包含openid,用于绑定身份使用,注意这个是我们自己生成的
tokenstr用户不是第一次使用QQ登录时返回,登录成功的JWT token
usernamestr用户不是第一次使用QQ登录时返回,用户名
user_idint用户不是第一次使用QQ登录时返回,用户id
{
    "access_token": xxxx,
}{
    "token": "xxx",
    "username": "python",
    "user_id": 1
}
6.2.2 后端逻辑实现:
# oauth/views.py
class QQAuthURLLView(APIView):
    """定义QQ第三方登录的视图类"""
    def get(self, request):
        """
        获取QQ登录的链接
        :param request:
        :return:
        """
        # 1.通过查询字符串
        next = request.query_params.get('state')
        if not next:
            next = "/"

        # 获取QQ登录网页
        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 Response({"login_url": login_url})

class QQOauthView(APIView):
    """验证QQ登录"""
    def get(self,request):
        """
        第三方登录检查
        :param request:
        :return:
        """
        # 1. 获取code值
        code = request.query_params.get("code")
        # 2.检查参数
        if not code:
            return Response({'errors': '缺少code值'}, status=400)

        # 3.通过code获取token值
        state = '/'
        # 创建qq对象
        qq = OAuthQQ(client_id=settings.QQ_CLIENT_ID,
                     client_secret=settings.QQ_CLIENT_SECRET,
                     redirect_uri=settings.QQ_REDIRECT_URI,
                     state=state)
        access_token = qq.get_access_token(code=code)
        # print("access:%s" % access_token)
        # 4. 获取openid值
        openid = qq.get_open_id(access_token=access_token)
        print("openid:%s" % openid)
        # 5.判断是否绑定过美多账号
        try:
            qq_user = OAuthUser.objects.get(openid=openid)
        except:
            # 6.未绑定,进入绑定页面,完成绑定
            tjs = TJS(settings.SECRET_KEY, 300)
            open_id = tjs.dumps({'openid': openid}).decode()

            return Response({'access_token': open_id})
        else:
            # 7.绑定过,则登录成功
            # 生成jwt-token值
            user = qq_user.user
            jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
            jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER

            payload = jwt_payload_handler(user)  # 生成载荷部分
            token = jwt_encode_handler(payload)  # 生成token

            response = Response(
                {
                    'token': token,
                    'username': user.username,
                    'user_id': user.id
                }
            )

            return response

7.补充:使用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': '18512345678'})
token = token.decode()

# 检验token
# 验证失败,会抛出itsdangerous.BadData异常
serializer = Serializer(settings.SECRET_KEY, 300)
try:
    data = serializer.loads(token)
except BadData:
    return None

8. OpenID绑定美多商城用户

如果用户是首次使用QQ登录,则需要绑定用户,页面如下:
在这里插入图片描述
业务逻辑:

用户需要填写手机号、密码、图片验证码、短信验证码
如果用户未在美多商城注册过,则会将手机号作为用户名为用户创建一个美多账户,并绑定用户
如果用户已在美多商城注册过,则检验密码后直接绑定用户

1.1 1. 绑定QQ身份的处理流程

在这里插入图片描述

1.2 后端接口设计

请求方式: POST /oauth/qq/user/

请求参数: JSON 或 表单

参数类型是否必须说明
mobilestr手机号
passwordstr密码
sms_codestr短信验证码
access_tokenstr凭据 (包含openid)

返回数据: JSON

返回值类型是否必须说明
tokenstrJWT token
idint用户id
usernamestr 是 用户名

1.3 后盾逻辑实现:

  • 在oauth/views.py修改QQAuthUserView视图:
#-*-coding:utf-8-*-

class QQOauthView(APIView):
    """验证QQ登录"""
    def get(self,request):
        ...
        
    def post(self, request):
        """
        QQ绑定页面的请求,完成绑定用户操作
        :param request:
        :return:
        """
        # 1. 获取前端数据
        data = request.data
        # 2.调用序列化起验证数据
        ser = QQOauthSerializers(data=data)
        ser.is_valid()
        print(ser.errors)
        # 保存绑定数据
        ser.save()
        return Response(ser.data)
  • 在oauth/serializers.py中定义序列化器:
# -*-coding:utf-8-*-
import re
from django.conf import settings
from rest_framework_jwt.serializers import serializers
from itsdangerous import TimedJSONWebSignatureSerializer as TJS
from rest_framework_jwt.settings import api_settings

from oauth.models import OAuthUser
from users.models import User
from verifications.views import SMSCodeView, ImageCheckViews

class QQOauthSerializers(serializers.ModelSerializer):
    """QQ用户绑定的序列化起"""
    # 指名模型类中没有的字段
    mobile = serializers.CharField(max_length=11)
    sms_code = serializers.CharField(max_length=6, min_length=6, write_only=True)
    access_token = serializers.CharField(write_only=True)   # 反序列化输入

    token = serializers.CharField(read_only=True)
    user_id = serializers.IntegerField(read_only=True)  # 序列化输出


    class Meta:
        model=User
        fields=('password', 'mobile', 'username', 'sms_code', 'token','access_token', 'user_id')

        extra_kwargs = {
            'username':{
                'read_only':True
            },
            'password':{
                'write_only':True
            }
        }

    def validated_mobile(self, value):
        """
        验证手机号
        :param value:
        :return:
        """
        if not re.match(r"1[3-9]\d{9}$", value):
            raise serializers.ValidationError("手机号格式错误")
        return value


    def validate(self, attrs):
        """
        验证access_token
        :param attrs:
        :return:
        """
        tjs = TJS(settings.SECRET_KEY, 300)
        try:
            data = tjs.loads(attrs["access_token"]) # 解析token
        except:
            raise serializers.ValidationError("无效的token")

        # 获取openid
        openid = data.get("openid")
        # attrs中添加openid
        attrs["openid"] = openid
        # 验证短信验证码:
        rel_sms_code = SMSCodeView.checkSMSCode(attrs["mobile"])
        if not rel_sms_code:
            raise serializers.ValidationError('短信验证码失效')
            # 3、比对用户输入的短信和redis中真实短信
        if attrs['sms_code'] != rel_sms_code:
            raise serializers.ValidationError('短信验证不一致')

        # 验证手机号是否被注册过
        try:
            user = User.objects.get(mobile=attrs['mobile'])
        except:
            # 未注册过,注册为新用户
            return attrs
        else:
            # 注册过 查询用户进行绑定
            # 判断密码
            if not user.check_password(attrs['password']):
                raise serializers.ValidationError('密码错误')
            attrs['user'] = user
            return attrs

    def create(self, validated_data):
        """
        保存用户
        :param self:
        :param validated_data:
        :return:
        """
        # 判断用户
        user = validated_data.get('user', None)
        if user is None:
            # 创建用户
            user = User.objects.create_user(username=validated_data['mobile'],
                                            password=validated_data['password'],
                                            mobile=validated_data['mobile'])
        # 绑定操作
        OAuthUser.objects.create(user=user, openid=validated_data["openid"])
        # user_id=user.id
        # 生成加密后的token数据
        jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
        jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER

        payload = jwt_payload_handler(user)  # 生成载荷部分
        token = jwt_encode_handler(payload)  # 生成token

        # user添加token属性
        user.token = token
        user.user_id = user.id

        return user

二、微博登录:

1.前期准备:

新浪微博登录,亦即我们所说的第三方登录,是指用户可以不在本项目中输入密码,而直接通过第三方的验证,成功登录本项目。
若想实现微博登录,需要成为微博开发者,审核通过才可实现。注册方法可参考链接
https://open.weibo.com/connect
成为微博互联开发者后,还需创建应用,即获取本项目对应与微博互联的应用ID,创建应用的方法参考链接
https://open.weibo.com/authentication
微博登录开发文档连接:
https://open.weibo.com/wiki/%E6%8E%88%E6%9D%83%E6%9C%BA%E5%88%B6%E8%AF%B4%E6%98%8E
微博的ai接口:
https://open.weibo.com/wiki/Connect/login
新浪微博错误码列表:
http://blog.unvs.cn/archives/sina-api-error-code.html

2.创建模型类:

参考QQ登录时创建的数据模型

3.微博登录认证–oauth2.0:

OAuth2.0较1.0相比,整个授权验证流程更简单更安全,也是未来最主要的用户身份验证和授权方式。

关于OAuth2.0协议的授权流程可以参考下面的流程图,其中Client指第三方应用,Resource Owner指用户,Authorization Server是我们的授权服务器,Resource Server是API服务器。
在这里插入图片描述
3.1. 引导需要授权的用户到如下地址:
URL:

https://api.weibo.com/oauth2/authorize?client_id=YOUR_CLIENT_ID&response_type=code&redirect_uri=YOUR_REGISTERED_REDIRECT_URI

3.2 如果用户同意授权,页面跳转至 YOUR_REGISTERED_REDIRECT_URI/?code=CODE
3.3换取Access Token
URL

https://api.weibo.com/oauth2/access_token?client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET&grant_type=authorization_code&redirect_uri=YOUR_REGISTERED_REDIRECT_URI&code=CODE

其中client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET可以使用basic方式加入header中,返回值

JSON

{
    "access_token": "SlAV32hkKG",
    "remind_in": 3600,
    "expires_in": 3600
}

3.4 使用获得的Access Token调用API

3.微博登录SDK使用:

  • 由微博官方给出的文档,自定义微博登录SDK:
class OAuthWeibo(object):
    """ 微博认证辅助工具类 """
    def __init__(self,client_id = None, client_secret = None, redirect_uri = None, state=None):
        self.client_id = client_id or settings.WEIBO_CLIENT_ID
        self.redirect_uri = redirect_uri or settings.WEIBO_REDIRECT_URI
        self.client_secret=client_secret or settings.WEIBO_CLIENT_SECRET
        self.state = state or settings.WEIBO_STATE

    def get_weibo_url(self):
        """
        获取微博的验证页面链接
        :return:
        """
        data_dict = {
            'client_id': self.client_id,
            'redirect_uri': self.redirect_uri,
            'state':self.state
        }
        # print(data_dict)
        # 构造微博登录链接
        weibo_url = 'https://api.weibo.com/oauth2/authorize?' + urlencode(data_dict)
        return weibo_url

    def get_access_token(self, code):
        """
        获取微博的accesstoken值
        https://api.weibo.com/oauth2/access_token
        ?client_id=YOUR_CLIENT_ID
        &client_secret=YOUR_CLIENT_SECRET
        &grant_type=authorization_code
        &redirect_uri=YOUR_REGISTERED_REDIRECT_URI
        &code=CODE

        :param code:
        :return:
        """
        # 构造参数
        data_dict = {
            "client_id":self.client_id,
            "client_secret":self.client_secret,
            "grant_type":"authorization_code",
            "redirect_uri":self.redirect_uri,
            "code":code
        }

        # 发送请求
        url = "https://api.weibo.com/oauth2/access_token"
        try:
            response = requests.post(url=url,data=data_dict)

            # 提取数据
            # {
            #     "access_token": "SlAV32hkKG",
            #     "remind_in": 3600,
            #     "expires_in": 3600
            # }
            data = response.text
            data_dict = json.loads(data)
            access_token = data_dict["access_token"]
            print(data_dict)
        except:
            raise Exception('微博请求失败')
        return access_token

4.返回微博登录网址的视图:

4.1 后端接口设计:

请求方式: GET oauth/weibo/authorization/?next=/
请求参数: 查询字符串

参数名类型是否必须说明
nextstr用户微博登录成功后进入美多商城的哪个网址

返回数据: JSON
|返回值| 类型| 是否必须| 说明|
|login_url| str |是 |微博登录网址|

{
	"login_url":"https://api.weibo.com/oauth2/authorize?client_id=YOUR_CLIENT_ID&response_type=code&redirect_uri=YOUR_REGISTERED_REDIRECT_URI"
}

4.2 准备匹配置信息:

在配置文件中添加关于微博登录的应用开发信息:

# settins.dev.py

# 微博登录的参数
WEIBO_CLIENT_ID = '33******85'
WEIBO_CLIENT_SECRET = '74**************************76'
WEIBO_REDIRECT_URI = 'http://www.meiduo.site:8080/sina_callback.html'

4.3 后端逻辑实现:

在oauth/views.py中实现后端逻辑:

class WeiboAuthURLLView(APIView):
    """定义微博第三方登录的视图类"""
    def get(self, request):
        """
        获取微博登录的链接
        oauth/weibo/authorization/?next=/
        :param request:
        :return:
        """
        # 1.通过查询字符串
        next = request.query_params.get('state')
        if not next:
            next = "/"

        # 获取微博登录网页
        oauth = OAuthWeibo(client_id=settings.WEIBO_CLIENT_ID,
                        client_secret=settings.WEIBO_CLIENT_SECRET,
                        redirect_uri=settings.WEIBO_REDIRECT_URI,
                        state=next)
        login_url = oauth.get_weibo_url()
        return Response({"login_url": login_url})

4.3 前端:

修改login.js, 在methods中增加weibo_login方法:

// 微博登录
        weibo_login: function(){
            var next = this.get_query_string('next') || '/';
            axios.get(this.host + '/oauth/weibo/authorization/?next=' + next, {
                    responseType: 'json',
                    withCredentials: true
                })
                .then(response => {
                    location.href = response.data.login_url;
                })
                .catch(error => {
                    console.log(error.response.data);
                })
        }

5.获取微博用户的token

在微博将用户重定向到此网页的时候,重定向的网址会携带微博提供的code参数,用于获取用户信息使用,我们需要将这个code参数发送给后端,在后端中使用code参数向微博请求用户的身份信息,并查询与该微博用户绑定的用户。

5.1 后端接口设计:

请求方式:GET oauth/sina/user/?code=0e*******a6a
**请求参数:**查询字符串参数
|参数| 类型| 是否必传| 说明|
|code| str| 是 |微博返回的授权凭证code|
**返回数据:**JSON

返回值类型是否必须说明
access_tokenstr用户是第一次使用微博登录时返回,其中包含weibotoken,用于绑定身份使用,注意这个是我们自己生成的
tokenstr用户不是第一次使用微博登录时返回,登录成功的JWT token
usernamestr用户不是第一次使用微博登录时返回,用户名
user_id int用户不是第一次使用weibo登录时返回,用户id
class WeiboOauthView(APIView):
    """验证微博登录"""
    def get(self, request):
        """
        第三方登录检查
        oauth/sina/user/
        ?code=0e67548e9e075577630cc983ff79fa6a
        :param request:
        :return:
        """
        # 1.获取code值
        code = request.query_params.get("code")

        # 2.检查参数
        if not code:
            return Response({'errors': '缺少code值'}, status=400)

        # 3.获取token值
        next = "/"

        # 获取微博登录网页
        weiboauth = OAuthWeibo(client_id=settings.WEIBO_CLIENT_ID,
                        client_secret=settings.WEIBO_CLIENT_SECRET,
                        redirect_uri=settings.WEIBO_REDIRECT_URI,
                        state=next)
        weibotoken = weiboauth.get_access_token(code=code)
        print(weibotoken)

        # 5.判断是否绑定过美多账号
        try:
            weibo_user = OAuthUser.objects.get(weibotoken=weibotoken)
        except:
            # 6.未绑定,进入绑定页面,完成绑定
            tjs = TJS(settings.SECRET_KEY, 300)
            weibotoken = tjs.dumps({'weibotoken': weibotoken}).decode()

            return Response({'access_token': weibotoken})
        else:
            # 7.绑定过,则登录成功
            # 生成jwt-token值
            user = weibo_user.user
            jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
            jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER

            payload = jwt_payload_handler(user)  # 生成载荷部分
            token = jwt_encode_handler(payload)  # 生成token

            response = Response(
                {
                    'token': token,
                    'username': user.username,
                    'user_id': user.id
                }
            )

        return response

6.weiboTkoen绑定美多商城用户:

如果用户是首次使用微博登录,则需要绑定用户,页面如下:
在这里插入图片描述
业务逻辑:

用户需要填写手机号、密码、图片验证码、短信验证码
如果用户未在美多商城注册过,则会将手机号作为用户名为用户创建一个美多账户,并绑定用户
如果用户已在美多商城注册过,则检验密码后直接绑定用户

6.1.后端接口设计

请求方式: POST /oauth/sina/user/
请求方式: JSON

参数类型是否必须说明
mobilestr手机号
passwordstr密码
sms_codestr短信验证码
access_tokenstr凭据 (包含微博token)

返回数据: JSON

返回值类型是否必须说明
tokenstr是 JWT token
idint用户id
usernamestr用户名

6.2 后端逻辑实现:

# 在oauth/views.py修改WeiboOauthView视图

class WeiboOauthView(APIView):
    """验证微博登录"""
    def get(self, request):
		...
	
	def post(self,request):
        """
        微博用户未绑定,绑定微博用户
        :return:
        """
        # 1. 获取前端数据
        data = request.data
        # 2.调用序列化起验证数据
        ser = WeiboOauthSerializers(data=data)
        ser.is_valid()
        print(ser.errors)
        # 保存绑定数据
        ser.save()
        return Response(ser.data)
# oauth/serializers.py

class WeiboOauthSerializers(serializers.ModelSerializer):
    """微博验证序列化器"""

    # 指名模型类中没有的字段
    mobile = serializers.CharField(max_length=11)
    sms_code = serializers.CharField(max_length=6, min_length=6, write_only=True)
    access_token = serializers.CharField(write_only=True)  # 反序列化输入

    token = serializers.CharField(read_only=True)
    user_id = serializers.IntegerField(read_only=True)  # 序列化输出

    class Meta:
        model = User
        fields = ('password', 'mobile', 'username', 'sms_code', 'token', 'access_token', 'user_id')

        extra_kwargs = {
            'username': {
                'read_only': True
            },
            'password': {
                'write_only': True
            }
        }

    def validated_mobile(self, value):
        """
        验证手机号
        :param value:
        :return:
        """
        if not re.match(r"1[3-9]\d{9}$", value):
            raise serializers.ValidationError("手机号格式错误")
        return value

    def validate(self, attrs):
        """
        验证access_token
        :param attrs:
        :return:
        """
        tjs = TJS(settings.SECRET_KEY, 300)
        try:
            data = tjs.loads(attrs["access_token"])  # 解析token
        except:
            raise serializers.ValidationError("无效的token")

        # 获取weibotoken
        weibotoken = data.get("weibotoken")
        # attrs中添加weibotoken
        attrs["weibotoken"] = weibotoken
        # 验证短信验证码:
        rel_sms_code = SMSCodeView.checkSMSCode(attrs["mobile"])
        if not rel_sms_code:
            raise serializers.ValidationError('短信验证码失效')
            # 3、比对用户输入的短信和redis中真实短信
        if attrs['sms_code'] != rel_sms_code:
            raise serializers.ValidationError('短信验证不一致')
        # 验证手机号是否被注册过
        try:
            user = User.objects.get(mobile=attrs['mobile'])
        except:
            # 未注册过,注册为新用户
            return attrs
        else:
            # 注册过 查询用户进行绑定
            # 判断密码
            if not user.check_password(attrs['password']):
                raise serializers.ValidationError('密码错误')
            attrs['user'] = user
            return attrs

    def create(self, validated_data):
        """
        保存用户
        :param self:
        :param validated_data:
        :return:
        """
        # 判断用户
        user = validated_data.get('user', None)
        if user is None:
            # 创建用户
            user = User.objects.create_user(username=validated_data['mobile'],
                                            password=validated_data['password'],
                                            mobile=validated_data['mobile'])
        # 绑定操作
        OAuthUser.objects.create(user=user, weibotoken=validated_data["weibotoken"])
        # user_id=user.id
        # 生成加密后的token数据
        jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
        jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER

        payload = jwt_payload_handler(user)  # 生成载荷部分
        token = jwt_encode_handler(payload)  # 生成token

        # user添加token属性
        user.token = token
        user.user_id = user.id

        return user
  • 0
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

浅弋、璃鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值