JAVA版微信小程式用戶數據的簽名驗證和加解密

  • 2019 年 12 月 9 日
  • 筆記

簽名驗證和加解密

數據簽名校驗

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

  • 簽名校驗演算法涉及用戶的session_key,通過 wx.login 登錄流程獲取用戶session_key,並自行維護與應用自身登錄態的對應關係。
  • 通過調用介面(如 wx.getUserInfo)獲取數據時,介面會同時返回 rawData、signature,其中 signature = sha1( rawData + session_key )
  • 開發者將 signature、rawData 發送到開發者伺服器進行校驗。伺服器利用用戶對應的 session_key 使用相同的演算法計算出簽名 signature2 ,比對 signature 與 signature2 即可校驗數據的完整性。

加密數據解密演算法

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

  • 對稱解密使用的演算法為 AES-128-CBC,數據採用PKCS#7填充。
  • 對稱解密的目標密文為 Base64_Decode(encryptedData),
  • 對稱解密秘鑰 aeskey = Base64_Decode(session_key), aeskey 是16位元組
  • 對稱解密演算法初始向量 iv 會在數據介面中返回。

微信官方提供了多種程式語言的示例程式碼(點擊下載),但就是沒提供JAVA版本的,可能的確PHP是最好的語言,騰訊提供的demo好多都是PHP版本的。

JAVA程式碼案例

pom.xml引入以下依賴:

<dependency>       <groupId>commons-codec</groupId>       <artifactId>commons-codec</artifactId>       <version>1.10</version>  </dependency>  <dependency>       <groupId>com.alibaba</groupId>       <artifactId>fastjson</artifactId>       <version>1.2.7</version>  </dependency>   <dependency>       <groupId>org.bouncycastle</groupId>       <artifactId>bcprov-jdk15on</artifactId>       <version>1.57</version>  </dependency>

我們可以參考PHP給出的程式碼,使用JAVA實現: AESUtil:

