|
|
@@ -0,0 +1,258 @@
|
|
|
+package com.usoftchina.qywx.sdk.weixin;
|
|
|
+
|
|
|
+
|
|
|
+import javax.crypto.Cipher;
|
|
|
+import javax.crypto.spec.IvParameterSpec;
|
|
|
+import javax.crypto.spec.SecretKeySpec;
|
|
|
+import java.nio.charset.Charset;
|
|
|
+import java.util.Arrays;
|
|
|
+import java.util.Base64;
|
|
|
+import java.util.Random;
|
|
|
+
|
|
|
+
|
|
|
+public class WXBizJsonMsgCrypt {
|
|
|
+ private static Charset CHARSET = Charset.forName("utf-8");;
|
|
|
+ private byte[] aesKey;
|
|
|
+ private String token;
|
|
|
+ private String receiveid;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 构造函数
|
|
|
+ * @param token 企业微信后台,开发者设置的token
|
|
|
+ * @param encodingAesKey 企业微信后台,开发者设置的EncodingAESKey
|
|
|
+ * @param receiveid, 不同场景含义不同,详见文档
|
|
|
+ *
|
|
|
+ * @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息
|
|
|
+ */
|
|
|
+ public WXBizJsonMsgCrypt(String token, String encodingAesKey, String receiveid) throws AesException {
|
|
|
+ if (encodingAesKey.length() != 43) {
|
|
|
+ throw new AesException(AesException.IllegalAesKey);
|
|
|
+ }
|
|
|
+
|
|
|
+ this.token = token;
|
|
|
+ this.receiveid = receiveid;
|
|
|
+ aesKey = Base64.getDecoder().decode(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[] receiveidBytes = receiveid.getBytes(CHARSET);
|
|
|
+
|
|
|
+ // randomStr + networkBytesOrder + text + receiveid
|
|
|
+ byteCollector.addBytes(randomStrBytes);
|
|
|
+ byteCollector.addBytes(networkBytesOrder);
|
|
|
+ byteCollector.addBytes(textBytes);
|
|
|
+ byteCollector.addBytes(receiveidBytes);
|
|
|
+
|
|
|
+ // ... + 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.getEncoder().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.getDecoder().decode(text);
|
|
|
+
|
|
|
+ // 解密
|
|
|
+ original = cipher.doFinal(encrypted);
|
|
|
+ } catch (Exception e) {
|
|
|
+ e.printStackTrace();
|
|
|
+ throw new AesException(AesException.DecryptAESError);
|
|
|
+ }
|
|
|
+
|
|
|
+ String jsonContent, from_receiveid;
|
|
|
+ try {
|
|
|
+ // 去除补位字符
|
|
|
+ byte[] bytes = PKCS7Encoder.decode(original);
|
|
|
+
|
|
|
+ // 分离16位随机字符串,网络字节序和receiveid
|
|
|
+ byte[] networkOrder = Arrays.copyOfRange(bytes, 16, 20);
|
|
|
+
|
|
|
+ int jsonLength = recoverNetworkBytesOrder(networkOrder);
|
|
|
+
|
|
|
+ jsonContent = new String(Arrays.copyOfRange(bytes, 20, 20 + jsonLength), CHARSET);
|
|
|
+ from_receiveid = new String(Arrays.copyOfRange(bytes, 20 + jsonLength, bytes.length),
|
|
|
+ CHARSET);
|
|
|
+ } catch (Exception e) {
|
|
|
+ e.printStackTrace();
|
|
|
+ throw new AesException(AesException.IllegalBuffer);
|
|
|
+ }
|
|
|
+
|
|
|
+ // receiveid不相同的情况
|
|
|
+ if (!from_receiveid.equals(receiveid)) {
|
|
|
+ throw new AesException(AesException.ValidateCorpidError);
|
|
|
+ }
|
|
|
+ return jsonContent;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 将企业微信回复用户的消息加密打包.
|
|
|
+ * <ol>
|
|
|
+ * <li>对要发送的消息进行AES-CBC加密</li>
|
|
|
+ * <li>生成安全签名</li>
|
|
|
+ * <li>将消息密文和安全签名打包成json格式</li>
|
|
|
+ * </ol>
|
|
|
+ *
|
|
|
+ * @param replyMsg 企业微信待回复用户的消息,json格式的字符串
|
|
|
+ * @param timeStamp 时间戳,可以自己生成,也可以用URL参数的timestamp
|
|
|
+ * @param nonce 随机串,可以自己生成,也可以用URL参数的nonce
|
|
|
+ *
|
|
|
+ * @return 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的json格式的字符串
|
|
|
+ * @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);
|
|
|
+
|
|
|
+ String result = JsonParse.generate(encrypt, signature, timeStamp, nonce);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 检验消息的真实性,并且获取解密后的明文.
|
|
|
+ * <ol>
|
|
|
+ * <li>利用收到的密文生成安全签名,进行签名验证</li>
|
|
|
+ * <li>若验证通过,则提取json中的加密消息</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 {
|
|
|
+
|
|
|
+ // 密钥,公众账号的app secret
|
|
|
+ // 提取密文
|
|
|
+ Object[] encrypt = JsonParse.extract(postData);
|
|
|
+
|
|
|
+ // 验证安全签名
|
|
|
+ String signature = SHA1.getSHA1(token, timeStamp, nonce, encrypt[1].toString());
|
|
|
+
|
|
|
+ // 和URL中的签名比较是否相等
|
|
|
+ // System.out.println("第三方收到URL中的签名:" + msg_sign);
|
|
|
+ // System.out.println("第三方校验签名:" + signature);
|
|
|
+ if (!signature.equals(msgSignature)) {
|
|
|
+ throw new AesException(AesException.ValidateSignatureError);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 解密
|
|
|
+ String result = decrypt(encrypt[1].toString());
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 验证URL
|
|
|
+ * @param msgSignature 签名串,对应URL参数的msg_signature
|
|
|
+ * @param timeStamp 时间戳,对应URL参数的timestamp
|
|
|
+ * @param nonce 随机串,对应URL参数的nonce
|
|
|
+ * @param echoStr 随机串,对应URL参数的echostr
|
|
|
+ *
|
|
|
+ * @return 解密之后的echostr
|
|
|
+ * @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息
|
|
|
+ */
|
|
|
+ public String VerifyURL(String msgSignature, String timeStamp, String nonce, String echoStr)
|
|
|
+ throws AesException {
|
|
|
+ String signature = SHA1.getSHA1(token, timeStamp, nonce, echoStr);
|
|
|
+
|
|
|
+ if (!signature.equals(msgSignature)) {
|
|
|
+ throw new AesException(AesException.ValidateSignatureError);
|
|
|
+ }
|
|
|
+
|
|
|
+ String result = decrypt(echoStr);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+}
|