Django_Restful Framework之QQ登陸API實現(二)

 

  上篇已經介紹了QQ第三方登陸的流程分析和模型類的建立,而且也知道了再整個過程當中咱們須要提供哪些API爲前端提供數據。前端

1、提供用戶登陸URL的API實現

  在上篇咱們已經分析了當用戶點擊QQ登陸按鈕時,後端須要爲前端提供進行QQ登陸的URL,可能許多人會疑惑爲何不直接由前端處理URL,直接是由該URL進行進行如QQ登入界面?redis

  這是因爲咱們須要根據QQ開發者文檔提供相應的地址,和查詢字符串等。數據庫

  而因爲第三方登陸在其餘的項目中,可能也會使用到第三方登陸(QQ登陸),因此咱們須要考慮解耦的性能,這裏咱們定義了一個utils.py,用來存放 獲取QQ登陸URL、獲取受權證書sccess_token、獲取QQ用戶的openid、以及生成綁定用戶的token、和檢測綁定用戶的token。其代碼以下:django

  QQ登陸的輔助工具類json

 

from django.conf import settings
import urllib.parse
from urllib.request import urlopen
import logging
import json
from itsdangerous import TimedJSONWebSignatureSerializer as TJWSSerializer, BadData

from . import constants
from .exceptions import OAuthQQAPIError

logger = logging.getLogger('django')

class OAuthQQ(object):
    """
    QQ認證輔助工具類
    """
    def __init__(self, client_id=None, client_secret=None, redirect_uri=None, state=None):
        self.client_id = client_id if client_id else settings.QQ_CLIENT_ID
        self.redirect_uri = redirect_uri if redirect_uri else settings.QQ_REDIRECT_URI
        # self.state = state if state else settings.QQ_STATE
        self.state = state or settings.QQ_STATE
        self.client_secret = client_secret if client_secret else settings.QQ_CLIENT_SECRET

    def get_login_url(self):
        url = 'https://graph.qq.com/oauth2.0/authorize?'
        params = {
            'response_type': 'code',
            'client_id': self.client_id,
            'redirect_uri': self.redirect_uri,
            'state': self.state
        }

        url += urllib.parse.urlencode(params)
        return url

    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]

    def get_openid(self, access_token):
        '''根據受權證書去獲取用戶的openid'''
        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"} )\n;

            # 解析
            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

    def generate_bind_user_access_token(self, openid):
        '''根據openid生成進入綁定用戶頁面的token'''
        serializer = TJWSSerializer(settings.SECRET_KEY, constants.BIND_USER_ACCESS_TOKEN_EXPIRES)
        token = serializer.dumps({'openid': openid})
        return token.decode()

    @staticmethod
    def check_bind_user_access_token(access_token):
        '''檢測用戶攜帶進行綁定操做的token'''
        serializer = TJWSSerializer(settings.SECRET_KEY, constants.BIND_USER_ACCESS_TOKEN_EXPIRES)
        try:
            data = serializer.loads(access_token)
        except BadData:
            return None
        else:
            return data['openid']

    
View Code

  視圖後端

#  url(r'^qq/authorization/$', views.QQAuthURLView.as_view()),
class QQAuthURLView(APIView):
    """
    獲取QQ登陸的url    ?next=xxx
    """
    def get(self, request):
        # 獲取next參數
        next = request.query_params.get("next")

        # 拼接QQ登陸的網址
        oauth_qq = OAuthQQ(state=next)
        login_url = oauth_qq.get_login_url()

        # 返回
        return Response({'login_url': login_url})

  這裏,經過直接獲取到進行QQ登陸的URL,直接返回給前端。api

 

2、QQ登陸的回調處理的API實現

  從上篇的QQ登陸流程分析,咱們能夠知道當用戶進入QQ登入頁面時,進行了QQ登入認證,隨後QQ服務器會根據咱們提供的回調地址將頁面重定向到該頁面,而在進入該頁面時,咱們須要進行一下驗證:服務器

  1. 用戶是否第一次進行QQ登入,便是否在數據庫中與本項目的帳號進行了綁定。
  2. 若進行了綁定,咱們直接讓其從哪裏來回哪裏去,即重定向到其進入QQ登入以前頁面的HTML頁面中,即在state中保存的URI中。
  3. 若沒有進行綁定,則爲其生成access_token,跳轉到與本項目的帳號進行綁定的頁面。

