開發微信小程序時,接入小程序的受權登陸能夠快速實現用戶註冊登陸的步驟,是快速創建用戶體系的重要一步。這篇文章將介紹 python + sanic + 微信小程序實現用戶快速註冊登陸全棧方案。javascript
微信小程序登陸時序圖以下:html
這個流程分爲兩大部分:java
小程序使用 wx.login() API 獲取 code,調用 wx.getUserInfo() API 獲取 encryptedData 和 iv,而後將這三個信息發送給第三方服務器。python
第三方服務器獲取到 code、encryptedData和 iv 後,使用 code 換取 session_key,而後將 session_key 利用 encryptedData 和 iv 解密在服務端獲取用戶信息。根據用戶信息返回 jwt 數據,完成登陸。git
下面咱們先看一下小程序提供的 API。github
在這個受權登陸的過程當中,用到的 API 以下:算法
wx.login數據庫
wx.getUserInfojson
wx.chekSession
是可選的,這裏並無用到。小程序
調用此接口能夠獲取登陸憑證(code),以用來換取用戶登陸態信息,包括用戶的惟一標識(openid) 及本次登陸的 會話密鑰(session_key)。
若是接口調用成功,返回結果以下:
參數名 | 類型 | 說明 |
---|---|---|
errMsg | String | 調用結果 |
code | String | 用戶容許登陸後,回調內容會帶上 code(有效期五分鐘),開發者須要將 code 發送到開發者服務器後臺,使用code 換取 session_key api,將 code 換成 openid 和 session_key |
開發者服務器使用登陸憑證 code 獲取 session_key 和 openid。其中 session_key 是對用戶數據進行加密簽名的密鑰。爲了自身應用安全,session_key 不該該在網絡上傳輸。因此這一步應該在服務器端實現。
此接口用來獲取用戶信息。
當
withCredentials
爲 true 時,要求此前有調用過 wx.login 且登陸態還沒有過時,此時返回的數據會包含 encryptedData, iv 等敏感信息;當 withCredentials 爲 false 時,不要求有登陸態,返回的數據不包含 encryptedData, iv 等敏感信息。
接口success 時返回參數以下:
參數名 | 類型 | 說明 |
---|---|---|
userInfo | OBJECT | 用戶信息對象,不包含 openid 等敏感信息 |
rawData | String | 不包括敏感信息的原始數據字符串,用於計算簽名。 |
signature | String | 使用 sha1( rawData + sessionkey ) 獲得字符串,用於校驗用戶信息,參考文檔 signature。 |
encryptedData | String | 包括敏感數據在內的完整用戶信息的加密數據,詳細見加密數據解密算法 |
iv | String | 加密算法的初始向量,詳細見加密數據解密算法 |
encryptedData
解密後爲如下 json 結構,詳見加密數據解密算法
{ "openId": "OPENID", "nickName": "NICKNAME", "gender": GENDER, "city": "CITY", "province": "PROVINCE", "country": "COUNTRY", "avatarUrl": "AVATARURL", "unionId": "UNIONID", "watermark": { "appid":"APPID", "timestamp":TIMESTAMP } }
因爲解密 encryptedData 須要 session_key 和 iv 因此,在給服務器端發送受權驗證的過程當中須要將 code、encryptedData 和 iv 一塊兒發送。
服務器端受權須要提供兩個 API:
/oauth/token 經過小程序提供的驗證信息獲取服務器本身的 token
/accounts/wxapp 若是登陸用戶是未註冊用戶,使用此接口註冊爲新用戶。
開始受權時,小程序調用此 API 嘗試換取jwt,若是用戶未註冊返回401,若是用戶發送參數錯誤,返回403。
接口 獲取 jwt 成功時返回參數以下:
參數名 | 類型 | 說明 |
---|---|---|
account_id | string | 當前受權用戶的用戶 ID |
access_token | string | jwt(登陸流程中的第三方 session_key |
token_type | string | token 類型(固定Bearer) |
小程序受權後應該先調用此接口,若是結果是用戶未註冊,則應該調用新用戶註冊的接口先註冊新用戶,註冊成功後再調用此接口換取 jwt。
註冊新用戶時,服務器端須要存儲當前用戶的 openid,因此和受權接口同樣,請求時須要的參數爲 code、encryptedData 和 iv。
註冊成功後,將返回用戶的 ID 和註冊時間。此時,應該再次調用獲取 token 的接口去換取第三方 token,以用來下次登陸。
接口定義好以後,來看下先後端總體的受權登陸流程。
這個流程須要注意的是,在 C 步(使用 code 換取 session )以後咱們獲得 session_key,而後須要用 session_key 解密獲得用戶數據。
而後使用 openid 判斷用戶是否已經註冊,若是用戶已經註冊,生成 jwt 返回給小程序。
若是用戶未註冊返回401, 提示用戶未註冊。
jwt(3rd_session)
用於第三方服務器和小程序之間作登陸態校驗,爲了保證安全性,jwt 應該知足:
足夠長。建議有 2^128 組合
避免使用 srand(當前時間),而後 rand() 的方法,而是採用操做系統提供的真正隨機數機制。
設置必定的有效時間,
固然,在小程序中也可使用手機號登陸,不過這是另外一個功能了,就不在這裏敘述了。
說了這麼多,接下來看代碼吧。
代碼邏輯爲:
用戶在小程序受權
小程序將受權消息發送到服務器,服務器檢查用戶是否已經註冊,若是註冊返回 jwt,若是沒註冊提示用戶未註冊,而後小程序從新請求註冊接口,註冊用戶,註冊成功後重復這一步。
爲了簡便,這裏在小程序 啓動的時候就請求受權。代碼實現以下。
//app.js var config = require('./config.js') App({ onLaunch: function() { //調用API從本地緩存中獲取數據 var jwt = wx.getStorageSync('jwt'); var that = this; if (!jwt.access_token){ //檢查 jwt 是否存在 若是不存在調用登陸 that.login(); } else { console.log(jwt.account_id); } }, login: function() { // 登陸部分代碼 var that = this; wx.login({ // 調用 login 獲取 code success: function(res) { var code = res.code; wx.getUserInfo({ // 調用 getUserInfo 獲取 encryptedData 和 iv success: function(res) { // success that.globalData.userInfo = res.userInfo; var encryptedData = res.encryptedData || 'encry'; var iv = res.iv || 'iv'; console.log(config.basic_token); wx.request({ // 發送請求 獲取 jwt url: config.host + '/auth/oauth/token?code=' + code, header: { Authorization: config.basic_token }, data: { username: encryptedData, password: iv, grant_type: "password", auth_approach: 'wxapp', }, method: "POST", success: function(res) { if (res.statusCode === 201) { // 獲得 jwt 後存儲到 storage, wx.showToast({ title: '登陸成功', icon: 'success' }); wx.setStorage({ key: "jwt", data: res.data }); that.globalData.access_token = res.data.access_token; that.globalData.account_id = res.data.sub; } else if (res.statusCode === 401){ // 若是沒有註冊調用註冊接口 that.register(); } else { // 提示錯誤信息 wx.showToast({ title: res.data.text, icon: 'success', duration: 2000 }); } }, fail: function(res) { console.log('request token fail'); } }) }, fail: function() { // fail }, complete: function() { // complete } }) } }) }, register: function() { // 註冊代碼 var that = this; wx.login({ // 調用登陸接口獲取 code success: function(res) { var code = res.code; wx.getUserInfo({ // 調用 getUserInfo 獲取 encryptedData 和 iv success: function(res) { // success that.globalData.userInfo = res.userInfo; var encryptedData = res.encryptedData || 'encry'; var iv = res.iv || 'iv'; console.log(iv); wx.request({ // 請求註冊用戶接口 url: config.host + '/auth/accounts/wxapp', header: { Authorization: config.basic_token }, data: { username: encryptedData, password: iv, code: code, }, method: "POST", success: function(res) { if (res.statusCode === 201) { wx.showToast({ title: '註冊成功', icon: 'success' }); that.login(); } else if (res.statusCode === 400) { wx.showToast({ title: '用戶已註冊', icon: 'success' }); that.login(); } else if (res.statusCode === 403) { wx.showToast({ title: res.data.text, icon: 'success' }); } console.log(res.statusCode); console.log('request token success'); }, fail: function(res) { console.log('request token fail'); } }) }, fail: function() { // fail }, complete: function() { // complete } }) } }) }, get_user_info: function(jwt) { wx.request({ url: config.host + '/auth/accounts/self', header: { Authorization: jwt.token_type + ' ' + jwt.access_token }, method: "GET", success: function (res) { if (res.statusCode === 201) { wx.showToast({ title: '已註冊', icon: 'success' }); } else if (res.statusCode === 401 || res.statusCode === 403) { wx.showToast({ title: '未註冊', icon: 'error' }); } console.log(res.statusCode); console.log('request token success'); }, fail: function (res) { console.log('request token fail'); } }) }, globalData: { userInfo: null } })
服務端使用 sanic
框架 + swagger_py_codegen
生成 rest-api。
數據庫使用 MongoDB,python-weixin
實現了登陸過程當中 code 換取 session_key 以及 encryptedData 解密的功能,因此使用python-weixin 做爲 python 微信 sdk 使用。
爲了過濾無效請求,服務器端要求用戶在獲取 token 或受權時在 header 中帶上
Authorization
信息。Authorization
在登陸前使用的是 Basic 驗證(格式 (Basic hashkey) 注 hashkey爲client_id + client_secret 作BASE64處理),只是用來校驗請求的客戶端是否合法。不過Basic 基本等同於明文,並不能用它來進行嚴格的受權驗證。jwt 原理及使用參見 理解JWT(JSON Web Token)認證及實踐
使用 swagger 生成代碼結構以下:
因爲代碼太長,這裏只放獲取 jwt 的邏輯:
def get_wxapp_userinfo(encrypted_data, iv, code): from weixin.lib.wxcrypt import WXBizDataCrypt from weixin import WXAPPAPI from weixin.oauth2 import OAuth2AuthExchangeError appid = Config.WXAPP_ID secret = Config.WXAPP_SECRET api = WXAPPAPI(appid=appid, app_secret=secret) try: # 使用 code 換取 session key session_info = api.exchange_code_for_session_key(code=code) except OAuth2AuthExchangeError as e: raise Unauthorized(e.code, e.description) session_key = session_info.get('session_key') crypt = WXBizDataCrypt(appid, session_key) # 解密獲得 用戶信息 user_info = crypt.decrypt(encrypted_data, iv) return user_info def verify_wxapp(encrypted_data, iv, code): user_info = get_wxapp_userinfo(encrypted_data, iv, code) # 獲取 openid openid = user_info.get('openId', None) if openid: auth = Account.get_by_wxapp(openid) if not auth: raise Unauthorized('wxapp_not_registered') return auth raise Unauthorized('invalid_wxapp_code') def create_token(request): # verify basic token approach = request.json.get('auth_approach') username = request.json['username'] password = request.json['password'] if approach == 'password': account = verify_password(username, password) elif approach == 'wxapp': account = verify_wxapp(username, password, request.args.get('code')) if not account: return False, {} payload = { "iss": Config.ISS, "iat": int(time.time()), "exp": int(time.time()) + 86400 * 7, "aud": Config.AUDIENCE, "sub": str(account['_id']), "nickname": account['nickname'], "scopes": ['open'] } token = jwt.encode(payload, 'secret', algorithm='HS256') # 因爲 account 中 _id 是一個 object 須要轉化成字符串 return True, {'access_token': token, 'account_id': str(account['_id'])}
具體代碼能夠在 Metis:https://github.com/gusibi/Metis 查看。
Note
: 若是試用代碼,請先設定 oauth2_client,使用本身的配置。不要將私密配置信息提交到 github。
最後,感謝女友支持。
歡迎關注(April_Louisa) | 請我喝芬達 |
---|---|
![]() |
![]() |