ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

Django中使用QQ登录

2022-09-14 12:03:29  阅读:424  来源: 互联网

标签:QQ openid code 登录 用户 Django token user


Django中使用QQ登录

1.返回QQ登录网址的视图

(1)后端接口设计:

请求方式: GET /oauth/qq/authorization/?next=xxx

请求参数: 查询字符串

参数名 类型 是否必须 说明
next str 用户QQ登录成功后进入网站的哪个具体网址

返回数据: JSON

{
    "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"
}
返回值 类型 是否必须 说明
login_url str qq登录网址

(2)准备配置信息:

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

# QQ登录参数,填写自己的应用信息
QQ_CLIENT_ID = '101******'
QQ_CLIENT_SECRET = 'c6ce949e04e**********'
QQ_REDIRECT_URI = 'http://xxx.com/xxx.html'

(3)后端逻辑实现:

在视图中实现后端逻辑

# url(r'^qq/authorization/$', views.QQAuthURLView.as_view()),
class QQAuthURLView(APIView):
    """提供QQ登录页面网址
    https://graph.qq.com/oauth2.0/authorize?response_type=code&client_id=xxx&redirect_uri=xxx&state=xxx
    """
    def get(self, request):

        # next表示从哪个页面进入到的登录页面,将来登录成功后,就自动回到那个页面
        next = request.query_params.get('next')
        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})

(4)前端

编写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);
                })
        }

2.准备回调页

用户在QQ登录成功后,QQ会将用户重定向回我们配置的回调callback网址:

http://xxx.com/xxx.html

我们在前端项目目录中新建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 xxx商业股份有限公司 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: {
        // data部分的数据不重要,不需要理解是干嘛的,重要的是前端将code发给后端
        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;
        },
});

3.获取QQ用户OpenID

在QQ将用户重定向到回调页的时候,重定向的网址会携带QQ提供的code参数,用于获取用户信息使用,我们需要将这个code参数发送给后端,在后端中使用code参数向QQ请求用户的身份信息,并查询与该QQ用户绑定的用户,如果绑定了,也就是用户不是第一次使用QQ登录,后端就返回token,进行状态保持,如果没有绑定,后端会把openid进行加密处理后返回前端,让前端保存,当用户在前端填写完表单绑定用户时,前端把用户信息和加密的openid一起发给后端,后端解密openid,把用户信息和openid分别存进对应的表中,完成绑定并且返回token完成状态保持。

(1)后端接口设计

请求方式 : GET /oauth/qq/user/?code=xxx

请求参数: 查询字符串参数

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

返回数据: JSON

{
    "access_token": xxxx,
}
或
{
    "token": "xxx",
    "username": "python",
    "user_id": 1
}
返回值 类型 是否必须 说明
access_token str 用户是第一次使用QQ登录时返回,其中包含openid,用于绑定身份使用,注意这个是我们自己生成的
token str 用户不是第一次使用QQ登录时返回,登录成功的JWT token
username str 用户不是第一次使用QQ登录时返回,用户名
user_id int 用户不是第一次使用QQ登录时返回,用户id

(2)后端逻辑实现

  • 创建模型类

    创建模型类基类,用于增加数据新建时间和更新时间。

    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/models.py中定义QQ身份(openid)与用户模型类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)
    
        class Meta:
            db_table = 'tb_oauth_qq'
            verbose_name = 'QQ登录用户数据'
            verbose_name_plural = verbose_name
    

    进行数据库迁移

    python manage.py makemigrations
    python manage.py migrate
    
  • 在views.py中实现后端逻辑

# url(r'^qq/user/$', views.QQAuthUserView.as_view()),
class QQAuthUserView(GenericAPIView):
    """用户扫码登录的回调处理"""

    # 指定序列化器
    serializer_class = serializers.QQAuthUserSerializer

    def get(self, request):
        # 提取code请求参数
        code = request.query_params.get('code')
        if not code:
            return Response({'message':'缺少code'}, status=status.HTTP_400_BAD_REQUEST)

        # 创建工具对象
        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:
            return Response({'message': 'QQ服务异常'}, status=status.HTTP_503_SERVICE_UNAVAILABLE)

(3)根据OpenID查询用户

  • 如果能够查询到美多商城用户,就直接生成状态保持信息,登录到美多商城
  • 如果不能查询到美多商城用户,就直接将OpenID序列化并返回给前端,用于后续的绑定美多商城用户操作
