上篇已經介紹了QQ第三方登陸的流程分析和模型類的建立,而且也知道了再整個過程當中咱們須要提供哪些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']
視圖後端
# 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
從上篇的QQ登陸流程分析,咱們能夠知道當用戶進入QQ登入頁面時,進行了QQ登入認證,隨後QQ服務器會根據咱們提供的回調地址將頁面重定向到該頁面,而在進入該頁面時,咱們須要進行一下驗證:服務器
故其業務邏輯代碼以下: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咱們也能夠發現。
業務邏輯分析:
其流程圖以下:
其代碼邏輯以下:
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第三方登陸便介紹到此,關鍵不是代碼,而是業務邏輯思惟~~~~~~