故其業務邏輯代碼以下:ide

  視圖:請求方式 : GET /oauth/qq/user/?code=xxx工具

class QQAuthUserView(APIView):
    """
    QQ登陸的用戶  ?code=xxxx
    """
    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_qq = OAuthQQ()
        try:
            # 憑藉code 獲取access_token
            access_token = oauth_qq.get_access_token(code)   # 上述的utils.py輔助工具類中方法

            # 憑藉access_token獲取 openid
            openid = oauth_qq.get_openid(access_token)
        except OAuthQQAPIError:
            return Response({'message': '訪問QQ接口異常'}, status=status.HTTP_503_SERVICE_UNAVAILABLE)

        # 根據openid查詢數據庫OAuthQQUser  判斷數據是否存在
        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:
            # 若是數據存在,表示用戶已經綁定過身份, 簽發JWT token
            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
            })

  注:這裏返回給前端生成的access_token,是經過Django的一個擴展類實現的 ---》

 pip install itsdangerous

  至於爲何要生成 token,而不是直接返回openid?

主要是因爲:

  當用戶進行綁定時,若攜帶的openid過來進行綁定,咱們沒法知道用戶攜帶的該openid是否爲其以前進行QQ登陸時,後端向QQ服務器獲取的openid,若用戶進行篡改爲別的QQ用戶的openid,那麼咱們進行綁定時,便會將別的QQ用戶的openid與本項目的帳號進行了綁定。

  那麼有沒有解決方法呢?就是利用itsdangerous包生成的jwt_token,這種生成的token分爲三個部分:header、payload(存放用戶的某些信息)、以及signature,而signature是由前兩個者配合本項目的secret_key進行生成的,故當用戶拿到token來進行驗證時,服務器這邊會將token的header和payload取出而且再配合 secret_key生成 signature②,與用戶攜帶過來的signature進行對對,這樣用戶若修改了token咱們也能夠發現。

 

3、綁定用戶身份接口

  業務邏輯分析:

  • 用戶須要填寫手機號、密碼、圖片驗證碼、短信驗證碼、而且攜帶token
  • 若是用戶未在本項目註冊過,則會將手機號做爲用戶名爲用戶建立一個本項目的帳戶,並綁定用戶
  • 若是用戶已在本項目註冊過,則檢驗密碼後直接綁定用戶

  其流程圖以下:

  其代碼邏輯以下:

class QQAuthUserView(CreateAPIView):
    """
    QQ登陸的用戶  ?code=xxxx
    """
    serializer_class = 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)

        oauth_qq = OAuthQQ()
        try:
            # 憑藉code 獲取access_token
            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查詢數據庫OAuthQQUser  判斷數據是否存在
        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:
            # 若是數據存在,表示用戶已經綁定過身份, 簽發JWT token
            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
            })

  咱們本能夠定義一個視圖post去實現,可是這裏咱們修改了上述的繼承類 APIView 爲 CreateAPIView,關於該類便再也不詳細的解析,它多繼承了 CreateModelMixin 、GenericAPIView,只須要定義一個指明序列化器類便可,

serializer_class = OAuthQQUserSerializer

  而其內部提供了視圖方法 -- post方法,這裏再也不復述。隨後定義一個序列化器。

from django_redis import get_redis_connection
from rest_framework import serializers
from rest_framework_jwt.settings import api_settings

from users.models import User
from .utils import OAuthQQ
from .models import OAuthQQUser


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 real_sms_code.decode() != sms_code:
            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.get('user')
        mobile = validated_data['mobile']
        password = validated_data['password']

        # 判斷用戶是否存在
        if not user:
            user = User.objects.create_user(username=mobile, mobile=mobile, password=password)

        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

 

  

  關於QQ第三方登陸便介紹到此,關鍵不是代碼,而是業務邏輯思惟~~~~~~

相關文章
相關標籤/搜索