# url(r'^qq/user/$', views.QQAuthUserView.as_view()),
class QQAuthUserView(GenericAPIView):
    """用户扫码登录的回调处理"""

    # 指定序列化器
    serializer_class = serializers.QQAuthUserSerializer

    def get(self, request):
        # 提取code请求参数
        code = request.query_params.get('code')
        if not code:
            return Response({'message':'缺少code'}, status=status.HTTP_400_BAD_REQUEST)

        # 创建工具对象
        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:
            return Response({'message': 'QQ服务异常'}, status=status.HTTP_503_SERVICE_UNAVAILABLE)

        # 使用openid查询该QQ用户是否在美多商城中绑定过用户
        try:
            oauth_user = OAuthQQUser.objects.get(openid=openid)
        except OAuthQQUser.DoesNotExist:
            # 如果openid没绑定美多商城用户,创建用户并绑定到openid
            # 为了能够在后续的绑定用户操作中前端可以使用openid,在这里将openid签名后响应给前端
            access_token_openid = generate_save_user_token(openid)  # generate_save_user_token方法为自己编写的加密token的方法,下面会介绍
            return Response({'access_token':access_token_openid})
        else:
            # 如果openid已绑定美多商城用户,直接生成JWT token,并返回
            jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
            jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER

            # 获取oauth_user关联的user
            user = oauth_user.user
            payload = jwt_payload_handler(user)
            token = jwt_encode_handler(payload)

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

            return response
  • 准备序列化OpenID的工具方法
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
from django.conf import settings

def generate_save_user_token(openid):
    """
    生成保存用户数据的token
    :param openid: 用户的openid
    :return: token
    """
    serializer = Serializer(settings.SECRET_KEY, expires_in=constants.SAVE_QQ_USER_TOKEN_EXPIRES)
    data = {'openid': openid}
    token = serializer.dumps(data)
    return token.decode()

补充:使用itsdangerous的使用

  • itsdangerous模块的参考资料连接http://itsdangerous.readthedocs.io/en/latest/

  • 安装:pip install itsdangerous== 1.1.0

  • 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

4.OpenID绑定美多商城用户

如果用户是首次使用QQ登录,则需要绑定用户。

业务逻辑:

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

(1)后端接口设计

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

请求参数: JSON 或 表单

参数 类型 是否必须 说明
mobile str 手机号
password str 密码
sms_code str 短信验证码
access_token str 凭据 (包含openid)

返回数据: JSON

返回值 类型 是否必须 说明
token str JWT token
id int 用户id
username str 用户名

(2)后端逻辑实现

  • 修改QQAuthUserView视图,添加post方法:
# url(r'^qq/user/$', views.QQAuthUserView.as_view()),
class QQAuthUserView(GenericAPIView):
    """用户扫码登录的回调处理"""

    # 指定序列化器
    serializer_class = serializers.QQAuthUserSerializer

    def get(self, request):
        ......

    def post(self, request):
        """openid绑定到用户"""

        # 获取序列化器对象
        serializer = self.get_serializer(data=request.data)
        # 开启校验
        serializer.is_valid(raise_exception=True)
        # 保存校验结果,并接收
        user = serializer.save()

        # 生成JWT 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)

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

        return response
  • 新建serializers.py文件,编写序列化器:
class QQAuthUserSerializer(serializers.Serializer):
    """
    QQ登录创建用户序列化器
    """
    access_token = serializers.CharField(label='操作凭证')
    mobile = serializers.RegexField(label='手机号', regex=r'^1[3-9]\d{9}$')
    password = serializers.CharField(label='密码', max_length=20, min_length=8)
    sms_code = serializers.CharField(label='短信验证码')

    def validate(self, data):
        # 检验access_token
        access_token = data['access_token']
        # 获取身份凭证
        openid = check_save_user_token(access_token) # check_save_user_token 解密openid
        if not openid:
            raise serializers.ValidationError('无效的access_token')

        # 将openid放在校验字典中,后面会使用
        data['openid'] = openid

        # 检验短信验证码
        mobile = data['mobile']
        sms_code = data['sms_code']
        redis_conn = get_redis_connection('verify_codes')
        real_sms_code = redis_conn.get('sms_%s' % mobile)
        if real_sms_code.decode() != sms_code:
            raise serializers.ValidationError('短信验证码错误')

        # 如果用户存在,检查用户密码
        try:
            user = User.objects.get(mobile=mobile)
        except User.DoesNotExist:
            pass
        else:
            password = data['password']
            if not user.check_password(password):
                raise serializers.ValidationError('密码错误')

            # 将认证后的user放进校验字典中,后续会使用
            data['user'] = user
        return data

    def create(self, validated_data):
        # 获取校验的用户
        user = validated_data.get('user')

        if not user:
            # 用户不存在,新建用户
            user = User.objects.create_user(
                username=validated_data['mobile'],
                password=validated_data['password'],
                mobile=validated_data['mobile'],
            )

        # 将用户绑定openid
        OAuthQQUser.objects.create(
            openid=validated_data['openid'],
            user=user
        )
        # 返回用户数据
        return user
  • 准备序列化OpenID的工具方法
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
from django.conf import settings

def check_save_user_token(access_token):
    """
    检验保存用户数据的token
    :param token: token
    :return: openid or None
    """
    serializer = Serializer(settings.SECRET_KEY, expires_in=constants.SAVE_QQ_USER_TOKEN_EXPIRES)
    try:
        data = serializer.loads(access_token)
    except BadData:
        return None
    else:
        return data.get('openid')

(3)前端

      // 保存
        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);
                        }
                    })
            }
        }

标签:QQ,openid,code,登录,用户,Django,token,user
来源: https://www.cnblogs.com/minqiliang/p/16692534.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有