小程式中神祕的使用者資料

騰訊IVWEB團隊發表於2018-04-26

前面

上一篇文章手把手教會你小程式登入鑑權介紹了小程式如何進行登入鑑權,那麼一般小程式的使用者標識可以使用上文所述微信提供的jscode2session介面來換取,小程式還提供了一個getUserInfo的API來獲取使用者資料,這個使用者資料裡面也可以包含當前的使用者標識openid。本文就如何獲取小程式中的使用者資料及資料完整性校驗等內容來展開詳述

API介紹

wx.getUserInfo是用來獲取使用者資訊的API介面,下面是對應的引數欄位:

欄位 型別 是否必填
withCredentials Boolean
lang String
timeout Number
success Function
fail Function
complete Function

lang

lang 指定返回使用者資訊的語言,有三個值:

  • zh_CN 簡體中文
  • zh_TW 繁體中文
  • en 英文,預設為en

timeout

timeout 指定API呼叫的超時時間, getUserInfoAPI其實底層也是客戶端發起一個http請求,來獲取到使用者的相關資料,經過封裝後返回給小程式端,後面會給大家詳細介紹。

withCredentials

withCredentials 這個欄位是一個布林型別的值,決定了在呼叫API時小程式返回的資料裡是否帶上登入態資訊,不填的話預設該欄位的值為true

那麼此時API返回的結果為:

欄位 型別 描述
encryptedData String 加密後的使用者資料
iv String 解密演算法向量
rawData String 使用者開放資料
signature String 簽名
userInfo Object 使用者開放資料

如果該欄位的值為false,就不會返回上面這兩個欄位:encryptedData, iv

  • encryptedData 為包括敏感資料在內的完整使用者資訊的加密資料,敏感資料涉及到了使用者的openidunionid等。那麼資料加密採用的演算法為AES-128-CBC分組對稱加解密演算法,後面我們對這個加密演算法進行詳細分析。

  • iv 為上述解密演算法的演算法初始向量。同樣我們在後面會詳細介紹。

  • rawData 為一個物件字串,裡面包含了使用者的一些開放資料,分別是:nickName(微信暱稱)province(所屬省份)language(微信客戶端內設定的語言型別)gender(使用者性別)country(所在國家)city(所在城市)avatarUrl(微信頭像地址)

  • signature 為了保證資料的有效性和安全性,小程式對明文資料進行了簽名。這個值是sha1(rawData + session_key)計算後的值,sha1則是一種密碼的雜湊函式,相比於md5雜湊函式來說抗攻擊性更強。

  • userInfo 欄位是一個物件,也是使用者開放資料,和rawData展示的內容一致,只不過rawData將物件序列化為字串作為返回值。

API之http請求

前面給大家講到在客戶端內呼叫getUserInfoAPI時,微信客戶端會向微信服務端傳送一條請求,在微信開發者工具裡通過 http請求抓包可以看到,發出了一條https://servicewechat.com/wxa-dev-logic/jsoperatewxdata這樣的http請求。

請求體裡攜帶了幾個重要的引數,包括data, grant_type等,data欄位是一個JSON字串,裡面有一個欄位api_name,其值為'webapi_userinfo'。而grant_type欄位也對應了一個值“webapi_userinfo”。

響應體返回了一個JSON物件,首先是一個baseresponse欄位,裡面包含了介面呼叫的返回碼errcode和呼叫結果errmsg。該物件還返回了一個data欄位,這個data欄位對應了一個JSON字串,裡面就是通過呼叫API拿到的所有使用者資料資訊。在開發者工具內,我們還可以看到返回了一個debug_info欄位,這個裡面同樣包含了使用者的資料data,只不過這裡的data還返回了使用者的openid,同時還返回了使用者的session_key登入態憑據。

一般我們可以在開發者工具內通過抓包,來除錯一些資訊的有效性,包括使用者的session_keyopenid

AES-128-CBC 加密演算法

上面我們說過,在小程式裡通過API獲取到的使用者完整資訊encryptedData,是需要通過AES-128-CBC演算法來加解密的。首先我們先來了解什麼是AES-128-CBC

AES 全稱為 Advanced Encryption Standard,是美國國家標準與技術研究院(NIST)在2001年建立了電子資料的加密規範,它是一種分組加密標準,每個加密塊大小為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位元組為一組就對應了加密後的16位元組的密文。如果最後剩餘的明文不夠16位元組時,就需要進行填充了,通常會採用PKCS#7(PKCS#5僅支援填充8位元組的資料塊,而PKCS#7支援1-255之間的位元組塊)來進行填充。

如果最後剩餘的明文為13個位元組,也就是缺少了3個位元組才能為一組,那麼這個時候就需要填充3個位元組的0x03:

