• Django商城项目笔记No.12用户部分-QQ登录2获取QQ用户openid


    Django商城项目笔记No.12用户部分-QQ登录2获取QQ用户openid

    上一步获取QQ登录网址之后,测试登录之后本该跳转到这个界面

    但是报错了:

    新建oauth_callback.html

    <!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/host.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" id="app" 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="image_code" v-on:blur="check_image_code" name="pic_code" id="pic_code" class="msg_input">
                                    <img v-bind:src="image_code_url" v-on:click="generate_image_code" alt="图形验证码" class="pic_code">
                                    <span v-show="error_image_code" class="error_tip">{{ error_image_code_message }}</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>
    View Code

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

    var vm = new Vue({
        el: '#app',
        data: {
            host: host,
            is_show_waiting: true,
    
            error_password: false,
            error_phone: false,
            error_image_code: false,
            error_sms_code: false,
            error_image_code_message: '',
            error_phone_message: '',
            error_sms_code_message: '',
    
            image_code_id: '', // 图片验证码id
            image_code_url: '',
    
            sms_code_tip: '获取短信验证码',
            sending_flag: false, // 正在发送短信标志
    
            password: '',
            mobile: '', 
            image_code: '',
            sms_code: '',
            access_token: ''
        },
        mounted: function(){
    
        },
        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;
            },
            // 生成uuid
            generate_uuid: function(){
                var d = new Date().getTime();
                if(window.performance && typeof window.performance.now === "function"){
                    d += performance.now(); //use high-precision timer if available
                }
                var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
                    var r = (d + Math.random()*16)%16 | 0;
                    d = Math.floor(d/16);
                    return (c =='x' ? r : (r&0x3|0x8)).toString(16);
                });
                return uuid;
            },
            // 生成一个图片验证码的编号,并设置页面中图片验证码img标签的src属性
            generate_image_code: function(){
                // 生成一个编号
                // 严格一点的使用uuid保证编号唯一, 不是很严谨的情况下,也可以使用时间戳
                this.image_code_id = this.generate_uuid();
    
                // 设置页面中图片验证码img标签的src属性
                this.image_code_url = this.host + "/image_codes/" + this.image_code_id + "/";
            },
            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_image_code: function (){
                if(!this.image_code) {
                    this.error_image_code_message = '请填写图片验证码';
                    this.error_image_code = true;
                } else {
                    this.error_image_code = false;
                }    
            },
            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(){
                if (this.sending_flag == true) {
                    return;
                } 
                this.sending_flag = true;
    
                // 校验参数,保证输入框有数据填写
                this.check_phone();
                this.check_image_code();
    
                if (this.error_phone == true || this.error_image_code == true) {
                    this.sending_flag = false;
                    return;
                }
    
                // 向后端接口发送请求,让后端发送短信验证码
                axios.get(this.host + '/sms_codes/' + this.mobile + '/?text=' + this.image_code+'&image_code_id='+ this.image_code_id, {
                        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_image_code_message = '图片验证码有误';
                            this.error_image_code = true;
                        } else {
                            console.log(error.response.data);
                        }
                        this.sending_flag = false;
                    })
            },
            // 保存
            on_submit: function(){
                this.check_pwd();
                this.check_phone();
                this.check_sms_code();
    
            }
        }
    });
    View Code

    重新测试,就成功了

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

    那么接下来,我们就可以处理第二步了

    根据code获取access_token

    返回的情况有两种。

    第一种是没有绑定过,就返回access_token

    第二种是绑定过了,那就返回用户的消息。

    注意:这个access_token是自己生成的

    为啥呢要自己生成access_token呢?

    首先返回这个access_token是在未绑定的时候,显示如下界面的时候,返回的:

    在这个界面是需要openid的(因为点击保存时,后台需要拿着用户手机号与openid进行绑定),而我们返回的access_token中包含openid

    这个access_tokenqq服务器返回的不一样,这个是我们拿着qq服务器返回的openid做了一个处理,避免前端拿到openid修改。

    因为如果直接将openid给前端,那么前端是可以对openid进行修改的。

    如果将openid修改为B用户的openid,本来openidA用户的,那么点击保存的时候,我们就将A用户的美多账号与B用户的openid进行了绑定。

    所以避免这种事情的发生,我们就对openid进行一个处理,如果前端修改,在绑定的时候,我们后端可以知道修改了。

    itsdangerous模块使用

    使用itsdangerous生成凭据access_token

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

    # 安装
    pip install itsdangerous
    TimedJSONWebSignatureSerializer的使用

    使用TimedJSONWebSignatureSerializer可以生成带有有效期的token

    TimedJsonWebSignatureSerializer的用法与Json的用法类似:

    获取access_token实现

    分析完接口之后,我们来写视图逻辑,视图逻辑分析如下:

     

    补充如下代码

    这里调用了get_access_token方法,此方法代码如下:

        def get_access_token(self, code):
            url = 'https://graph.qq.com/oauth2.0/token?'
            params = {
                'grant_type': 'authorization_code',
                'client_id': self.client_id,
                'client_secret': self.client_secret,
                'code': code,
                'redirect_uri': self.redirect_uri
            }
            url += urllib.parse.urlencode(params)
            try:
                # 发送请求
                resp = urlopen(url)
                # 读取响应体数据
                resp_data = resp.read()  # bytes
                resp_data = resp_data.decode()  # str
                # access_token=FE04************************CCE2&expires_in=7776000&refresh_token=88E4************************BE14
                # 解析access_token
                resp_dict = urllib.parse.parse_qs(resp_data)
            except Exception as e:
                logger.error('获取access_token异常:%s' % e)
                raise OAuthQQAPIError
            else:
                access_token = resp_dict.get('access_token')
                return access_token[0]
    View Code

    还抛出了一个自定义异常,此异常代码如下:

    还用到日志logger

    获取openid实现

    接下来处理第三步,获取openid

     

    视图逻辑代码如下:

    class QQAuthUserView(CreateAPIView):
        """
        QQ登录的用户 ?code=xxxxxx
        """
        serializer_class = serializers.OAuthQQUserSerializer
        def get(self, request):
            # 获取code
            code = request.query_params.get('code')
            if not code:
                return Response({'message': '缺少code参数'}, status=status.HTTP_400_BAD_REQUEST)
    
            # 凭借code 获取access_token
            oauth_qq = OAuthQQ()
            try:
                access_token = oauth_qq.get_access_token(code)
                # 凭借access_token获取openid
                openid = oauth_qq.get_openid(access_token)
            except OAuthQQAPIError:
                return Response({'message': '访问QQ接口异常'}, status=status.HTTP_503_SERVICE_UNAVAILABLE)
    
            # 根据openid查数据库
            try:
                oauth_qq_user = OAuthQQUser.objects.get(openid=openid)
            except OAuthQQUser.DoesNotExist:
                # 如果数据不存在,处理openid并返回
                access_token = oauth_qq.generate_bind_user_access_token(openid)
                return Response({'access_token': access_token})
            else:
                # 如果存在,证明绑定过了已经,那么就签发JWTtoken
                jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
                jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
    
                user = oauth_qq_user.user
                payload = jwt_payload_handler(user)
                token = jwt_encode_handler(payload)
    
                return Response({
                    'username': user.username,
                    'user_id': user.id,
                    'token': token
                })
    View Code

    调用的get_openid方法如下:

        def get_openid(self, access_token):
            url = 'https://graph.qq.com/oauth2.0/me?access_token=' + access_token
    
            try:
                # 发送请求
                resp = urlopen(url)
                # 读取响应体
                resp_data = resp.read()  # bytes
                resp_data = resp_data.decode()  # str
    
                # callback( {"client_id":"YOUR_APPID","openid":"YOUR_OPENID"} );
                # 解析
                resp_data = resp_data[10:-4]
                resp_dict = json.loads(resp_data)
            except Exception as e:
                logger.error('获取openid异常:%s' % e)
                raise OAuthQQAPIError
            else:
                openid = resp_dict.get('openid')
                return openid
    View Code

    调用的generate_bind_user_access_token如下:

        def generate_bind_user_access_token(self, openid):
            serializer = TJWSSerializer(settings.SECRET_KEY, expires_in=constants.SAVE_QQ_USER_TOKEN_EXPIRES)
            token = serializer.dumps({'openid': openid})
            return token.decode()
    View Code

    用到的常量:

    获取openid前端实现与测试

    后端逻辑处理完,需要配置url

     

    修改oauth_callback.js

        mounted: function(){
            // 从路径中获取qq重定向返回的code
            var code = this.get_query_string('code');
            axios.get(this.host + '/oauth/qq/user/?code=' + code, {
                    responseType: 'json',
                })
                .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;
                        var state = this.get_query_string('state');
                        location.href = state;
                    } else {
                        // 用户未绑定
                        this.access_token = response.data.access_token;
                        this.generate_image_code();
                        this.is_show_waiting = false;
                    }
                })
                .catch(error => {
                    console.log(error.response.data);
                    alert('服务器异常');
                })
        },
    View Code

    绑定QQ用户实现

    接下来在QQAuthUserView中增加post逻辑,分析如下:

    这个post中的逻辑,跟创建模型逻辑一样,那么我们就可以继承CreateApiView

    所以,上边的逻辑,都放到序列化器OAuthQQUserSerializer中:

    class OAuthQQUserSerializer(serializers.ModelSerializer):
        sms_code = serializers.CharField(label='短信验证码', write_only=True)
        access_token = serializers.CharField(label='操作凭证', write_only=True)
        token = serializers.CharField(read_only=True)
        mobile = serializers.RegexField(label='手机号', regex=r'^1[3-9]d{9}$')
    
        class Meta:
            model = User
            fields = ('mobile', 'password', 'sms_code', 'access_token', 'id', 'username', 'token')
            extra_kwargs = {
                'username': {
                    'read_only': True
                },
                'password': {
                    'write_only': True,
                    'min_length': 8,
                    'max_length': 20,
                    'error_messages': {
                        'min_length': '仅允许8-20个字符的密码',
                        'max_length': '仅允许8-20个字符的密码',
                    }
                }
            }
    
    
        def validate(self, attrs):
            # 校验access_token
            access_token = attrs['access_token']
            openid = OAuthQQ.check_bind_user_access_token(access_token)
            if not openid:
                raise serializers.ValidationError('无效的access_token')
    
            attrs['openid'] = openid
    
            # 校验短信验证码
            mobile = attrs['mobile']
            sms_code = attrs['sms_code']
            redis_conn = get_redis_connection('verify_codes')
            real_sms_code = redis_conn.get('sms_%s' % mobile)
            if sms_code != real_sms_code.decode():
                raise serializers.ValidationError('短信验证码错误')
    
            # 如果用户存在,检查密码
            try:
                user = User.objects.get(mobile=mobile)
            except User.DoesNotExist:
                pass
            else:
                password = attrs['password']
                if not user.check_password(password):
                    raise serializers.ValidationError('手机号所对应的密码错误')
    
                attrs['user'] = user
    
            return attrs
    
        def create(self, validated_data):
            openid = validated_data['openid']
            user = validated_data['user']
            mobile = validated_data['mobile']
            password = validated_data['password']
    
            # 如果用户不存在,创建用户
            if not user:
                user = User.objects.create(username=mobile, mobile=mobile, password=password)
            # 再绑定QQ,创建OAuthQQUser数据
            OAuthQQUser.objects.create(user=user, openid=openid)
    
            # 签发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)
    
            user.token = token
    
            return user
    View Code

    check_bind_user_access_token方法,代码如下:

        @staticmethod
        def check_bind_user_access_token(access_token):
            serializer = TJWSSerializer(settings.SECRET_KEY, expires_in=constants.SAVE_QQ_USER_TOKEN_EXPIRES)
            try:
                data = serializer.loads(access_token)
            except BadData:
                return None
            else:
                return data['openid']
    View Code

    前端代码

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

    测试成功

     

  • 相关阅读:
    UVA 11991 Easy Problem from Rujia Liu(map,vector的使用)
    UVA 11995 I Can Guess the Data Structure! (STL应用)
    HDU 2795 Billboard(线段树,单点更新)
    HDU 1394 Minimum Inversion Number (线段树,单点更新)
    UVA 11827 Maximum GCD(读入技巧,stringstream的使用)
    contest 2 总结
    Const 1 总结
    开始进行大量题目练习
    函数式线段树的个人理解
    poj 2318 TOYS
  • 原文地址:https://www.cnblogs.com/blog-rui/p/9747078.html
Copyright © 2020-2023  润新知