DRF框架QQ登陸功能

用戶模塊---QQ登陸
流程圖
QQ登陸文檔:http://wiki.connect.qq.com/%E5%87%86%E5%A4%87%E5%B7%A5%E4%BD%9C_oauth2-0前端

流程簡述:
1.當點擊qq登陸圖標時,進入生成登陸url的接口
2.在前端的回調函數中跳轉到qq登陸的掃碼頁面
3.掃碼登錄後,qq會攜帶code訪問申請時指定的回調地址,
使用code獲取access_token
使用access_token最終獲取openid
4用來判斷用戶是不是第一次使用QQ登陸
5.是,返回綁定/註冊,頁面第一次使用QQ登陸,額外判斷,是否已經註冊帳號
5.1是,統一跳轉到綁定/註冊界面,
5.1.1將openid和帳號進行綁定/註冊,手動調用jwt功能返回jwt,id,usernamepython

5.2否,返回首頁,手動調用jwt功能返回jwt,id,usernameredis

具體實現:django

使用到的模塊:json

import urllib

urllib.parse.urlencode(query)  # 將字典轉換爲url路徑中的查詢字符串

urllib.parse.parse_qs(qs)  # 將查詢字符串格式數據轉換爲python的字典

urllib.request.urlopen(url, data=None)

在python後端發起http請求,

若是data爲None,發送GET請求,

若是data不爲None,發送POST請求

response.read().decode()
返回response響應對象,能夠經過read()讀取響應體數據,須要注意讀取出的響應體數據爲bytes類型

手動調用jwt生成token用於驗證登陸狀態後端

from rest_framework_jwt.settings import api_settings

jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
payload = jwt_payload_handler(user)
jwt_token = jwt_encode_handler(payload)

itsdangerous模塊用於生成token和解析tokenapi

from itsdangerous import TimedJSONWebSignatureSerializer


serializer = TimedJSONWebSignatureSerializer(settings.SECRET_KEY, 300)
# 須要轉換成token的數據
data = {'openid': openid}

# .dumps生成token,bytes類型,須要解碼
token = serializer.dumps(data).decode()

# .loads將token解析爲須要的數據,
openid = serializer.loads(token).get('openid')

模型類,保存QQ的openid和本站用戶之間的關係:函數

from django.db import models

class BaseModel(models.Model):
    """基類模型"""
    create_time = models.DateTimeField(auto_now_add=True, verbose_name="建立時間")
    update_time = models.DateTimeField(auto_now=True, verbose_name="更新時間")

    class Meta:
        # 指定BaseModel爲抽象類,不會建立實體表
        abstract = True

class OAuthQQUser(BaseModel):
    """QQ登陸的模型"""
    openid = models.CharField(max_length=64, verbose_name='openid', db_index=True)
    user = models.ForeignKey('user.User', on_delete=models.CASCADE, verbose_name='用戶')
    objects = models.Manager()

    class Meta:
        db_table = 'tb_oauth_qq'
        verbose_name = 'QQ登陸用戶數據'
        verbose_name_plural = verbose_name

定義一個工具類,主要負責生成token和驗證token是否正確工具

class OAuthQQ(object):
    """用於qq登陸的工具類"""

    def __init__(self, client_id=None, client_secret=None, redirect_uri=None, state=None):
        """QQ登陸開發文檔中須要的參數"""
        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  # 用於保存登陸成功後的跳轉頁面路徑

在OAuthQQ工具類中新增生成登陸url的方法post

def generate_qq_login_url(self):
        """生成用於qq登陸掃碼的url地址"""
        params = {
            'response_type': 'code',  # 默認值
            'client_id': self.client_id,
            'redirect_uri': self.redirect_uri,
            'state': self.state,
            'scope': 'get_user_info',  # 用戶勾選的受權範圍,get_user_info表示,獲取登陸用戶的暱稱、頭像、性別
        }
        url = 'https://graph.qq.com/oauth2.0/authorize?'
        # 拼接查詢字符串,
        url += parse.urlencode(params)
        return url

定義返回掃碼QQ登陸url的接口

# 在點擊qq登陸圖標時向接口發起請求
# 後端生成用於QQ掃碼登陸的頁面的url地址
# 在前端回調函數中執行

# GET /oauth/qq/authorization/?state=xxx

class QQAuthUrlView(APIView):
    """獲取QQ掃碼登陸的網址接口"""

    def get(self, request):
        """
        :return 掃碼的url地址
        """
        state = request.query_params.get('state')
        oauthqq = OAuthQQ(state=state)
        qq_login_url = oauthqq.generate_qq_login_url()
        return Response({'qq_login_url': qq_login_url})

前端將code當參數傳入後端接口,生成獲取access_token的url
在OAuthQQ工具類新增,使用code請求並獲取QQ的access_token,的方法

