美多商場
QQ登入,亦即我們所說的第三方登入,是指使用者可以不在本專案中輸入密碼,而直接通過第三方的驗證,成功登入本專案。
1 使用QQ登入的流程
qq登入注意事項:
使用者掃描登入qq,並不代表就登入了美多商城。
只有在qq賬號與美多賬號繫結之後,使用者掃描登入qq,qq伺服器驗證qq賬號沒問題之後,返給我們一個openid,這時候美多伺服器就可以做一個關聯登入。
建立模型類
建立一個新的應用oauth,用來實現QQ第三方認證登入。總路由字首 oauth/
python ../../manage.py startapp oauth
在meiduo/meiduo_mall/utils/models.py
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: abstract = True # 說明是抽象模型類, 用於繼承使用,資料庫遷移時不會建立BaseModel的表
在oauth/models.py中定義QQ身份(openid)與使用者模型類User的關聯關係
from django.db import models from meiduo_mall.utils.models import BaseModel class OAuthQQUser(BaseModel): """ QQ登入使用者資料 """ user = models.ForeignKey('users.User', on_delete=models.CASCADE, verbose_name='使用者') openid = models.CharField(max_length=64, verbose_name='openid', db_index=True) class Meta: db_table = 'tb_oauth_qq' verbose_name = 'QQ登入使用者資料' verbose_name_plural = verbose_name
進行資料庫遷移
python manage.py makemigrations
python manage.py migrate
問題:這個qq模型類有資料建立時間和修改時間,那麼之前的使用者模型類呢?也有,如下圖:
2 獲取QQ登入網址
處理第一步:點選qq登入之後,要跳轉到掃描登入介面,而我們現在就需要來獲取一下掃描登入介面的地址。
也就是如下圖第一步:(紫色1)
注意:其實這裡的第1步,是獲取這個qq.com?xxxxxx這個url,也就是qq使用者掃描登入的介面url:
- 後端介面設計:
請求方式: GET /oauth/qq/authorization/?next=xxx
請求引數: 查詢字串
引數名 | 型別 | 是否必須 | 說明 |
---|---|---|---|
next | str | 否 | 使用者QQ登入成功後進入美多商城的哪個網址 |
返回資料: JSON
{
"login_url": "https://graph.qq.com/oauth2.0/show?which=Login&display=pc&response_type=code&client_id=101474184&redirect_uri=http%3A%2F%2Fwww.meiduo.site%3A8080%2Foauth_callback.html&state=%2F&scope=get_user_info"
}
返回值 | 型別 | 是否必須 | 說明 |
---|---|---|---|
login_url | str | 是 | qq登入網址 |
- 在配置檔案中新增關於QQ登入的應用開發資訊
# QQ登入引數
QQ_CLIENT_ID = '101474184'
QQ_CLIENT_SECRET = 'c6ce949e04e12ecc909ae6a8b09b637c'
QQ_REDIRECT_URI = 'http://www.meiduo.site:8080/oauth_callback.html'
QQ_STATE = '/'
- 新建oauth/utils.py檔案,建立QQ登入輔助工具類
先來看一下qq登入的介面文件,關於第一步的介紹,獲取認證code:
這裡是獲取Access_token,根據Authorization_code獲取Access_token,但是要先獲取Authorization Code。
client_id如下:
redirect_url如下:
from urllib.parse import urlencode
from django.conf import settings
import logging
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 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 # 用於儲存登入成功後的跳轉頁面路徑
def get_qq_login_url(self):
"""
獲取qq登入的網址
:return: url網址
"""
params = {
'response_type': 'code',
'client_id': self.client_id,
'redirect_uri': self.redirect_uri,
'state': self.state,
'scope': 'get_user_info',
}
url = 'https://graph.qq.com/oauth2.0/authorize?' + urlencode(params)
return url
知識點補充:urllib使用說明
在後端介面中,我們需要向QQ伺服器傳送請求,查詢使用者的QQ資訊,Python提供了標準模組urllib可以幫助我們傳送http請求。
urllib.parse.urlencode(query)
將query字典轉換為url路徑中的查詢字串
urllib.parse.parse_qs(qs)
將qs查詢字串格式資料轉換為python的字典
urllib.request.urlopen(url, data=None)
傳送http請求,如果data為None,傳送GET請求,如果data不為None,傳送POST請求
返回response響應物件,可以通過read()讀取響應體資料,需要注意讀取出的響應體資料為bytes型別
問題:第一步為啥是獲取認證code?
第一步不是要生成一個url麼?
其實這個url,就是用來向qq伺服器獲取認證code的。所以沒錯
- 在oauth/views.py中實現檢視
# url(r'^qq/authorization/$', views.QQAuthURLView.as_view()),
class QQAuthURLView(APIView):
"""
獲取QQ登入的url
"""
def get(self, request):
"""
提供用於qq登入的url
"""
next = request.query_params.get('next')
oauth = OAuthQQ(state=next)
login_url = oauth.get_qq_login_url()
return Response({'login_url': login_url})
3 QQ登入回撥處理
3.1 測試
qq登入介面已經搞定,接下來我們測試一下:
掃描登入:
登入成功之後,按理說應該到如下介面:
但是咱們的報錯了:
使用者在QQ登入成功後,QQ會將使用者重定向回我們配置的回撥callback網址,在本專案中,我們申請QQ登入開發資質時配置的回撥地址為:
http://www.meiduo.site:8080/oauth_callback.html
但是咱們還沒有oauth_callback.html介面
我們在front_end_pc目錄中新建oauth_callback.html檔案,用於接收QQ登入成功的使用者回撥請求。在該頁面中,提供了用於使用者首次使用QQ登入時需要繫結使用者身份的表單資訊。
<!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>
在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();
}
}
});
在QQ將使用者重定向到此網頁的時候,重定向的網址會攜帶QQ提供的code引數,用於獲取使用者資訊使用,我們需要將這個code引數傳送給後端,在後端中使用code引數向QQ請求使用者的身份資訊,並查詢與該QQ使用者繫結的使用者。
重新測試,如下:
在這裡介面的url中是包含code和state的。
接下來,我們就可以處理第二步了(紫色2)
也就是根據code獲取access_token
3.2 後端介面設計
請求方式 : GET /oauth/qq/user/?code=xxx
請求引數: 查詢字串引數
引數 | 型別 | 是否必傳 | 說明 |
---|---|---|---|
code | str | 是 | qq返回的授權憑證code |
返回資料: JSON
{
"access_token": xxxx,
}
或
{
"token": "xxx",
"username": "python",
"user_id": 1
}
返回值 | 型別 | 是否必須 | 說明 |
---|---|---|---|
access_token | str | 否 | 使用者是第一次使用QQ登入時返回,其中包含openid,用於繫結身份使用,注意這個是我們自己生成的 |
token | str | 否 | 使用者不是第一次使用QQ登入時返回,登入成功的JWT token |
username | str | 否 | 使用者不是第一次使用QQ登入時返回,使用者名稱 |
user_id | int | 否 | 使用者不是第一次使用QQ登入時返回,使用者id |
注意:這個access_token是自己生成的
為啥呢要自己生成access_token呢?
首先返回這個access_token是在未繫結的時候,顯示如下介面的時候,返回的:
在這個介面是需要openid的(因為點選儲存時,後臺需要拿著使用者手機號與openid進行繫結),而我們返回的access_token中包含openid。這個access_token與qq伺服器返回的不一樣,這個是我們拿著qq伺服器返回的openid做了一個處理,避免前端拿到openid修改。因為如果直接將openid給前端,那麼前端是可以對openid進行修改的。本來openid是A使用者的,如果將openid修改為B使用者的openid,那麼點選儲存的時候,我們就將A使用者的美多賬號與B使用者的openid進行了繫結。所以避免這種事情的發生,我們就對openid進行一個處理,如果前端修改,在繫結的時候,我們後端可以知道修改了。
使用itsdangerous生成憑據access_token
安裝
pip install itsdangerous
TimedJsonWebSignatureSerializer的用法與Json的用法類似:
json
dict -> json str
json.dumps()
json str -> dict
json.loads()
使用TimedJSONWebSignatureSerializer可以生成帶有有效期的token
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
from django.conf import settings
# serializer = Serializer(祕鑰, 有效期秒)
serializer = Serializer(settings.SECRET_KEY, 300)
# serializer.dumps(資料), 返回bytes型別
token = serializer.dumps({'mobile': '18512345678'})
token = token.decode()
# 檢驗token
# 驗證失敗,會丟擲itsdangerous.BadData異常
serializer = Serializer(settings.SECRET_KEY, 300)
try:
data = serializer.loads(token)
except BadData:
return None
3.3 獲取access_token
介面介紹
先來看一下qq官方文件如何定義此介面的:
還是在這個介面,看第二步驟即可。
3.4 後端實現
檢視邏輯分析如下:
class QQAuthUserView(APIView):
"""
QQ登入的使用者 ?code=xxxx
"""
def get(self ) :
# 獲取code
# 憑藉code 獲取access_ token
# 憑藉access_token獲取openid
# 根據openid查詢資料庫0AuthoQuser 判斷資料是否存在
# 如果資料存在,表示使用者已經繫結過身份, 簽發JWT token
# 如果資料不存在,處理openid並返回
然後補充如下程式碼:
def get(self, request):
"""
獲取qq登入的使用者資料
"""
# 獲取code
code = request.query_params.get('code')
if not code:
return Response({'message': '缺少code'}, status=status.HTTP_400_BAD_REQUEST)
# 憑藉code 獲取access_ token 獲取使用者openid
oauth = OAuthQQ()
try:
access_token = oauth.get_access_token(code)
openid = oauth.get_openid(access_token)
except QQAPIError:
return Response({'message': 'QQ服務異常'}, status=status.HTTP_503_SERVICE_UNAVAILABLE)
# 根據openid查詢資料庫0AuthoQuser 判斷資料是否存在
# 如果資料存在,表示使用者已經繫結過身份, 簽發JWT token
# 如果資料不存在,處理openid並返回
這裡呼叫了get_access_token方法,此方法程式碼如下:
def get_access_token(self, code):
"""
獲取access_token
:param code: qq提供的code
:return: 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?' + urlencode(params)
try:
# 傳送請求
response = urlopen(url)
# 讀取響應體資料
response_data = response.read() # bytes
response_data = response.decode() # str
# 解析 access_token
data = parse_qs(response_data)
except Exception as e:
logger.error("獲取access_token異常 %s" % e)
raise OAuthQQAPIError
else:
access_token = data.get('access_token', None)
return access_token
此方法中用到的client_secret屬性如下:
呼叫了settings配置檔案中的常量如下:
還丟擲了一個自定義異常,此異常程式碼如下:
還用到日誌logger:
3.5 獲取openid實現
接下來處理第三步,獲取openid。
3.5.1 獲取openid介面介紹
首先先來看qq官方介面:
3.5.2 具體操作
檢視邏輯程式碼如下:
class QQAuthUserView(APIView):
"""
QQ登入的使用者
"""
def get(self, request):
"""
獲取qq登入的使用者資料
"""
code = request.query_params.get('code')
if not code:
return Response({'message': '缺少code'}, status=status.HTTP_400_BAD_REQUEST)
oauth = OAuthQQ()
# 獲取使用者openid
try:
access_token = oauth.get_access_token(code)
openid = oauth.get_openid(access_token)
except OAuthQQAPIError:
return Response({'message': 'QQ服務異常'}, status=status.HTTP_503_SERVICE_UNAVAILABLE)
# 判斷使用者是否存在
try:
qq_user = OAuthQQUser.objects.get(openid=openid)
except OAuthQQUser.DoesNotExist:
# 使用者第一次使用QQ登入
token = oauth.generate_save_user_token(openid)
return Response({'access_token': token})
else:
# 找到使用者, 生成token
user = qq_user.user
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)
response = Response({
'token': token,
'user_id': user.id,
'username': user.username
})
return response
呼叫的generate_bind_user_access_token如下:
@staticmethod
def generate_save_user_token(openid):
"""
生成儲存使用者資料的token
:param openid: 使用者的openid
:return: token
"""
serializer = Serializer(settings.SECRET_KEY, expires_in=constants.SAVE_QQ_USER_TOKEN_EXPIRES)
data = {'openid': openid}
token = serializer.dumps(data)
return token.decode()
用到的常量:
18_獲取openid前端實現與測試 p231
在OAuthQQ輔助類中新增方法:
def get_openid(self, access_token):
"""
獲取使用者的openid
:param access_token: qq提供的access_token
:return: open_id
"""
url = 'https://graph.qq.com/oauth2.0/me?access_token=' + access_token
response = urlopen(url)
response_data = response.read().decode()
try:
# 返回的資料 callback( {"client_id":"YOUR_APPID","openid":"YOUR_OPENID"} )\n;
data = json.loads(response_data[10:-4])
except Exception:
data = parse_qs(response_data)
logger.error('code=%s msg=%s' % (data.get('code'), data.get('msg')))
raise QQAPIError
openid = data.get('openid', None)
return openid
@staticmethod
def generate_save_user_token(openid):
"""
生成儲存使用者資料的token
:param openid: 使用者的openid
:return: token
"""
serializer = Serializer(settings.SECRET_KEY, expires_in=constants.SAVE_QQ_USER_TOKEN_EXPIRES)
data = {'openid': openid}
token = serializer.dumps(data)
return token.decode()
在oauth/views.py中實現檢視
class QQAuthUserView(APIView):
"""
QQ登入的使用者
"""
def get(self, request):
"""
獲取qq登入的使用者資料
"""
code = request.query_params.get('code')
if not code:
return Response({'message': '缺少code'}, status=status.HTTP_400_BAD_REQUEST)
oauth = OAuthQQ()
# 獲取使用者openid
try:
access_token = oauth.get_access_token(code)
openid = oauth.get_openid(access_token)
except QQAPIError:
return Response({'message': 'QQ服務異常'}, status=status.HTTP_503_SERVICE_UNAVAILABLE)
# 判斷使用者是否存在
try:
qq_user = OAuthQQUser.objects.get(openid=openid)
except OAuthQQUser.DoesNotExist:
# 使用者第一次使用QQ登入
token = oauth.generate_save_user_token(openid)
return Response({'access_token': token})
else:
# 找到使用者, 生成token
user = qq_user.user
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)
response = Response({
'token': token,
'user_id': user.id,
'username': user.username
})
return response