用戶模塊---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