明文資料:   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個位元組的整數倍,最後要再加入一個16位元組0x10的組再進行加密。

因此,我們發現PKCS#7填充的兩個特點:

  • 填充的位元組都是一個相同的位元組

  • 該位元組的值,就是要填充的位元組的個數

我們再來一起看明文加密的過程,CBC模式對於每個待加密的密碼塊在加密前會先與前一個密碼塊的密文進行異或運算,然後將得到的結果再通過加密器加密,其中第一個密碼塊會與我們前文所述的iv初始化向量的資料塊進行異或運算。如下圖(圖片來自wiki百科):

1

但是需要明確說明的是,這裡API返回的iv是解密演算法對應的初始化向量,而非加密演算法對應的初始化向量。所以大家肯定也就猜到了,CBC模式解密時第一個密碼塊也是需要和初始化向量進行異或運算的。如下圖(圖片來自wiki百科):

2

在小程式裡,這裡加密和解密的密碼器為我們上一篇文章所獲取到的經過base64編碼的session_key

小程式中的應用

那麼在前面我們大致瞭解了小程式中是如何對使用者資料進行加密的之後,我們就一起以nodejs為例來看看如何在服務端對使用者資料進行解密,以及解密後的資料完整性校驗:

在util.js檔案中,定義了兩個方法:

decryptByAES方法是利用服務端在登入時通過微信提供的jscode2session介面拿到的session_key和呼叫wx.getUserInfo後將返回的iv初始化向量來解密encryptedData

encryptedBySha1方法是通過sha1雜湊演算法來加密session_key生成小程式應用自身的使用者登入態標識,保證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檔案中,呼叫了上篇文章裡的getSessionKey方法,獲取使用者的openidsession_key,拿到這兩者後,對加密的使用者資料進行解密操作,同時將解密後的使用者資料及使用者的session_key和skey存入資料表中。

這裡需要注意到一點:如果當前小程式繫結了開放平臺的移動應用或網站應用,或公眾平臺的公眾號等,那麼encryptedData還會多返回一個unionId的欄位,這個unionId可在小程式和其他已繫結的平臺之間區分使用者的唯一性,也就是說同一使用者,對同一個微信開放平臺下的不同應用,unionid是相同的。一般,我們可以用unionId來打通小程式和其他應用之間的使用者登入態。

// auth.js
const { decryptByAES, encryptBySha1 } = require('../util');
return getSessionKey(code, appid, secret)
    .then(resData => {
        // 選擇加密演算法生成自己的登入態標識
        const { session_key } = resData;
        const skey = encryptBySha1(session_key);

        let decryptedData = JSON.parse(decryptByAES(encryptedData, session_key, iv));
        // 存入使用者資料表中
        return saveUserInfo({
            userInfo: decryptedData,
            session_key,
            skey
        })
    })
    .catch(err => {
        return {
            result: -10003,
            errmsg: JSON.stringify(err)
        }
    })
複製程式碼

校驗資料完整性和有效性

當我們通過解密拿到使用者的完整資料後,可以對拿到的資料進行資料的完整性和有效性校驗,防止使用者資料被惡意篡改。這裡說明如何進行相關的資料校驗:

有效性校驗:在前面我們介紹到,當withCredentials設定為true時,返回的資料還會帶上一個signature的欄位,其值是sha1(rawData + session_key)的結果,開發者可以將所拿到的signature,在自己服務端使用相同的sha1演算法算出對應的signature2,即

signature2 = encryptedBySha1(rawData + session_key);
複製程式碼

通過對比signature與signature2是否一致,來確定使用者資料的完整性。

完整性校驗:在前面拿到的encryptedData並進行相關解密操作後,會看到使用者資料的object物件裡存在一個watermark的欄位,官方稱之為資料水印,這個欄位結構為:

"watermark": {
    "appid":"APPID",
    "timestamp":TIMESTAMP
}
複製程式碼

這裡開發同學可以校驗watermark內的appid和自身appid是否一致,以及watermark內的資料獲取的timestamp時間戳,來校驗資料的時效性。

最後

那麼上面就是小程式中如何對使用者資料進行加解密操作,以及如何對使用者資料進行相關處理和校驗的介紹,請大家多多指教!

參考文章:

密碼演算法詳解——AES

AES五種加密模式(CBC、ECB、CTR、OCF、CFB)

對加密演算法 AES-128-CBC 的一些理解

高階加密標準AES的工作模式(ECB、CBC、CFB、OFB)


《IVWEB 技術週刊》 震撼上線了,關注公眾號:IVWEB社群,每週定時推送優質文章。

相關文章