def get_qq_access_token(self, code):
        """獲取access_token"""
        params = {
            'grant_type': 'authorization_code',
            'client_id': self.client_id,
            'client_secret': self.client_secret,
            'code': code,
            'redirect_uri': self.redirect_uri,
        }
        url = 'https://graph.qq.com/oauth2.0/token?'
        url += parse.urlencode(params)
        # 向qq方發起http請求,獲取包含access_token的查詢字符串
        # 形式access_token=FE04************************CCE2&expires_in=7776000&refresh_token=88E4************************BE14
        try:
            response = request.urlopen(url)
            response_data = response.read().decode()
            # 講查詢字符串轉換爲python中的字典,[{}]
            data = parse.parse_qs(response_data)
            access_token = data.get('access_token', None)[0]
        except Exception as e:
            logger.error(e)
            raise Exception('獲取access_token異常')
        return access_token

根據access_token生成獲取openid的url
後端發送http請求,從返回值中獲取openid

def get_qq_openid(self, access_token):
        """獲取openid"""
        url = 'https://graph.qq.com/oauth2.0/me?access_token='
        url += access_token
        logger.error(url)
        try:
            response = request.urlopen(url)
            response_data = response.read().decode()
            # 返回一個字符串  callback( {"client_id":"YOUR_APPID","openid":"YOUR_OPENID"} )\n;
            data_dict = json.loads(response_data[10:-4])
            openid = data_dict.get('openid', None)
        except Exception as e:
            logger.error(e)
            raise Exception('獲取openid異常')
        return openid

根據openid去判斷,該用戶是不是第一次使用QQ登陸功能.
定義類視圖和serializers序列化器

class QQAuthUserView(GenericAPIView):
    """QQ登陸後接口"""
    serializer_class = OAuthQQUserSerializer

    def get(self, request):
        """QQ登陸"""
        code = request.query_params.get('code')
        if not code:
            return Response({'message': 'code不存在'}, 400)

        # 目標是經過 code獲取access_token
        oauthqq = OAuthQQ()
        access_token = oauthqq.get_qq_access_token(code)

        # 經過access_token獲取openid
        openid = oauthqq.get_qq_openid(access_token)

        # 獲取openid後須要判斷
        # oauthqquser = OAuthQQUser.get

        try:
            oauthqquser = OAuthQQUser.objects.get(openid=openid)
        except Exception as e:
            logger.error('此人未綁定或未註冊:%s' % e)
            # 1.第一次用qq登陸
            # 使用openid生成記錄qq身份的token,以便註冊或綁定時驗證身份
            access_token = OAuthQQ.generate_save_user_token(openid)
            return Response({'access_token': access_token})
        # 1.1 已經註冊本站帳號--->跳轉綁定界面
        # 1.2 未註冊本站帳號--->註冊並綁定

        # 2.之前已經qq登陸過(必定有本站帳號)
        else:
            user = oauthqquser.user
            # 生成jwt_token,用於記錄登陸狀態
            jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
            jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
            payload = jwt_payload_handler(user)
            jwt_token = jwt_encode_handler(payload)

            data = {
                'user_id': user.id,
                'username': user.username,
                'token': jwt_token
            }
            return Response(data=data)

    def post(self, request):
        """QQ帳號綁定和新增功能"""

        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        user = serializer.save()

        jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
        jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
        payload = jwt_payload_handler(user)
        jwt_token = jwt_encode_handler(payload)

        data = {
            'user_id': user.id,
            'username': user.mobile,
            'token': jwt_token
        }
        return Response(data=data)

序列化器

class OAuthQQUserSerializer(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 = OAuthQQ.check_token_by_openid(access_token)
        if not openid:
            raise serializers.ValidationError('access_token失效')
        # 檢驗短信驗證碼
        mobile = data['mobile']
        sms_code = data['sms_code']
        redis_conn = get_redis_connection('verify_codes')
        real_sms_code = redis_conn.get('sms_%s' % mobile).decode()
        if not real_sms_code:
            raise serializers.ValidationError('短信驗證碼失效或過時')

        if real_sms_code != sms_code:
            raise serializers.ValidationError('短信驗證碼錯誤')

        # 若是用戶存在,檢查用戶密碼
        try:
            user = User.objects.get(mobile=mobile)
        except Exception as e:
            logger.error('本站用戶不存在,等待註冊---%s' % e)
            pass
        else:
            # 若是存在就校驗密碼
            password = data['password']
            if not user.check_password(password):
                raise serializers.ValidationError('密碼錯誤')
            data['user'] = user

        data['openid'] = openid
        return data

    def create(self, validated_data):
        user = validated_data.get('user', None)
        if not user:
            # 用戶不存在,先註冊本站新用戶
            user = User.objects.create_user(
                username=validated_data['mobile'],
                password=validated_data['password'],
                mobile=validated_data['mobile'],
            )
        # 新老用戶都綁定QQ的openid
        OAuthQQUser.objects.create(
            openid=validated_data['openid'],
            user=user
        )
        return user
相關文章
相關標籤/搜索