一、應用場景
我們在工作開發中可能經常要和其它部門或者第三方進行API對接,那麼如何保證我們提供的API和對接方能夠安全的數據傳輸呢。這就需要用到接口加密的方式來保障安全。本文主要介紹的內容就是一種比較靠譜的公衆平臺API加密實現方式。供大家參考。
二、技術介紹
開放平臺的消息加密解密技術方案基於AES加解密算法來實現,具體如下:
1. EncodingAESKey即消息加解密Key,長度固定爲43個字符,從a-z,A-Z,0-9共62個字符中選取。
2. AES密鑰: AESKey=Base64_Decode(EncodingAESKey + “=”),EncodingAESKey尾部填充一個字符的“=”, 用Base64_Decode生成32個字節的AESKey;
3. AES採用CBC模式,祕鑰長度爲32個字節(256位),數據採用PKCS#7填充 ; PKCS#7:K爲祕鑰字節數(採用32),buf爲待加密的內容,N爲其字節數。Buf 需要被填充爲K的整數倍。在buf的尾部填充(K-N%K)個字節,每個字節的內容 是(K- N%K)。
4. BASE64採用MIME格式,字符包括大小寫字母各26個,加上10個數字,和加號“+”,斜槓“/”,一共64個字符,等號“=”用作後綴填充;
三、代碼實現
1.計算公衆平臺的消息簽名類。
/** * SHA1 class * * 計算公衆平臺的消息簽名接口. */ class SHA1 { /** * 用SHA1算法生成安全簽名 * @param token 票據 * @param timestamp 時間戳 * @param nonce 隨機字符串 * @param encrypt 密文 * @return 安全簽名 * @throws AesException */ public static String getSHA1(String token, String timestamp, String nonce, String encrypt) throws AesException { try { String[] array = new String[] { token, timestamp, nonce, encrypt }; StringBuffer sb = new StringBuffer(); // 字符串排序 Arrays.sort(array); for (int i = 0; i < 4; i++) { sb.append(array[i]); } String str = sb.toString(); // SHA1簽名生成 MessageDigest md = MessageDigest.getInstance("SHA-1"); md.update(str.getBytes()); byte[] digest = md.digest(); StringBuffer hexstr = new StringBuffer(); String shaHex = ""; for (int i = 0; i < digest.length; i++) { shaHex = Integer.toHexString(digest[i] & 0xFF); if (shaHex.length() < 2) { hexstr.append(0); } hexstr.append(shaHex); } return hexstr.toString(); } catch (Exception e) { e.printStackTrace(); throw new AesException(AesException.ComputeSignatureError); } } }
2. 工具類ByteGroup。
import java.util.ArrayList; class ByteGroup { ArrayList<Byte> byteContainer = new ArrayList<Byte>(); public byte[] toBytes() { byte[] bytes = new byte[byteContainer.size()]; for (int i = 0; i < byteContainer.size(); i++) { bytes[i] = byteContainer.get(i); } return bytes; } public ByteGroup addBytes(byte[] bytes) { for (byte b : bytes) { byteContainer.add(b); } return this; } public int size() { return byteContainer.size(); } }
3. 基於PKCS7算法的加解密類。
/** * 提供基於PKCS7算法的加解密接口. */ class PKCS7Encoder { static Charset CHARSET = Charset.forName("utf-8"); static int BLOCK_SIZE = 32; /** * 獲得對明文進行補位填充的字節. * * @param count 需要進行填充補位操作的明文字節個數 * @return 補齊用的字節數組 */ static byte[] encode(int count) { // 計算需要填充的位數 int amountToPad = BLOCK_SIZE - (count % BLOCK_SIZE); if (amountToPad == 0) { amountToPad = BLOCK_SIZE; } // 獲得補位所用的字符 char padChr = chr(amountToPad); String tmp = new String(); for (int index = 0; index < amountToPad; index++) { tmp += padChr; } return tmp.getBytes(CHARSET); } /** * 刪除解密後明文的補位字符 * * @param decrypted 解密後的明文 * @return 刪除補位字符後的明文 */ static byte[] decode(byte[] decrypted) { int pad = (int) decrypted[decrypted.length - 1]; if (pad < 1 || pad > 32) { pad = 0; } return Arrays.copyOfRange(decrypted, 0, decrypted.length - pad); } /** * 將數字轉化成ASCII碼對應的字符,用於對明文進行補碼 * * @param a 需要轉化的數字 * @return 轉化得到的字符 */ static char chr(int a) { byte target = (byte) (a & 0xFF); return (char) target; } }
4. 異常類。
@SuppressWarnings("serial") public class AesException extends Exception { public final static int OK = 0; public final static int ValidateSignatureError = -40001; public final static int ParseXmlError = -40002; public final static int ComputeSignatureError = -40003; public final static int IllegalAesKey = -40004; public final static int ValidateAppidError = -40005; public final static int EncryptAESError = -40006; public final static int DecryptAESError = -40007; public final static int IllegalBuffer = -40008; private int code; private static String getMessage(int code) { switch (code) { case ValidateSignatureError: return "簽名驗證錯誤"; case ParseXmlError: return "xml解析失敗"; case ComputeSignatureError: return "sha加密生成簽名失敗"; case IllegalAesKey: return "SymmetricKey非法"; case ValidateAppidError: return "appid校驗失敗"; case EncryptAESError: return "aes加密失敗"; case DecryptAESError: return "aes解密失敗"; case IllegalBuffer: return "解密後得到的buffer非法"; default: return null; // cannot be } } public int getCode() { return code; } AesException(int code) { super(getMessage(code)); this.code = code; } }
5. 公衆平臺加解密接口類。
/** * 提供接收和推送給公衆平臺消息的加解密接口(UTF8編碼的字符串). * <ol> * <li>第三方回覆加密消息給公衆平臺</li> * <li>第三方收到公衆平臺發送的消息,驗證消息的安全性,並對消息進行解密。</li> * </ol> */ public class WXBizMsgCrypt { static Charset CHARSET = Charset.forName("utf-8"); Base64 base64 = new Base64(); byte[] aesKey; String token; String appId; /** * 構造函數 * @param token 公衆平臺上,開發者設置的token * @param encodingAesKey 公衆平臺上,開發者設置的EncodingAESKey * @param appId 公衆平臺appid * * @throws AesException 執行失敗,請查看該異常的錯誤碼和具體的錯誤信息 */ public WXBizMsgCrypt(String token, String encodingAesKey, String appId) throws AesException { if (encodingAesKey.length() != 43) { throw new AesException(AesException.IllegalAesKey); } this.token = token; this.appId = appId; aesKey = Base64.decodeBase64(encodingAesKey + "="); } // 生成4個字節的網絡字節序 byte[] getNetworkBytesOrder(int sourceNumber) { byte[] orderBytes = new byte[4]; orderBytes[3] = (byte) (sourceNumber & 0xFF); orderBytes[2] = (byte) (sourceNumber >> 8 & 0xFF); orderBytes[1] = (byte) (sourceNumber >> 16 & 0xFF); orderBytes[0] = (byte) (sourceNumber >> 24 & 0xFF); return orderBytes; } // 還原4個字節的網絡字節序 int recoverNetworkBytesOrder(byte[] orderBytes) { int sourceNumber = 0; for (int i = 0; i < 4; i++) { sourceNumber <<= 8; sourceNumber |= orderBytes[i] & 0xff; } return sourceNumber; } // 隨機生成16位字符串 String getRandomStr() { String base = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; Random random = new Random(); StringBuffer sb = new StringBuffer(); for (int i = 0; i < 16; i++) { int number = random.nextInt(base.length()); sb.append(base.charAt(number)); } return sb.toString(); } /** * 對明文進行加密. * * @param text 需要加密的明文 * @return 加密後base64編碼的字符串 * @throws AesException aes加密失敗 */ String encrypt(String randomStr, String text) throws AesException { ByteGroup byteCollector = new ByteGroup(); byte[] randomStrBytes = randomStr.getBytes(CHARSET); byte[] textBytes = text.getBytes(CHARSET); byte[] networkBytesOrder = getNetworkBytesOrder(textBytes.length); byte[] appidBytes = appId.getBytes(CHARSET); // randomStr + networkBytesOrder + text + appid byteCollector.addBytes(randomStrBytes); byteCollector.addBytes(networkBytesOrder); byteCollector.addBytes(textBytes); byteCollector.addBytes(appidBytes); // ... + pad: 使用自定義的填充方式對明文進行補位填充 byte[] padBytes = PKCS7Encoder.encode(byteCollector.size()); byteCollector.addBytes(padBytes); // 獲得最終的字節流, 未加密 byte[] unencrypted = byteCollector.toBytes(); try { // 設置加密模式爲AES的CBC模式 Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES"); IvParameterSpec iv = new IvParameterSpec(aesKey, 0, 16); cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv); // 加密 byte[] encrypted = cipher.doFinal(unencrypted); // 使用BASE64對加密後的字符串進行編碼 String base64Encrypted = base64.encodeToString(encrypted); return base64Encrypted; } catch (Exception e) { e.printStackTrace(); throw new AesException(AesException.EncryptAESError); } } /** * 對密文進行解密. * * @param text 需要解密的密文 * @return 解密得到的明文 * @throws AesException aes解密失敗 */ String decrypt(String text) throws AesException { byte[] original; try { // 設置解密模式爲AES的CBC模式 Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); SecretKeySpec key_spec = new SecretKeySpec(aesKey, "AES"); IvParameterSpec iv = new IvParameterSpec(Arrays.copyOfRange(aesKey, 0, 16)); cipher.init(Cipher.DECRYPT_MODE, key_spec, iv); // 使用BASE64對密文進行解碼 byte[] encrypted = Base64.decodeBase64(text); // 解密 original = cipher.doFinal(encrypted); } catch (Exception e) { e.printStackTrace(); throw new AesException(AesException.DecryptAESError); } String xmlContent, from_appid; try { // 去除補位字符 byte[] bytes = PKCS7Encoder.decode(original); // 分離16位隨機字符串,網絡字節序和AppId byte[] networkOrder = Arrays.copyOfRange(bytes, 16, 20); int xmlLength = recoverNetworkBytesOrder(networkOrder); xmlContent = new String(Arrays.copyOfRange(bytes, 20, 20 + xmlLength), CHARSET); from_appid = new String(Arrays.copyOfRange(bytes, 20 + xmlLength, bytes.length), CHARSET); } catch (Exception e) { e.printStackTrace(); throw new AesException(AesException.IllegalBuffer); } // appid不相同的情況 if (!from_appid.equals(appId)) { throw new AesException(AesException.ValidateAppidError); } return xmlContent; } /** * 將公衆平臺回覆用戶的消息加密打包. * <ol> * <li>對要發送的消息進行AES-CBC加密</li> * <li>生成安全簽名</li> * <li>將消息密文和安全簽名打包成xml格式</li> * </ol> * * @param replyMsg 公衆平臺待回覆用戶的消息,xml格式的字符串 * @param timeStamp 時間戳,可以自己生成,也可以用URL參數的timestamp * @param nonce 隨機串,可以自己生成,也可以用URL參數的nonce * * @return 加密後的可以直接回複用戶的密文,包括msg_signature, timestamp, nonce, encrypt的xml格式的字符串 * @throws AesException 執行失敗,請查看該異常的錯誤碼和具體的錯誤信息 */ public String encryptMsg(String replyMsg, String timeStamp, String nonce) throws AesException { // 加密 String encrypt = encrypt(getRandomStr(), replyMsg); // 生成安全簽名 if (timeStamp == "") { timeStamp = Long.toString(System.currentTimeMillis()); } String signature = SHA1.getSHA1(token, timeStamp, nonce, encrypt); // 生成發送的xml String result = this.generateResp(encrypt, signature, timeStamp, nonce); return result; } /** * 檢驗消息的真實性,並且獲取解密後的明文. * <ol> * <li>利用收到的密文生成安全簽名,進行簽名驗證</li> * <li>若驗證通過,則提取xml中的加密消息</li> * <li>對消息進行解密</li> * </ol> * * @param msgSignature 簽名串,對應URL參數的msg_signature * @param timeStamp 時間戳,對應URL參數的timestamp * @param nonce 隨機串,對應URL參數的nonce * @param postData 密文,對應POST請求的數據 * * @return 解密後的原文 * @throws AesException 執行失敗,請查看該異常的錯誤碼和具體的錯誤信息 */ public String decryptMsg(String msgSignature, String timeStamp, String nonce, String postData) throws AesException { // 驗證安全簽名 String signature = SHA1.getSHA1(token, timeStamp, nonce, postData); System.out.println("signature: "+signature); if (!signature.equals(msgSignature)) { throw new AesException(AesException.ValidateSignatureError); } // 解密 String result = decrypt(postData); return result; } /** * @Description: 生成密文需要參數JSON * @exception * @author mazhq * @date 2019/9/11 17:59 */ public String generateResp(String encrypt, String signature, String timestamp, String nonce) { JSONObject encryptJson = new JSONObject(); encryptJson.put("encrypt", encrypt); encryptJson.put("signature", signature); encryptJson.put("timestamp", timestamp); encryptJson.put("nonce", nonce); return encryptJson.toJSONString(); } }
6.功能測試類。
public class ProgramTest { public static void main(String[] args) throws Exception { // 需要加密的明文 String encodingAesKey = "QcXnkOzZWW7w0kgG1UxQtolNWi6hWIuC0izesp8XkcG"; String token = "testToken2019"; String timestamp = "1566875499"; String nonce = "1494577237"; String appId = "wxe7cb807f01a4c762"; String replyMsg = "mazhq"; WXBizMsgCrypt pc = new WXBizMsgCrypt(token, encodingAesKey, appId); String mingwen = pc.encryptMsg(replyMsg, timestamp, nonce); System.out.println("加密後: " + mingwen); JSONObject json = JSONObject.parseObject(mingwen); String resp = pc.decryptMsg(json.getString("signature"), timestamp, nonce,json.getString("encrypt")); System.out.println("解密後: " +resp); } }
7.數據輸出內容。
加密後: {"signature":"2ea37bd43ebbc9e8f958cabee0eac0517f8b8dc4","encrypt":"/WQr/kY24MGKbnCjX2WBP6rf6w/Up6Nx6E1sr35qyIHyQp3YDmpnKD3UwoR4zs9eM09KxvEXHAKWPJ/Tpb7gfQ==","nonce":"1494577237","timestamp":"1566875499"}
signature: 2ea37bd43ebbc9e8f958cabee0eac0517f8b8dc4
解密密後: mazhq