微信小程序加密消息解密工具類

 

開發者後臺校驗與解密開放數據

微信會對這些開放數據做簽名和加密處理。開發者後臺拿到開放數據後可以對數據進行校驗簽名和解密,來保證數據不被篡改。

簽名校驗以及數據加解密涉及用戶的會話密鑰 session_key。 開發者應該事先通過 wx.login 登錄流程獲取會話密鑰 session_key 並保存在服務器。爲了數據不被篡改,開發者不應該把 session_key 傳到小程序客戶端等服務器外的環境。

數據簽名校驗

爲了確保開放接口返回用戶數據的安全性,微信會對明文數據進行簽名。開發者可以根據業務需要對數據包進行簽名校驗,確保數據的完整性。

  1. 通過調用接口(如 wx.getUserInfo)獲取數據時,接口會同時返回 rawData、signature,其中 signature = sha1( rawData + session_key )
  2. 開發者將 signature、rawData 發送到開發者服務器進行校驗。服務器利用用戶對應的 session_key 使用相同的算法計算出簽名 signature2 ,比對 signature 與 signature2 即可校驗數據的完整性。

如 wx.getUserInfo的數據校驗:

接口返回的rawData:

加密數據解密算法

接口如果涉及敏感數據(如wx.getUserInfo當中的 openId 和 unionId),接口的明文內容將不包含這些敏感數據。開發者如需要獲取敏感數據,需要對接口返回的加密數據(encryptedData) 進行對稱解密。 解密算法如下:

  1. 對稱解密使用的算法爲 AES-128-CBC,數據採用PKCS#7填充。
  2. 對稱解密的目標密文爲 Base64_Decode(encryptedData)。
  3. 對稱解密祕鑰 aeskey = Base64_Decode(session_key), aeskey 是16字節。
  4. 對稱解密算法初始向量 爲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
    }
}

注:

  1. 解密後得到的json數據根據需求可能會增加新的字段,舊字段不會改變和刪減,開發者需要預留足夠的空間

會話密鑰 session_key 有效性

開發者如果遇到因爲 session_key 不正確而校驗簽名失敗或解密失敗,請關注下面幾個與 session_key 有關的注意事項。

  1. wx.login 調用時,用戶的 session_key 可能會被更新而致使舊 session_key 失效(刷新機制存在最短週期,如果同一個用戶短時間內多次調用 wx.login,並非每次調用都導致 session_key 刷新)。開發者應該在明確需要重新登錄時才調用 wx.login,及時通過 auth.code2Session 接口更新服務器存儲的 session_key。
  2. 微信不會把 session_key 的有效期告知開發者。我們會根據用戶使用小程序的行爲對 session_key 進行續期。用戶越頻繁使用小程序,session_key 有效期越長。
  3. 開發者在 session_key 失效時,可以通過重新執行登錄流程獲取有效的 session_key。使用接口 wx.checkSession可以校驗 session_key 是否有效,從而避免小程序反覆執行登錄流程。
  4. 當開發者在實現自定義登錄態時,可以考慮以 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;
    }

}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章