import org.bouncycastle.jce.provider.BouncyCastleProvider;  import java.security.AlgorithmParameters;  import java.security.InvalidAlgorithmParameterException;  import java.security.InvalidKeyException;  import java.security.Key;  import java.security.NoSuchAlgorithmException;  import java.security.NoSuchProviderException;  import java.security.Security;  import javax.crypto.BadPaddingException;  import javax.crypto.Cipher;  import javax.crypto.IllegalBlockSizeException;  import javax.crypto.NoSuchPaddingException;  import javax.crypto.spec.IvParameterSpec;  import javax.crypto.spec.SecretKeySpec;  /**   * AES解密   * 創建者 柒   * 創建時間    2018年3月12日   */  public class AESUtil {        static {          Security.addProvider(new BouncyCastleProvider());      }        /**       * AES解密       * @param content 密文       * @return       * @throws InvalidAlgorithmParameterException       * @throws NoSuchProviderException       */      public static byte[] decrypt(byte[] content, byte[] keyByte, byte[] ivByte)              throws InvalidAlgorithmParameterException {          try {              Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");              Key sKeySpec = new SecretKeySpec(keyByte, "AES");              //生成iv              AlgorithmParameters params = AlgorithmParameters.getInstance("AES");              params.init(new IvParameterSpec(ivByte));              cipher.init(Cipher.DECRYPT_MODE, sKeySpec, params);// 初始化              return cipher.doFinal(content);          } catch (NoSuchAlgorithmException e) {              e.printStackTrace();          } catch (NoSuchPaddingException e) {              e.printStackTrace();          } catch (InvalidKeyException e) {              e.printStackTrace();          } catch (IllegalBlockSizeException e) {              e.printStackTrace();          } catch (BadPaddingException e) {              e.printStackTrace();          } catch (Exception e) {              e.printStackTrace();          }          return null;      }  }

WXBizDataCrypt:

import java.io.UnsupportedEncodingException;  import java.security.InvalidAlgorithmParameterException;  import org.apache.commons.codec.binary.Base64;  import org.apache.commons.lang.StringUtils;  import com.alibaba.fastjson.JSON;  import com.alibaba.fastjson.JSONObject;  /**   * 對微信小程式用戶加密數據的解密   * 創建者 柒   * 創建時間 2018年3月12日   */  public class WXBizDataCrypt {        public static String illegalAesKey = "-41001";//非法密鑰      public static String illegalIv = "-41002";//非法初始向量      public static String illegalBuffer = "-41003";//非法密文      public static String decodeBase64Error = "-41004"; //解碼錯誤      public static String noData = "-41005"; //數據不正確        private String appid;        private String sessionKey;        public WXBizDataCrypt(String appid, String sessionKey) {          this.appid = appid;          this.sessionKey = sessionKey;      }        /**       * 檢驗數據的真實性,並且獲取解密後的明文.       * @param encryptedData  string 加密的用戶數據       * @param iv  string 與用戶數據一同返回的初始向量       * @return data string 解密後的原文       * @return String 返回用戶資訊       */      public String decryptData(String encryptedData, String iv) {          if (StringUtils.length(sessionKey) != 24) {              return illegalAesKey;          }          // 對稱解密秘鑰 aeskey = Base64_Decode(session_key), aeskey 是16位元組。          byte[] aesKey = Base64.decodeBase64(sessionKey);            if (StringUtils.length(iv) != 24) {              return illegalIv;          }          // 對稱解密演算法初始向量 為Base64_Decode(iv),其中iv由數據介面返回。          byte[] aesIV = Base64.decodeBase64(iv);            // 對稱解密的目標密文為 Base64_Decode(encryptedData)          byte[] aesCipher = Base64.decodeBase64(encryptedData);            try {              byte[] resultByte = AESUtil.decrypt(aesCipher, aesKey, aesIV);              if (null != resultByte && resultByte.length > 0) {                  String userInfo = new String(resultByte, "UTF-8");                  JSONObject jsons = JSON.parseObject(userInfo);                  String id = jsons.getJSONObject("watermark").getString("appid");                  if (!StringUtils.equals(id, appid)) {                      return illegalBuffer;                  }                  return userInfo;              } else {                  return noData;              }          } catch (InvalidAlgorithmParameterException e) {              e.printStackTrace();          } catch (UnsupportedEncodingException e) {              e.printStackTrace();          }          return null;      }        /**       * encryptedData 和 iv 兩個參數通過小程式wx.getUserInfo()方法獲取       * @param args       * @see       */      public static void main(String[] args) {          String appId = "wx4f4bc4dec97d474b";          String sessionKey = "tiihtNczf5v6AKRyjwEUhQ==";          String encryptedData = "CiyLU1Aw2KjvrjMdj8YKliAjtP4gsMZM"                  + "QmRzooG2xrDcvSnxIMXFufNstNGTyaGS"                  + "9uT5geRa0W4oTOb1WT7fJlAC+oNPdbB+"                  + "3hVbJSRgv+4lGOETKUQz6OYStslQ142d"                  + "NCuabNPGBzlooOmB231qMM85d2/fV6Ch"                  + "evvXvQP8Hkue1poOFtnEtpyxVLW1zAo6"                  + "/1Xx1COxFvrc2d7UL/lmHInNlxuacJXw"                  + "u0fjpXfz/YqYzBIBzD6WUfTIF9GRHpOn"                  + "/Hz7saL8xz+W//FRAUid1OksQaQx4CMs"                  + "8LOddcQhULW4ucetDf96JcR3g0gfRK4P"                  + "C7E/r7Z6xNrXd2UIeorGj5Ef7b1pJAYB"                  + "6Y5anaHqZ9J6nKEBvB4DnNLIVWSgARns"                  + "/8wR2SiRS7MNACwTyrGvt9ts8p12PKFd"                  + "lqYTopNHR1Vf7XjfhQlVsAJdNiKdYmYV"                  + "oKlaRv85IfVunYzO0IKXsyl7JCUjCpoG"                  + "20f0a04COwfneQAGGwd5oa+T8yO5hzuy"                  + "Db/XcxxmK01EpqOyuxINew==";          String iv = "r7BXXKkLb8qrSNn05n0qiA==";            WXBizDataCrypt biz = new WXBizDataCrypt(appId, sessionKey);            System.out.println(biz.decryptData(encryptedData, iv));        }  }

運行main方法,獲取返回結果:

{"openId":"oGZUI0egBJY1zhBYw2KhdUfwVJJE","nickName":"Band","gender":1,"language":"zh_CN","city":"Guangzhou","province":"Guangdong","country":"CN","avatarUrl":"http://wx.qlogo.cn/mmopen/vi_32/aSKcBBPpibyKNicHNTMM0qJVh8Kjgiak2AHWr8MHM4WgMEm7GFhsf8OYrySdbvAMvTsw3mo8ibKicsnfN5pRjl1p8HQ/0","unionId":"ocMvos6NjeKLIBqg5Mr9QjxrP1FA","watermark":{"timestamp":1477314187,"appid":"wx4f4bc4dec97d474b"}}