|
|
@@ -0,0 +1,284 @@
|
|
|
+package com.fs.common.utils;
|
|
|
+
|
|
|
+import com.alibaba.fastjson.JSON;
|
|
|
+import com.alibaba.fastjson.serializer.SerializerFeature;
|
|
|
+import org.apache.commons.codec.binary.Base64;
|
|
|
+
|
|
|
+import javax.crypto.Cipher;
|
|
|
+import javax.crypto.spec.IvParameterSpec;
|
|
|
+import javax.crypto.spec.SecretKeySpec;
|
|
|
+import java.security.*;
|
|
|
+import java.security.spec.PKCS8EncodedKeySpec;
|
|
|
+import java.security.spec.X509EncodedKeySpec;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.Random;
|
|
|
+import java.util.TreeMap;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 红杉健康开放平台加解密工具类
|
|
|
+ * 遵循文档规则:AES-256-CBC加密内容 + RSA加密AES密钥 + MD5签名
|
|
|
+ */
|
|
|
+public class RedwoodCryptoUtil {
|
|
|
+
|
|
|
+ // 加密算法配置(与文档一致)
|
|
|
+ private static final String AES_ALGORITHM = "AES/CBC/PKCS5Padding";
|
|
|
+ private static final String AES_KEY_ALGORITHM = "AES";
|
|
|
+ private static final int AES_KEY_LENGTH = 256;
|
|
|
+ private static final String RSA_ALGORITHM = "RSA/ECB/PKCS1Padding";
|
|
|
+ private static final String RSA_KEY_ALGORITHM = "RSA";
|
|
|
+ private static final String SIGN_ALGORITHM = "MD5";
|
|
|
+
|
|
|
+ // 配置参数(需根据实际环境替换)
|
|
|
+ private final String appSecret; // 平台提供的应用秘钥(作为AES初始化向量IV)
|
|
|
+ private final String platformPublicKey; // 红杉平台RSA公钥(加密AES密钥用)
|
|
|
+ private final String thirdPrivateKey; // 三方平台RSA私钥(解密响应密钥用)
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 构造函数:初始化配置参数
|
|
|
+ *
|
|
|
+ * @param appSecret 平台应用秘钥(文档中appsecret)
|
|
|
+ * @param platformPublicKey 红杉平台RSA公钥(Base64编码)
|
|
|
+ * @param thirdPrivateKey 三方平台RSA私钥(Base64编码)
|
|
|
+ */
|
|
|
+ public RedwoodCryptoUtil(String appSecret, String platformPublicKey, String thirdPrivateKey) {
|
|
|
+ this.appSecret = appSecret;
|
|
|
+ this.platformPublicKey = platformPublicKey;
|
|
|
+ this.thirdPrivateKey = thirdPrivateKey;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 加密请求参数:生成接口所需的content、key、sign
|
|
|
+ *
|
|
|
+ * @param plainParam 明文请求参数(Map格式)
|
|
|
+ * @return 加密后的请求参数Map(包含content、key、sign)
|
|
|
+ * @throws Exception 加密异常
|
|
|
+ */
|
|
|
+ public Map<String, String> encryptRequest(Map<String, Object> plainParam) throws Exception {
|
|
|
+ // 1. 参数按ASCII码排序并转为JSON字符串(不转义Unicode)
|
|
|
+ Map<String, Object> sortedMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
|
|
|
+ sortedMap.putAll(plainParam);
|
|
|
+ String contentStr = new String(
|
|
|
+ JSON.toJSONBytes(sortedMap, SerializerFeature.BrowserCompatible)
|
|
|
+ );
|
|
|
+
|
|
|
+ // 2. 生成32位随机AES密钥(randStr)
|
|
|
+ String aesKey = generateRandomAesKey();
|
|
|
+
|
|
|
+ // 3. AES-256-CBC加密contentStr(IV为appSecret)
|
|
|
+ String encryptedContent = aesEncrypt(contentStr, aesKey, appSecret);
|
|
|
+
|
|
|
+ // 4. MD5生成签名(对原始contentStr)
|
|
|
+ String sign = generateSign(contentStr);
|
|
|
+
|
|
|
+ // 5. 用平台公钥加密AES密钥(RSA-PKCS1-SHA256)
|
|
|
+ String encryptedAesKey = rsaEncrypt(aesKey, platformPublicKey);
|
|
|
+
|
|
|
+ // 6. 返回加密结果(Base64编码后)
|
|
|
+ Map<String, String> encryptedParam = new HashMap<>();
|
|
|
+ encryptedParam.put("content", Base64.encodeBase64String(encryptedContent.getBytes()));
|
|
|
+ encryptedParam.put("key", Base64.encodeBase64String(encryptedAesKey.getBytes()));
|
|
|
+ encryptedParam.put("sign", sign);
|
|
|
+ return encryptedParam;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 解密响应参数:从content、key、sign中还原明文
|
|
|
+ *
|
|
|
+ * @param encryptedResponse 加密响应参数(包含content、key、sign)
|
|
|
+ * @return 明文响应结果(Map格式)
|
|
|
+ * @throws Exception 解密异常
|
|
|
+ */
|
|
|
+ public Map<String, Object> decryptResponse(Map<String, String> encryptedResponse) throws Exception {
|
|
|
+ // 1. 提取加密字段并Base64解码
|
|
|
+ String encryptedContent = new String(Base64.decodeBase64(encryptedResponse.get("content")));
|
|
|
+ String encryptedAesKey = new String(Base64.decodeBase64(encryptedResponse.get("key")));
|
|
|
+ String sign = encryptedResponse.get("sign");
|
|
|
+
|
|
|
+ // 2. 用三方私钥解密AES密钥
|
|
|
+ String aesKey = rsaDecrypt(encryptedAesKey, thirdPrivateKey);
|
|
|
+
|
|
|
+ // 3. AES-256-CBC解密content(IV为appSecret)
|
|
|
+ String contentStr = aesDecrypt(encryptedContent, aesKey, appSecret);
|
|
|
+
|
|
|
+ // 4. 验证签名(防止数据篡改)
|
|
|
+ if (!verifySign(contentStr, sign)) {
|
|
|
+ throw new SecurityException("签名验证失败,数据可能被篡改");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 5. 解析JSON为Map返回
|
|
|
+ return JSON.parseObject(contentStr, Map.class);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * AES加密(AES-256-CBC)
|
|
|
+ */
|
|
|
+ private String aesEncrypt(String content, String key, String iv) throws Exception {
|
|
|
+ SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(), AES_KEY_ALGORITHM);
|
|
|
+ IvParameterSpec ivSpec = new IvParameterSpec(iv.getBytes());
|
|
|
+ Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
|
|
|
+ cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec);
|
|
|
+ byte[] encryptedBytes = cipher.doFinal(content.getBytes("UTF-8"));
|
|
|
+ return Base64.encodeBase64String(encryptedBytes);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * AES解密(AES-256-CBC)
|
|
|
+ */
|
|
|
+ private String aesDecrypt(String encryptedContent, String key, String iv) throws Exception {
|
|
|
+ SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(), AES_KEY_ALGORITHM);
|
|
|
+ IvParameterSpec ivSpec = new IvParameterSpec(iv.getBytes());
|
|
|
+ Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
|
|
|
+ cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec);
|
|
|
+ byte[] decryptedBytes = cipher.doFinal(Base64.decodeBase64(encryptedContent));
|
|
|
+ return new String(decryptedBytes, "UTF-8");
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * RSA加密(用公钥加密AES密钥)
|
|
|
+ */
|
|
|
+ private String rsaEncrypt(String content, String publicKeyStr) throws Exception {
|
|
|
+ X509EncodedKeySpec keySpec = new X509EncodedKeySpec(Base64.decodeBase64(publicKeyStr));
|
|
|
+ PublicKey publicKey = KeyFactory.getInstance(RSA_KEY_ALGORITHM).generatePublic(keySpec);
|
|
|
+ Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
|
|
|
+ cipher.init(Cipher.ENCRYPT_MODE, publicKey);
|
|
|
+ byte[] encryptedBytes = cipher.doFinal(content.getBytes("UTF-8"));
|
|
|
+ return Base64.encodeBase64String(encryptedBytes);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * RSA解密(用私钥解密AES密钥)
|
|
|
+ */
|
|
|
+ private String rsaDecrypt(String encryptedContent, String privateKeyStr) throws Exception {
|
|
|
+ PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKeyStr));
|
|
|
+ PrivateKey privateKey = KeyFactory.getInstance(RSA_KEY_ALGORITHM).generatePrivate(keySpec);
|
|
|
+ Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
|
|
|
+ cipher.init(Cipher.DECRYPT_MODE, privateKey);
|
|
|
+ byte[] decryptedBytes = cipher.doFinal(Base64.decodeBase64(encryptedContent));
|
|
|
+ return new String(decryptedBytes, "UTF-8");
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成MD5签名
|
|
|
+ */
|
|
|
+ private String generateSign(String content) throws Exception {
|
|
|
+ MessageDigest md5 = MessageDigest.getInstance(SIGN_ALGORITHM);
|
|
|
+ byte[] signBytes = md5.digest(content.getBytes("UTF-8"));
|
|
|
+ return bytesToHex(signBytes);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 验证MD5签名
|
|
|
+ */
|
|
|
+ private boolean verifySign(String content, String sign) throws Exception {
|
|
|
+ String generatedSign = generateSign(content);
|
|
|
+ return generatedSign.equalsIgnoreCase(sign);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成32位随机AES密钥(字母+数字)
|
|
|
+ */
|
|
|
+ private String generateRandomAesKey() {
|
|
|
+ String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
|
+ StringBuilder sb = new StringBuilder(32);
|
|
|
+ Random random = new SecureRandom();
|
|
|
+ for (int i = 0; i < 32; i++) {
|
|
|
+ sb.append(chars.charAt(random.nextInt(chars.length())));
|
|
|
+ }
|
|
|
+ return sb.toString();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 字节数组转16进制字符串
|
|
|
+ */
|
|
|
+ private String bytesToHex(byte[] bytes) {
|
|
|
+ StringBuilder hex = new StringBuilder();
|
|
|
+ for (byte b : bytes) {
|
|
|
+ String hexStr = Integer.toHexString(b & 0xFF);
|
|
|
+ if (hexStr.length() == 1) {
|
|
|
+ hex.append("0");
|
|
|
+ }
|
|
|
+ hex.append(hexStr);
|
|
|
+ }
|
|
|
+ return hex.toString();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成RSA密钥对,用于platformPublicKey和thirdPrivateKey
|
|
|
+ */
|
|
|
+ public static void generateRSAKeyPair() throws Exception {
|
|
|
+ KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
|
|
|
+ keyPairGenerator.initialize(2048); // 使用2048位密钥长度
|
|
|
+ KeyPair keyPair = keyPairGenerator.generateKeyPair();
|
|
|
+
|
|
|
+ PublicKey publicKey = keyPair.getPublic();
|
|
|
+ PrivateKey privateKey = keyPair.getPrivate();
|
|
|
+
|
|
|
+ // 获取Base64编码的公钥和私钥
|
|
|
+ String publicKeyStr = Base64.encodeBase64String(publicKey.getEncoded());
|
|
|
+ String privateKeyStr = Base64.encodeBase64String(privateKey.getEncoded());
|
|
|
+
|
|
|
+ System.out.println("公钥 (platformPublicKey):");
|
|
|
+ System.out.println(wrapPublicKey(publicKeyStr));
|
|
|
+ System.out.println();
|
|
|
+ System.out.println("私钥 (thirdPrivateKey):");
|
|
|
+ System.out.println(privateKeyStr);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 将公钥包装成PEM格式
|
|
|
+ */
|
|
|
+ private static String wrapPublicKey(String publicKey) {
|
|
|
+ return "-----BEGIN PUBLIC KEY-----\n" +
|
|
|
+ splitLines(publicKey) +
|
|
|
+ "\n-----END PUBLIC KEY-----";
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 将长Base64字符串按64字符分行
|
|
|
+ */
|
|
|
+ private static String splitLines(String base64Str) {
|
|
|
+ StringBuilder sb = new StringBuilder();
|
|
|
+ for (int i = 0; i < base64Str.length(); i += 64) {
|
|
|
+ if (sb.length() > 0) sb.append("\n");
|
|
|
+ sb.append(base64Str.substring(i, Math.min(i + 64, base64Str.length())));
|
|
|
+ }
|
|
|
+ return sb.toString();
|
|
|
+ }
|
|
|
+
|
|
|
+ // ------------------------------ 测试示例 ------------------------------
|
|
|
+ public static void main(String[] args) throws Exception {
|
|
|
+ // 1. 配置参数(替换为文档中的测试参数或实际环境参数)
|
|
|
+ String appSecret = "uuRKVwbfSutCpFTv"; // 文档中测试appsecret
|
|
|
+ String platformPublicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk2oCpi9eSMqcmqO3rrMN\n" +
|
|
|
+ "b9fqEFMckcRkU520PXyxFalzA2mfLNgNPe8TB9mBSqn7xPxLBBBOSaFwEAeAAs4E\n" +
|
|
|
+ "AojMVilbfrTXJh3ucyKlGvTpX3lMKgJaYG7asEzKVbw5V4j8oTBtNh3s20jP7zBm\n" +
|
|
|
+ "Y61opsLTIW0qiuvMxezJS8wHc6ajY99RHWBASd3AGozsc3cFLSgIUyh2BhcbB2l7\n" +
|
|
|
+ "V8UyDQj7+QI1BwNpkvGxYhP0DF4Yu7buaaf61gygkeeOZ/8JOjcDWBfVB6Q6wEja\n" +
|
|
|
+ "os0E9U+rYXN6v90va1wrRp+zRev4sMBWesvwc6uzcuYO/RfSJ19FDMUreCScm8tk\n" +
|
|
|
+ "PQIDAQAB"; // 文档中测试红杉密钥-公钥
|
|
|
+ String thirdPrivateKey = "MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCTagKmL15Iypyao7eusw1v1+oQUxyRxGRTnbQ9fLEVqXMDaZ8s2A097xMH2YFKqfvE/EsEEE5JoXAQB4ACzgQCiMxWKVt+tNcmHe5zIqUa9OlfeUwqAlpgbtqwTMpVvDlXiPyhMG02HezbSM/vMGZjrWimwtMhbSqK68zF7MlLzAdzpqNj31EdYEBJ3cAajOxzdwUtKAhTKHYGFxsHaXtXxTINCPv5AjUHA2mS8bFiE/QMXhi7tu5pp/rWDKCR545n/wk6NwNYF9UHpDrASNqizQT1T6thc3q/3S9rXCtGn7NF6/iwwFZ6y/Bzq7Ny5g79F9InX0UMxSt4JJyby2Q9AgMBAAECggEAEqfp6eo4vnGV3CQ4DM3wN2VV4/cAuJnoMITW2Kk9KAan3ZiyYlR9aIcnG2k1aaOVj1p2i+8cWUkrC3xHgRNdgoyZf5YAVErCp7pGASAzUPQJzOFm+DIQCgA9gO5W9P67Kw7VGfks+RpUbXQLjLPNYXQCuIgTfDl6ltY8the/ae4Y/a4dyhBhQ+Q/sygACk/Woiq7B78pc/qCaeMyZyik42aVuC7ZMuIgzERSOBFDrerI/upBWmLE4faeUk4h/PE1Hrj+H2RJ9aM5j4Idn/AJ4H4r8TjzT5m+ldQBfErs37HAVbyP1lQJjmO+VBBvqFchGNDtU1yGIRpxfU5mJI5dQQKBgQDRFPe+IKsheKNF2z0EUke/hU+XIDNc3Qwvp+2ok78G3LiKLRM61II/k+N0PLmJii0JEjqF0figj83Cyr4nnbhymOKIIplftgv085E1ed6QH0m54AwJBCLLA6gHiHin1t21Xh7o38FF6pHQ6EnQjfnVCODkTKxQEihvV3oAv/UBeQKBgQC0fnDPZ29YcIdI2W6xteDFaBfWLTw9usHDu2NnZBPEa+E5Whvcnw6xCgYEI+yasGEt9wOG6tnMEaxIMHRtn7LXaa7nrYxvqE5yTCLpjle3NM3KJRoCxZvOzJwlGWKFRPVpxBe1Sm0DB/IfTbBqzYCVkKQgpnqcpyI91TvxBL/r5QKBgA/bETaf75poNamUiLoNK1fA2lpRnNOMB+KNT56bJb91eaEw7eZmO0JrCrLD8CYYDnZDpaCEXeB/R1FgYq9KbLR0F6nPReZWPe3jkr2FcnVnigXIkeEVKTZQHqwDk3LW/pVEf/+VCGku8sPu+boRKkMXm0Z08hRYbCyVa7Em3YOxAoGAOomzfqC2TQGZ7reOHha1wnBjIrRjEEYsp5VzxMmBW7f9QMOHu8LeWe69SsR37Sd9LRIq06wBXRzyOit050TfFNwSvNLddC0q3AjzXbormqCGiaQEzpdWU/iqP6H/AOf/jADsC4EK3+vIy/w/VjQ2GsvhXzF/HKVcBp/Mo/t9Xz0CgYACKTI8f7z43g6MHy0+QOYBJ4BBDS8SzhLdNIXoUnc4O8kySTospxNVRzZGUMKJ4R8T7bpfR+50H6nCxBZo125eD4lcMHL3Dd/JwabKSer7Nzjt7+UMbgncQIs3m9FjtP1rvYuHvHI7RbxFH+hrYVWMKysWpbS9xtMThF9BosmufA=="; // 文档中测试三方密钥-私钥
|
|
|
+
|
|
|
+ // 2. 创建工具类实例
|
|
|
+ RedwoodCryptoUtil cryptoUtil = new RedwoodCryptoUtil(appSecret, platformPublicKey, thirdPrivateKey);
|
|
|
+
|
|
|
+ // 3. 加密请求参数示例(模拟购药问诊申请参数)
|
|
|
+ Map<String, Object> plainParam = new HashMap<>();
|
|
|
+ plainParam.put("prescription_type", 1);
|
|
|
+ plainParam.put("register_type", "picture");
|
|
|
+ plainParam.put("union_code", "TEST20250819001");
|
|
|
+ // 其他参数按接口要求补充...
|
|
|
+
|
|
|
+ Map<String, String> encryptedParam = cryptoUtil.encryptRequest(plainParam);
|
|
|
+ System.out.println("加密后的请求参数:" + encryptedParam);
|
|
|
+
|
|
|
+ // 4. 解密响应参数示例(模拟接口返回的加密数据)
|
|
|
+ Map<String, String> mockEncryptedResponse = new HashMap<>();
|
|
|
+ mockEncryptedResponse.put("content", "加密后的content");
|
|
|
+ mockEncryptedResponse.put("key", "加密后的key");
|
|
|
+ mockEncryptedResponse.put("sign", "加密后的sign");
|
|
|
+
|
|
|
+ Map<String, Object> plainResponse = cryptoUtil.decryptResponse(mockEncryptedResponse);
|
|
|
+ System.out.println("解密后的响应结果:" + plainResponse);
|
|
|
+ }
|
|
|
+}
|