Loading

Django中使用QQ登录

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);
                        }
                    })
            }
        }
posted @ 2022-09-14 11:47  minqiliang  阅读(108)  评论(0编辑  收藏  举报
-->