上一篇文章 手把手教會你小程序登錄鑒權 介紹了小程序如何進行登錄鑒權,那么一般小程序的用戶標識可以使用上文所述微信提供的 jscode2session 接口來換取,小程序還提供了一個 getUserInfo 的API來獲取用戶數(shù)據(jù),這個用戶數(shù)據(jù)里面也可以包含當前的用戶標識openid。本文就 如何獲取小程序中的用戶數(shù)據(jù)及數(shù)據(jù)完整性校驗 等內(nèi)容來展開詳述
API介紹
wx.getUserInfo 是用來獲取用戶信息的API接口,下面是對應的參數(shù)字段:
lang 指定返回用戶信息的語言,有三個值:lang
zh_CN 簡體中文
zh_TW 繁體中文
en 英文,默認為en
timeout
timeout 指定API調(diào)用的超時時間, getUserInfo API其實底層也是客戶端發(fā)起一個http請求,來獲取到用戶的相關數(shù)據(jù),經(jīng)過封裝后返回給小程序端,后面會給大家詳細介紹。
withCredentials
withCredentials 這個字段是一個布爾類型的值,決定了在調(diào)用API時小程序返回的數(shù)據(jù)里是否帶上登錄態(tài)信息,不填的話默認該字段的值為 true
那么此時API返回的結果為:
encryptedData 為包括敏感數(shù)據(jù)在內(nèi)的完整用戶信息的加密數(shù)據(jù),敏感數(shù)據(jù)涉及到了用戶的 openid 及 unionid 等。那么數(shù)據(jù)加密采用的算法為 AES-128-CBC 分組對稱加解密算法,后面我們對這個加密算法進行詳細分析。如果該字段的值為 false ,就不會返回上面這兩個字段: encryptedData , iv 。
iv 為上述解密算法的算法初始向量。同樣我們在后面會詳細介紹。
rawData 為一個對象字符串,里面包含了用戶的一些開放數(shù)據(jù),分別是: nickName(微信昵稱) 、 province(所屬省份) 、 language(微信客戶端內(nèi)設置的語言類型) 、 gender(用戶性別) 、 country(所在國家) 、 city(所在城市) 、 avatarUrl(微信頭像地址)
signature 為了保證數(shù)據(jù)的有效性和安全性,小程序對明文數(shù)據(jù)進行了簽名。這個值是 sha1(rawData + session_key) 計算后的值, sha1 則是一種密碼的哈希函數(shù),相比于 md5 哈希函數(shù)來說抗攻擊性更強。
userInfo 字段是一個對象,也是用戶開放數(shù)據(jù),和rawData展示的內(nèi)容一致,只不過rawData將對象序列化為字符串作為返回值。
API之http請求
前面給大家講到在客戶端內(nèi)調(diào)用 getUserInfo API時,微信客戶端會向微信服務端發(fā)送一條請求,在微信開發(fā)者工具里通過 http請求抓包可以看到,發(fā)出了一條 https://servicewechat.com/wxa-dev-logic/jsoperatewxdata 這樣的http請求。
請求體里攜帶了幾個重要的參數(shù),包括 data , grant_type 等,data字段是一個JSON字符串,里面有一個字段 api_name ,其值為'webapi_userinfo'。而grant_type字段也對應了一個值“webapi_userinfo”。
響應體返回了一個JSON對象,首先是一個 baseresponse 字段,里面包含了接口調(diào)用的返回碼 errcode 和調(diào)用結果 errmsg 。該對象還返回了一個 data 字段,這個data字段對應了一個JSON字符串,里面就是通過調(diào)用API拿到的所有用戶數(shù)據(jù)信息。在開發(fā)者工具內(nèi),我們還可以看到返回了一個 debug_info 字段,這個里面同樣包含了用戶的數(shù)據(jù) data ,只不過這里的 data 還返回了用戶的openid,同時還返回了用戶的session_key登錄態(tài)憑據(jù)。
一般我們可以在開發(fā)者工具內(nèi)通過抓包,來調(diào)試一些信息的有效性,包括用戶的 session_key和 openid 。
AES-128-CBC 加密算法
上面我們說過,在小程序里通過API獲取到的用戶完整信息 encryptedData ,是需要通過 AES-128-CBC 算法來加解密的。首先我們先來了解什么是 AES-128-CBC :
AES 全稱為 Advanced Encryption Standard,是美國國家標準與技術研究院(NIST)在2001年建立了電子數(shù)據(jù)的加密規(guī)范,它是一種分組加密標準,每個加密塊大小為128位,允許的密鑰長度為128、192和256位。
分組加密有五種模式,分別是
ECB(Electronic Codebook Book) 電碼本模式
CBC(Cipher Block Chaining) 密碼分組鏈接模式
CTR(Counter) 計算器模式
CFB(Cipher FeedBack) 密碼反饋模式
OFB(Output FeedBack) 輸出反饋模式
這里我們主要來看 AES-128-CBC 的分組加密算法,即用同一組key進行明文和密文的轉換,以128bit為一組,128bit也就是16byte,那么明文的每16字節(jié)為一組就對應了加密后的16字節(jié)的密文。如果最后剩余的明文不夠16字節(jié)時,就需要進行填充了,通常會采用 PKCS#7 (PKCS#5僅支持填充8字節(jié)的數(shù)據(jù)塊,而PKCS#7支持1-255之間的字節(jié)塊)來進行填充。
如果最后剩余的明文為13個字節(jié),也就是缺少了3個字節(jié)才能為一組,那么這個時候就需要填充3個字節(jié)的0x03:
明文數(shù)據(jù): 05 05 05 05 05 05 05 05 05 05 05 05 05
PKCS#7填充: 05 05 05 05 05 05 05 05 05 05 05 05 05 03 03 03
若明文正好是16個字節(jié)的整數(shù)倍,最后要再加入一個16字節(jié)0x10的組再進行加密。
因此,我們發(fā)現(xiàn)PKCS#7填充的兩個特點:
我們再來一起看明文加密的過程,CBC模式對于每個待加密的密碼塊在加密前會先與前一個密碼塊的密文進行異或運算,然后將得到的結果再通過加密器加密,其中第一個密碼塊會與我們前文所述的 iv初始化向量 的數(shù)據(jù)塊進行異或運算。如下圖(圖片來自wiki百科)
但是需要明確說明的是,這里API返回的 iv 是解密算法對應的初始化向量,而非加密算法對應的初始化向量。所以大家肯定也就猜到了,CBC模式解密時第一個密碼塊也是需要和初始化向量進行異或運算的。如下圖(圖片來自wiki百科):
在小程序里,這里加密和解密的密碼器為我們上一篇文章所獲取到的經(jīng)過base64編碼的 session_key 。
小程序中的應用
那么在前面我們大致了解了小程序中是如何對用戶數(shù)據(jù)進行加密的之后,我們就一起以nodejs為例來看看如何在服務端對用戶數(shù)據(jù)進行解密,以及解密后的數(shù)據(jù)完整性校驗:
在util.js文件中,定義了兩個方法:
decryptByAES 方法是利用服務端在登錄時通過微信提供的 jscode2session 接口拿到的 session_key 和調(diào)用wx.getUserInfo后將返回的 iv 初始化向量來解密 encryptedData 。
encryptedBySha1 方法是通過 sha1 哈希算法來加密 session_key 生成小程序應用自身的用戶登錄態(tài)標識,保證 session_key 的安全性。
// util.js
const crypto = require('crypto');
module.exports = {
decryptByAES: function (encrypted, key, iv) {
encrypted = new Buffer(encrypted, 'base64');
key = new Buffer(key, 'base64');
iv = new Buffer(iv, 'base64');
const decipher = crypto.createDecipheriv('aes-128-cbc', key, iv)
let decrypted = decipher.update(encrypted, 'base64', 'utf8')
decrypted += decipher.final('utf8');
return decrypted
},
encryptBySha1: function (data) {
return crypto.createHash('sha1').update(data, 'utf8').digest('hex')
}
};
在auth.js文件中,調(diào)用了上篇文章里的 getSessionKey 方法,獲取用戶的 openid 和 session_key ,拿到這兩者后,對加密的用戶數(shù)據(jù)進行解密操作,同時將解密后的用戶數(shù)據(jù)及用戶的session_key和skey存入數(shù)據(jù)表中。
這里需要注意到一點:如果當前小程序綁定了開放平臺的移動應用或網(wǎng)站應用,或公眾平臺的公眾號等,那么 encryptedData 還會多返回一個 unionId 的字段,這個unionId可在小程序和其他已綁定的平臺之間區(qū)分用戶的唯一性,也就是說 同一用戶,對同一個微信開放平臺下的不同應用,unionid是相同的 。一般,我們可以用unionId來打通小程序和其他應用之間的用戶登錄態(tài)。
// auth.js
const { decryptByAES, encryptBySha1 } = require('../util');
return getSessionKey(code, appid, secret)
.then(resData => {
// 選擇加密算法生成自己的登錄態(tài)標識
const { session_key } = resData;
const skey = encryptBySha1(session_key);
let decryptedData = JSON.parse(decryptByAES(encryptedData, session_key, iv));
// 存入用戶數(shù)據(jù)表中
return saveUserInfo({
userInfo: decryptedData,
session_key,
skey
})
})
.catch(err => {
return {
result: -10003,
errmsg: JSON.stringify(err)
}
})
校驗數(shù)據(jù)完整性和有效性
當我們通過解密拿到用戶的完整數(shù)據(jù)后,可以對拿到的數(shù)據(jù)進行數(shù)據(jù)的完整性和有效性校驗,防止用戶數(shù)據(jù)被惡意篡改。這里說明如何進行相關的數(shù)據(jù)校驗:
有效性校驗:在前面我們介紹到,當 withCredentials 設置為true時,返回的數(shù)據(jù)還會帶上一個 signature 的字段,其值是 sha1(rawData + session_key) 的結果,開發(fā)者可以將所拿到的signature,在自己服務端使用相同的sha1算法算出對應的signature2,即
signature2 = encryptedBySha1(rawData + session_key);
通過對比signature與signature2是否一致,來確定用戶數(shù)據(jù)的完整性。
完整性校驗:在前面拿到的 encryptedData 并進行相關解密操作后,會看到用戶數(shù)據(jù)的object對象里存在一個 watermark 的字段,官方稱之為數(shù)據(jù)水印,這個字段結構為:
"watermark": {
"appid":"APPID",
"timestamp":TIMESTAMP
}
這里開發(fā)同學可以校驗watermark內(nèi)的appid和自身appid是否一致,以及watermark內(nèi)的數(shù)據(jù)獲取的timestamp時間戳,來校驗數(shù)據(jù)的時效性。