開發者後臺校驗與解密開放數據
微信會對這些開放數據做簽名和加密處理。開發者後臺拿到開放數據後可以對數據進行校驗簽名和解密,來保證數據不被篡改。
簽名校驗以及數據加解密涉及用戶的會話密鑰 session_key。 開發者應該事先通過 wx.login 登錄流程獲取會話密鑰 session_key 並保存在服務器。爲了數據不被篡改,開發者不應該把 session_key 傳到小程序客戶端等服務器外的環境。
數據簽名校驗
爲了確保開放接口返回用戶數據的安全性,微信會對明文數據進行簽名。開發者可以根據業務需要對數據包進行簽名校驗,確保數據的完整性。
- 通過調用接口(如 wx.getUserInfo)獲取數據時,接口會同時返回 rawData、signature,其中 signature = sha1( rawData + session_key )
- 開發者將 signature、rawData 發送到開發者服務器進行校驗。服務器利用用戶對應的 session_key 使用相同的算法計算出簽名 signature2 ,比對 signature 與 signature2 即可校驗數據的完整性。
如 wx.getUserInfo的數據校驗:
接口返回的rawData:
加密數據解密算法
接口如果涉及敏感數據(如wx.getUserInfo當中的 openId 和 unionId),接口的明文內容將不包含這些敏感數據。開發者如需要獲取敏感數據,需要對接口返回的加密數據(encryptedData) 進行對稱解密。 解密算法如下:
- 對稱解密使用的算法爲 AES-128-CBC,數據採用PKCS#7填充。
- 對稱解密的目標密文爲 Base64_Decode(encryptedData)。
- 對稱解密祕鑰 aeskey = Base64_Decode(session_key), aeskey 是16字節。
- 對稱解密算法初始向量 爲Base64_Decode(iv),其中iv由數據接口返回。
微信官方提供了多種編程語言的示例代碼。每種語言類型的接口名字均一致。調用方式可以參照示例。
另外,爲了應用能校驗數據的有效性,會在敏感數據加上數據水印( watermark )
watermark參數說明:
參數 | 類型 | 說明 |
---|---|---|
appid | String | 敏感數據歸屬 appId,開發者可校驗此參數與自身 appId 是否一致 |
timestamp | Int | 敏感數據獲取的時間戳, 開發者可以用於數據時效性校驗 |
如接口 wx.getUserInfo 敏感數據當中的 watermark:
{
"openId": "OPENID",
"nickName": "NICKNAME",
"gender": GENDER,
"city": "CITY",
"province": "PROVINCE",
"country": "COUNTRY",
"avatarUrl": "AVATARURL",
"unionId": "UNIONID",
"watermark":
{
"appid":"APPID",
"timestamp":TIMESTAMP
}
}
注:
- 解密後得到的json數據根據需求可能會增加新的字段,舊字段不會改變和刪減,開發者需要預留足夠的空間
會話密鑰 session_key 有效性
開發者如果遇到因爲 session_key 不正確而校驗簽名失敗或解密失敗,請關注下面幾個與 session_key 有關的注意事項。
- wx.login 調用時,用戶的 session_key 可能會被更新而致使舊 session_key 失效(刷新機制存在最短週期,如果同一個用戶短時間內多次調用 wx.login,並非每次調用都導致 session_key 刷新)。開發者應該在明確需要重新登錄時才調用 wx.login,及時通過 auth.code2Session 接口更新服務器存儲的 session_key。
- 微信不會把 session_key 的有效期告知開發者。我們會根據用戶使用小程序的行爲對 session_key 進行續期。用戶越頻繁使用小程序,session_key 有效期越長。
- 開發者在 session_key 失效時,可以通過重新執行登錄流程獲取有效的 session_key。使用接口 wx.checkSession可以校驗 session_key 是否有效,從而避免小程序反覆執行登錄流程。
- 當開發者在實現自定義登錄態時,可以考慮以 session_key 有效期作爲自身登錄態有效期,也可以實現自定義的時效性策略。
首先使用code換取session_key
登錄憑證校驗。通過 wx.login 接口獲得臨時登錄憑證 code 後傳到開發者服務器調用此接口完成登錄流程。
請求地址
GET https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code
請求參數
屬性 | 類型 | 默認值 | 必填 | 說明 |
---|---|---|---|---|
appid | string | 是 | 小程序 appId | |
secret | string | 是 | 小程序 appSecret | |
js_code | string | 是 | 登錄時獲取的 code | |
grant_type | string | 是 | 授權類型,此處只需填寫 authorization_code |
返回值
Object
返回的 JSON 數據包
屬性 | 類型 | 說明 |
---|---|---|
openid | string | 用戶唯一標識 |
session_key | string | 會話密鑰 |
unionid | string | 用戶在開放平臺的唯一標識符,在滿足 UnionID 下發條件的情況下會返回,詳見 UnionID 機制說明。 |
errcode | number | 錯誤碼 |
errmsg | string | 錯誤信息 |
errcode 的合法值
值 | 說明 | 最低版本 |
---|---|---|
-1 | 系統繁忙,此時請開發者稍候再試 | |
0 | 請求成功 | |
40029 | code 無效 | |
45011 | 頻率限制,每個用戶每分鐘100次 |
獲取sessionKey之後就可以解密了
package com.water.elephant.utils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.util.Arrays;
import org.bouncycastle.util.encoders.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.Key;
import java.security.Security;
/**
* 微信工具類
*/
public class WechatUtil {
public static String decryptData(String encryptDataB64, String sessionKeyB64, String ivB64) throws Exception {
return new String(
decryptOfDiyIV(
Base64.decode(encryptDataB64),
Base64.decode(sessionKeyB64),
Base64.decode(ivB64)
)
);
}
private static final String KEY_ALGORITHM = "AES";
private static final String ALGORITHM_STR = "AES/CBC/PKCS7Padding";
private static Key key;
private static Cipher cipher;
private static void init(byte[] keyBytes) {
// 如果密鑰不足16位,那麼就補足. 這個if 中的內容很重要
int base = 16;
if (keyBytes.length % base != 0) {
int groups = keyBytes.length / base + (keyBytes.length % base != 0 ? 1 : 0);
byte[] temp = new byte[groups * base];
Arrays.fill(temp, (byte) 0);
System.arraycopy(keyBytes, 0, temp, 0, keyBytes.length);
keyBytes = temp;
}
// 初始化
Security.addProvider(new BouncyCastleProvider());
// 轉化成JAVA的密鑰格式
key = new SecretKeySpec(keyBytes, KEY_ALGORITHM);
try {
// 初始化cipher
cipher = Cipher.getInstance(ALGORITHM_STR, "BC");
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 解密方法
*
* @param encryptedData 要解密的字符串
* @param keyBytes 解密密鑰
* @param ivs 自定義對稱解密算法初始向量 iv
* @return 解密後的字節數組
*/
private static byte[] decryptOfDiyIV(byte[] encryptedData, byte[] keyBytes, byte[] ivs) {
byte[] encryptedText = null;
init(keyBytes);
try {
cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(ivs));
encryptedText = cipher.doFinal(encryptedData);
} catch (Exception e) {
e.printStackTrace();
}
return encryptedText;
}
}