From 7deee7d7861fc7bd567ef9f6f43e46616d4258dc Mon Sep 17 00:00:00 2001 From: wdc Date: Wed, 18 Dec 2024 10:29:28 +0800 Subject: [PATCH 1/5] =?UTF-8?q?feat=EF=BC=9A=E5=9F=BA=E4=BA=8E=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=BA=93=E8=87=AA=E5=AE=9A=E4=B9=89=E5=AF=86=E9=92=A5?= =?UTF-8?q?=E5=8A=A0=E8=BD=BD=E9=80=BB=E8=BE=91=E7=A4=BA=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 6 ++ yop-java-sdk-test/pom.xml | 5 + .../provider/db/YopDbCredentialsProvider.java | 99 +++++++++++++++++++ 3 files changed, 110 insertions(+) create mode 100644 yop-java-sdk-test/src/test/java/com/yeepay/yop/sdk/config/provider/db/YopDbCredentialsProvider.java diff --git a/pom.xml b/pom.xml index 7b318f95..e6bd1454 100644 --- a/pom.xml +++ b/pom.xml @@ -212,6 +212,12 @@ ${org.springframework} + + org.springframework + spring-context + ${org.springframework} + + org.bouncycastle bcprov-jdk15on diff --git a/yop-java-sdk-test/pom.xml b/yop-java-sdk-test/pom.xml index 279c82f9..6978abd0 100644 --- a/yop-java-sdk-test/pom.xml +++ b/yop-java-sdk-test/pom.xml @@ -24,6 +24,11 @@ + + org.springframework + spring-context + test + junit junit diff --git a/yop-java-sdk-test/src/test/java/com/yeepay/yop/sdk/config/provider/db/YopDbCredentialsProvider.java b/yop-java-sdk-test/src/test/java/com/yeepay/yop/sdk/config/provider/db/YopDbCredentialsProvider.java new file mode 100644 index 00000000..b1086d89 --- /dev/null +++ b/yop-java-sdk-test/src/test/java/com/yeepay/yop/sdk/config/provider/db/YopDbCredentialsProvider.java @@ -0,0 +1,99 @@ +/* + * Copyright: Copyright (c)2011 + * Company: 易宝支付(YeePay) + */ + +package com.yeepay.yop.sdk.config.provider.db; + +import com.yeepay.yop.sdk.auth.credentials.YopCredentials; +import com.yeepay.yop.sdk.auth.credentials.provider.YopCredentialsProviderRegistry; +import com.yeepay.yop.sdk.base.auth.credentials.provider.YopBaseCredentialsProvider; +import com.yeepay.yop.sdk.base.config.YopAppConfig; +import com.yeepay.yop.sdk.config.enums.CertStoreType; +import com.yeepay.yop.sdk.config.provider.file.YopCertConfig; +import com.yeepay.yop.sdk.security.CertTypeEnum; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import java.util.*; + +/** + * title: 示例:从数据库加载密钥配置
+ * description: 描述
+ * Copyright: Copyright (c)2014
+ * Company: 易宝支付(YeePay)
+ * + * @author wdc + * @version 1.0.0 + * @since 2024/12/12 + */ +@Component// 该注解依赖spring包, Maven坐标: org.springframework:spring-context:xxx +public class YopDbCredentialsProvider extends YopBaseCredentialsProvider { + + // 1、表结构简单设计 + // |-------id--------|appKey-------|-----certType-----|-----certValue-----| + // | 1 |app_001|RSA2048|xxxxxxxxxx| + // | 2 |app_002|SM2|xxxxxxxxxx| + + // 2、此处模拟数据库存储 + Map mockDbStore = new HashMap<>(); + { + mockDbStore.put("app_001", new String[]{"1", "app_001", "RSA2048", "app_001的密钥串xxxxxx"}); + mockDbStore.put("app_002", new String[]{"2", "app_002", "SM2", "app_002的密钥串xxxxxx"}); + } + + // 3、借助spring 方式, 在provider构造好后,保证系统启动时注册该自定义provider为单例 + @PostConstruct + public void init() { + YopCredentialsProviderRegistry.registerProvider(this); + } + + // 4、指定默认的appKey,当请求时没指定appKey时,会使用该appKey + @Override + public String getDefaultAppKey() { + // 此处简单硬编码,可以根据自身情况指定 + return "app_001"; + } + + // 5、模拟从数据库查询 + private YopCertConfig mockFindCertConfigByAppKey(String appKey) { + YopCertConfig yopCertConfig = new YopCertConfig(); + String[] dbRow = mockDbStore.get(appKey); + yopCertConfig.setStoreType(CertStoreType.STRING); + yopCertConfig.setCertType(CertTypeEnum.parse(dbRow[2])); + yopCertConfig.setValue(dbRow[3]); + return yopCertConfig; + } + + // 6、实现根据appKey和certType,构造调用身份 + @Override + public YopCredentials getCredentials(String appKey, String credentialType) { + // 兼容默认appKey的场景 + appKey = useDefaultIfBlank(appKey); + + // 构造调用身份 + YopAppConfig yopAppConfig = new YopAppConfig(); + yopAppConfig.setAppKey(appKey); + + // 从数据库加载密钥配置 + YopCertConfig yopCertConfig = mockFindCertConfigByAppKey(appKey); + + // 装载调用身份 + List isvPrivateKeys = new LinkedList<>(); + isvPrivateKeys.add(yopCertConfig); + yopAppConfig.setIsvPrivateKey(isvPrivateKeys); + return buildCredentials(yopAppConfig, credentialType); + } + + + // 7、实现支持的密钥类型,此处根据实际情况返回即可 + @Override + public List getSupportCertTypes(String appKey) { + Set result = new HashSet<>(); + // 从数据库加载支持的密钥类型列表 + for (Map.Entry appCert : mockDbStore.entrySet()) { + result.add(CertTypeEnum.parse(appCert.getValue()[2])); + } + return new ArrayList<>(result); + } +} From 566b2c7b2f1a5d5671e6b0cfe09ea0ee76d55a13 Mon Sep 17 00:00:00 2001 From: wdc Date: Fri, 27 Dec 2024 10:23:34 +0800 Subject: [PATCH 2/5] =?UTF-8?q?feat=EF=BC=9Asdk=E9=A2=9D=E5=A4=96=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E7=A4=BA=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/CustomFixedSdkConfigProvider.java | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/yop-java-sdk-test/src/test/java/com/yeepay/yop/sdk/service/CustomFixedSdkConfigProvider.java b/yop-java-sdk-test/src/test/java/com/yeepay/yop/sdk/service/CustomFixedSdkConfigProvider.java index f26f7a77..0c015bc3 100644 --- a/yop-java-sdk-test/src/test/java/com/yeepay/yop/sdk/service/CustomFixedSdkConfigProvider.java +++ b/yop-java-sdk-test/src/test/java/com/yeepay/yop/sdk/service/CustomFixedSdkConfigProvider.java @@ -4,9 +4,11 @@ */ package com.yeepay.yop.sdk.service; -import com.yeepay.yop.sdk.YopConstants; import com.yeepay.yop.sdk.base.config.provider.YopFixedSdkConfigProvider; import com.yeepay.yop.sdk.config.YopSdkConfig; +import com.yeepay.yop.sdk.config.provider.file.YopProxyConfig; + +import java.util.Arrays; /** * title: 自定义SDK配置
@@ -23,10 +25,20 @@ public class CustomFixedSdkConfigProvider extends YopFixedSdkConfigProvider { @Override protected YopSdkConfig loadSdkConfig() { YopSdkConfig yopSdkConfig = new YopSdkConfig(); - // 可以根据当前环境(测试/灰度/生产等等),自定义其他请求根路径 - yopSdkConfig.setServerRoot(YopConstants.DEFAULT_SERVER_ROOT); - yopSdkConfig.setYosServerRoot(YopConstants.DEFAULT_YOS_SERVER_ROOT); - yopSdkConfig.setSandboxServerRoot(YopConstants.DEFAULT_SANDBOX_SERVER_ROOT); + + // 示例:普通接口请求端点地址,配置多个时,遇到故障可以自动切换 + yopSdkConfig.setPreferredServerRoots(Arrays.asList("https://域名A","https://域名B")); + // 示例:文件上传、下载类接口请求端点地址 + yopSdkConfig.setYosServerRoot("https://域名C"); + // 示例:沙箱环境请求地址 + yopSdkConfig.setSandboxServerRoot("https://域名D"); + + // 示例:请求代理配置 + final YopProxyConfig proxyConfig = new YopProxyConfig(); + proxyConfig.setHost("xxx"); + proxyConfig.setPort(1234); + yopSdkConfig.setProxy(proxyConfig); + // 连接超时时间、读取超时时间等其他配置,可根据需要setXXX即可 return yopSdkConfig; } From 5eafe954332e299d635f581bdf56423f97249b94 Mon Sep 17 00:00:00 2001 From: wdc Date: Wed, 15 Jan 2025 14:35:06 +0800 Subject: [PATCH 3/5] =?UTF-8?q?feat=EF=BC=9A=E6=96=87=E4=BB=B6=E4=B8=8A?= =?UTF-8?q?=E4=BC=A0=E7=A4=BA=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yeepay/yop/sdk/utils/YopSdkInitUtils.java | 13 ++++ .../com/yeepay/yop/sdk/doc/ExampleTest.java | 25 +++++-- .../yop/sdk/example/YopRsaEncryptExample.java | 66 ++++++++++++++++++- 3 files changed, 99 insertions(+), 5 deletions(-) diff --git a/yop-java-sdk-base/src/main/java/com/yeepay/yop/sdk/utils/YopSdkInitUtils.java b/yop-java-sdk-base/src/main/java/com/yeepay/yop/sdk/utils/YopSdkInitUtils.java index fafb2f69..3d831180 100644 --- a/yop-java-sdk-base/src/main/java/com/yeepay/yop/sdk/utils/YopSdkInitUtils.java +++ b/yop-java-sdk-base/src/main/java/com/yeepay/yop/sdk/utils/YopSdkInitUtils.java @@ -4,6 +4,10 @@ */ package com.yeepay.yop.sdk.utils; +import com.yeepay.yop.sdk.service.common.YopClientBuilder; +import com.yeepay.yop.sdk.service.common.YopClientImpl; +import com.yeepay.yop.sdk.service.common.request.YopRequest; +import com.yeepay.yop.sdk.service.common.response.YopResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -63,4 +67,13 @@ private static void doLoadSpiClass(String spiClass) { } } + public static void main(String[] args) { + YopClientImpl yopClient = YopClientBuilder.builder().build(); + final YopRequest yopRequest = new YopRequest("/rest/v2.0/yop/platform/certs", "GET"); + yopRequest.getRequestConfig().setAppKey("您的国密appKey"); + yopRequest.addParameter("certType", "SM2"); + final YopResponse response = yopClient.request(yopRequest); + assert null != response; + } + } diff --git a/yop-java-sdk-test/src/test/java/com/yeepay/yop/sdk/doc/ExampleTest.java b/yop-java-sdk-test/src/test/java/com/yeepay/yop/sdk/doc/ExampleTest.java index a3fcde0a..6b0fc034 100644 --- a/yop-java-sdk-test/src/test/java/com/yeepay/yop/sdk/doc/ExampleTest.java +++ b/yop-java-sdk-test/src/test/java/com/yeepay/yop/sdk/doc/ExampleTest.java @@ -17,10 +17,14 @@ import com.yeepay.yop.sdk.service.common.request.YopRequest; import com.yeepay.yop.sdk.service.common.response.YopResponse; import com.yeepay.yop.sdk.service.common.response.YosUploadResponse; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.exception.ExceptionUtils; import org.junit.Ignore; import org.junit.Test; import java.security.PrivateKey; +import java.util.Arrays; +import java.util.stream.Collectors; /** * title:
@@ -84,13 +88,26 @@ appKey, new PKICredentialsItem(getPrivateKey( "MIGTAgEAMBMGByqGSM49AgEGCCqBHM9VAYItBHkwdwIBAQQg/WsUu5NQDTDJjjaXWLlNfBNZhamXAqCLcyLPSHDSD4qgCgYIKoEcz1UBgi2hRANCAAQUC8TdvSHnCXGlQzm62w+sqHK8wt/ZDXmuhyU4qOEJ8jRMiTzQWoX8BC0fB7ggzWIobHrJouBgnEm3AxVhShpZ", CertTypeEnum.SM2) , CertTypeEnum.SM2))); - request.addParameter("parentMerchantNo", "1234321"); - request.addParameter("orderId", "1234321"); - request.addParameter("orderAmount", "100.05"); - request.addParameter("notifyUrl", "https://xxx.com/notify"); + try { + request.addParameter("parentMerchantNo", "1234321"); + request.addParameter("orderId", "1234321"); + request.addParameter("orderAmount", "100.05"); + request.addParameter("notifyUrl", "https://xxx.com/notify"); + } catch (Exception e) { + throw new RuntimeException(e); + } assertTheRequest(yopClient, request); } + @Test + public void testCaused() { + try { + throw new RuntimeException("1", new RuntimeException("2", new RuntimeException("3"))); + } catch (RuntimeException e) { + System.out.println(Arrays.stream(ExceptionUtils.getThrowables(e)).map(t -> t.getClass().getCanonicalName() + ":" + t.getMessage()).collect(Collectors.joining(","))); + } + } + @Test public void singleUpload() { System.setProperty("yop.sdk.http", "true");//2021-12-08T11:59:16Z,d48782ac-93c1-466e-b417-f7a71e4965f0 diff --git a/yop-java-sdk-test/src/test/java/com/yeepay/yop/sdk/example/YopRsaEncryptExample.java b/yop-java-sdk-test/src/test/java/com/yeepay/yop/sdk/example/YopRsaEncryptExample.java index 3650f012..623d3c63 100644 --- a/yop-java-sdk-test/src/test/java/com/yeepay/yop/sdk/example/YopRsaEncryptExample.java +++ b/yop-java-sdk-test/src/test/java/com/yeepay/yop/sdk/example/YopRsaEncryptExample.java @@ -17,10 +17,14 @@ import org.apache.http.HttpStatus; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.client.methods.RequestBuilder; import org.apache.http.client.utils.HttpClientUtils; +import org.apache.http.entity.ContentType; import org.apache.http.entity.InputStreamEntity; +import org.apache.http.entity.mime.HttpMultipartMode; +import org.apache.http.entity.mime.MultipartEntityBuilder; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.joda.time.DateTime; @@ -33,6 +37,7 @@ import javax.crypto.KeyGenerator; import javax.crypto.spec.SecretKeySpec; import java.io.*; +import java.net.URL; import java.net.URLEncoder; import java.nio.file.Files; import java.security.*; @@ -95,7 +100,7 @@ public static void main(String[] args) throws Exception { // 加密会话密钥,可每笔调用都生成,也可以多笔公用一个,建议定时更换 String encryptKey = encodeUrlSafeBase64(generateRandomKey()); -// // get请求,form参数 + // get请求,form参数 getFormExample(YopRequestMethod.GET, "/rest/v1.0/test/errorcode2", YopRequestContentType.FORM_URL_ENCODE, encryptKey); @@ -103,6 +108,10 @@ public static void main(String[] args) throws Exception { postFormExample(YopRequestMethod.POST, "/rest/v1.0/test/old-api-mgr/find-api-by-uri", YopRequestContentType.FORM_URL_ENCODE, encryptKey); + // post请求,文件参数 + postMultipartFormExample(YopRequestMethod.POST, "/yos/v1.0/sys/merchant/qual/upload", + YopRequestContentType.MULTIPART_FORM, encryptKey); + // post请求,json参数 postJsonExample(YopRequestMethod.POST, "/rest/v1.0/test-wdc/test/http-json/test", YopRequestContentType.JSON, encryptKey); @@ -204,6 +213,61 @@ private static void postFormExample(YopRequestMethod requestMethod, String reque } } + private static void postMultipartFormExample(YopRequestMethod requestMethod, String requestUri, + YopRequestContentType requestContentType, String encryptKey) throws Exception { + + + // 普通参数参与签名,clientId + Multimap params = ArrayListMultimap.create(); + params.put("clientId", APP_KEY); + + // 参数不加密 + Set encryptHeaders = Collections.emptySet(); + Set encryptParams = Sets.newHashSet(); + + // 请求头 + Map headers = buildHeaders(requestMethod, requestUri, params, + requestContentType, "", encryptKey, encryptHeaders, encryptParams); + + // 构造http请求 + HttpPost postMethod = new HttpPost(SERVER_ROOT + requestUri); + MultipartEntityBuilder builder = MultipartEntityBuilder.create(); + builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE); + + // 添加普通参数 + for (Map.Entry> entry : params.asMap().entrySet()) { + String paramKey = entry.getKey(); + for (String value : entry.getValue()) { + builder.addTextBody(normalize(paramKey), normalize(value)); + } + } + + // 添加文件参数,merQual + // 方式一:本地文件流 +// builder.addBinaryBody("merQual", new FileInputStream("abc.txt"), +// ContentType.DEFAULT_BINARY, "abc.txt"); + // 方式二:远程文件流 + builder.addBinaryBody("merQual", new URL("https://open.yeepay.com/apis/docs/apis/common/ALL.json").openStream(), + ContentType.DEFAULT_BINARY, "abc.txt"); + + postMethod.setEntity(builder.build()); + + // 添加请求头 + for (Map.Entry entry : headers.entrySet()) { + postMethod.addHeader(entry.getKey(), entry.getValue()); + } + + // 发起http调用 + CloseableHttpResponse response = null; + try { + response = httpClient.execute(postMethod); + handleResponse(YopRequestType.WEB, response, encryptKey); + } finally { + HttpClientUtils.closeQuietly(response); + } + + } + private static void postJsonExample(YopRequestMethod requestMethod, String requestUri, YopRequestContentType requestContentType, String encryptKey) throws Exception { From e12e60a84d28c383e5ac08f0b251f757d411ce4a Mon Sep 17 00:00:00 2001 From: wdc Date: Fri, 24 Jan 2025 13:37:07 +0800 Subject: [PATCH 4/5] =?UTF-8?q?feat=EF=BC=9A=E5=A2=9E=E5=8A=A0=E5=9B=BD?= =?UTF-8?q?=E5=AF=86=E7=AE=97=E6=B3=95=E7=A4=BA=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/yeepay/yop/sdk/YopConstants.java | 1 + .../gm/security/encrypt/YopSm4Encryptor.java | 4 +- .../com/yeepay/yop/sdk/gm/utils/Sm2Utils.java | 64 + yop-java-sdk-test/pom.xml | 6 + .../yop/sdk/example/YopRsaEncryptExample.java | 152 ++- .../yop/sdk/example/YopSM2EncryptExample.java | 1171 +++++++++++++++++ 6 files changed, 1355 insertions(+), 43 deletions(-) create mode 100644 yop-java-sdk-test/src/test/java/com/yeepay/yop/sdk/example/YopSM2EncryptExample.java diff --git a/yop-java-sdk-base/src/main/java/com/yeepay/yop/sdk/YopConstants.java b/yop-java-sdk-base/src/main/java/com/yeepay/yop/sdk/YopConstants.java index 1ec60045..e74abcb7 100644 --- a/yop-java-sdk-base/src/main/java/com/yeepay/yop/sdk/YopConstants.java +++ b/yop-java-sdk-base/src/main/java/com/yeepay/yop/sdk/YopConstants.java @@ -86,6 +86,7 @@ public interface YopConstants { */ String YOP_ENCRYPT_V1 = "yop-encrypt-v1"; String SM4_CBC_PKCS5PADDING = "SM4/CBC/PKCS5Padding"; + String SM4_ECB_PKCS5PADDING = "SM4/ECB/PKCS5Padding"; String SM2 = "SM2"; String AES = "AES"; String AES_ECB_PKCS5PADDING = "AES/ECB/PKCS5Padding"; diff --git a/yop-java-sdk-crypto-gm/src/main/java/com/yeepay/yop/sdk/gm/security/encrypt/YopSm4Encryptor.java b/yop-java-sdk-crypto-gm/src/main/java/com/yeepay/yop/sdk/gm/security/encrypt/YopSm4Encryptor.java index c815ce9e..76f62a0f 100644 --- a/yop-java-sdk-crypto-gm/src/main/java/com/yeepay/yop/sdk/gm/security/encrypt/YopSm4Encryptor.java +++ b/yop-java-sdk-crypto-gm/src/main/java/com/yeepay/yop/sdk/gm/security/encrypt/YopSm4Encryptor.java @@ -30,6 +30,7 @@ import java.util.Map; import static com.yeepay.yop.sdk.YopConstants.SM4_CBC_PKCS5PADDING; +import static com.yeepay.yop.sdk.YopConstants.SM4_ECB_PKCS5PADDING; /** * title: 对称加密器
@@ -61,6 +62,7 @@ protected Map initialValue() { Map map = Maps.newHashMap(); try { map.put(SM4_CBC_PKCS5PADDING, Cipher.getInstance(SM4_CBC_PKCS5PADDING, BouncyCastleProvider.PROVIDER_NAME)); + map.put(SM4_ECB_PKCS5PADDING, Cipher.getInstance(SM4_ECB_PKCS5PADDING, BouncyCastleProvider.PROVIDER_NAME)); map.put(ALGORITHM_NAME_GCM_NOPADDING, Cipher.getInstance(ALGORITHM_NAME_GCM_NOPADDING, BouncyCastleProvider.PROVIDER_NAME)); } catch (Exception e) { throw new YopClientException("SystemError, YopSm4Encryptor InitFail, ex:", e); @@ -71,7 +73,7 @@ protected Map initialValue() { @Override public List supportedAlgs() { - return Arrays.asList(SM4_CBC_PKCS5PADDING, ALGORITHM_NAME_GCM_NOPADDING); + return Arrays.asList(SM4_CBC_PKCS5PADDING, SM4_ECB_PKCS5PADDING, ALGORITHM_NAME_GCM_NOPADDING); } @Override diff --git a/yop-java-sdk-crypto-gm/src/main/java/com/yeepay/yop/sdk/gm/utils/Sm2Utils.java b/yop-java-sdk-crypto-gm/src/main/java/com/yeepay/yop/sdk/gm/utils/Sm2Utils.java index 8294b06d..701a4a4f 100644 --- a/yop-java-sdk-crypto-gm/src/main/java/com/yeepay/yop/sdk/gm/utils/Sm2Utils.java +++ b/yop-java-sdk-crypto-gm/src/main/java/com/yeepay/yop/sdk/gm/utils/Sm2Utils.java @@ -180,6 +180,25 @@ public static boolean verifySign(String data, String signature, BCECPublicKey pu } } + /** + * sm2密钥进行签名验证-withId + * + * @param data + * @param signature + * @param publicKey + * @return + */ + public static boolean verifySignWithId(String data, String signature, BCECPublicKey publicKey, byte[] withId) { + try { + byte[] signByte = Encodes.decodeBase64(signature); + byte[] dataByte = data.getBytes(Charsets.UTF_8); + return verifyWithId(publicKey, dataByte, encodeSM2SignToDER(signByte), withId); + } catch (IOException e) { + throw new YopClientException("UnexpectedError, VerifySign Fail, data:" + + data + ", sign:" + signature + ", key:" + publicKey + ", ex:", e); + } + } + /** * 把64字节的纯R+S字节数组编码成DER编码 * @@ -232,6 +251,35 @@ public static byte[] sign(BCECPrivateKey priKey, byte[] srcData) throws CryptoEx return rawSign; } + /** + * 签名-withId + * + * @param priKey 私钥 + * @param srcData 原文 + * @return 64字节的纯R+S字节流 + * @throws CryptoException + */ + public static byte[] signWithId(BCECPrivateKey priKey, byte[] srcData, byte[] withId) throws CryptoException { + ECParameterSpec parameterSpec = priKey.getParameters(); + ECDomainParameters domainParameters = new ECDomainParameters(parameterSpec.getCurve(), parameterSpec.getG(), + parameterSpec.getN(), parameterSpec.getH()); + ECPrivateKeyParameters priKeyParameters = new ECPrivateKeyParameters(priKey.getD(), domainParameters); + //der编码后的签名值 + byte[] derSign = sign(priKeyParameters, withId, srcData); + + //der解码过程 + ASN1Sequence as = DERSequence.getInstance(derSign); + byte[] rBytes = ((ASN1Integer) as.getObjectAt(0)).getValue().toByteArray(); + byte[] sBytes = ((ASN1Integer) as.getObjectAt(1)).getValue().toByteArray(); + //由于大数的补0规则,所以可能会出现33个字节的情况,要修正回32个字节 + rBytes = fixToCurveLengthBytes(rBytes); + sBytes = fixToCurveLengthBytes(sBytes); + byte[] rawSign = new byte[rBytes.length + sBytes.length]; + System.arraycopy(rBytes, 0, rawSign, 0, rBytes.length); + System.arraycopy(sBytes, 0, rawSign, rBytes.length, sBytes.length); + return rawSign; + } + /** * 签名 * @@ -272,6 +320,22 @@ public static boolean verify(BCECPublicKey pubKey, byte[] srcData, byte[] sign) return verify(pubKeyParameters, null, srcData, sign); } + /** + * 验签 + * + * @param pubKey 公钥 + * @param srcData 原文 + * @param sign DER编码的签名值 + * @return + */ + public static boolean verifyWithId(BCECPublicKey pubKey, byte[] srcData, byte[] sign, byte[] withId) { + ECParameterSpec parameterSpec = pubKey.getParameters(); + ECDomainParameters domainParameters = new ECDomainParameters(parameterSpec.getCurve(), parameterSpec.getG(), + parameterSpec.getN(), parameterSpec.getH()); + ECPublicKeyParameters pubKeyParameters = new ECPublicKeyParameters(pubKey.getQ(), domainParameters); + return verify(pubKeyParameters, withId, srcData, sign); + } + /** * 验签 * diff --git a/yop-java-sdk-test/pom.xml b/yop-java-sdk-test/pom.xml index 6978abd0..f927bc28 100644 --- a/yop-java-sdk-test/pom.xml +++ b/yop-java-sdk-test/pom.xml @@ -49,6 +49,12 @@ jmh-generator-annprocess test
+ + cn.hutool + hutool-all + 5.1.5 + test + \ No newline at end of file diff --git a/yop-java-sdk-test/src/test/java/com/yeepay/yop/sdk/example/YopRsaEncryptExample.java b/yop-java-sdk-test/src/test/java/com/yeepay/yop/sdk/example/YopRsaEncryptExample.java index 623d3c63..7fa80160 100644 --- a/yop-java-sdk-test/src/test/java/com/yeepay/yop/sdk/example/YopRsaEncryptExample.java +++ b/yop-java-sdk-test/src/test/java/com/yeepay/yop/sdk/example/YopRsaEncryptExample.java @@ -100,12 +100,14 @@ public static void main(String[] args) throws Exception { // 加密会话密钥,可每笔调用都生成,也可以多笔公用一个,建议定时更换 String encryptKey = encodeUrlSafeBase64(generateRandomKey()); + // 注意:以下分别展示了不同请求方式的参数构造、加密、解密、验签的完整流程,请根据接口实际情况进行选择 + // get请求,form参数 getFormExample(YopRequestMethod.GET, "/rest/v1.0/test/errorcode2", YopRequestContentType.FORM_URL_ENCODE, encryptKey); // post请求,form参数 - postFormExample(YopRequestMethod.POST, "/rest/v1.0/test/old-api-mgr/find-api-by-uri", + postFormExample(YopRequestMethod.POST, "/rest/v1.0/frontcashier/agreement/sign/confirm", YopRequestContentType.FORM_URL_ENCODE, encryptKey); // post请求,文件参数 @@ -125,18 +127,20 @@ private static void getFormExample(YopRequestMethod requestMethod, String reques YopRequestContentType requestContentType, String encryptKey) throws Exception { // 请求参数 - String paramKey = "errorCode"; - String paramPlainValue = "000027", - paramEncryptValue = encryptParam(encryptKey, paramPlainValue); Multimap formParams = ArrayListMultimap.create(); - // 参数不加密 -// formParams.put(paramKey, paramPlainValue); - - // 参数加密 - formParams.put(paramKey, paramEncryptValue); Set encryptHeaders = Collections.emptySet(); Set encryptParams = Sets.newHashSet(); - encryptParams.add(paramKey); + + // 注意:以下两个代码块分别展示了加密、不加密两种示例,请根据情况选择其一 + + // 参数不加密-------------->开始 +// formParams.put("errorCode", "000027"); + // 参数不加密-------------->结束 + + // 参数加密-------------->开始 + formParams.put("errorCode", encryptParam(encryptKey, "000027")); + encryptParams.add("errorCode"); + // 参数加密-------------->结束 // 请求头 Map headers = buildHeaders(requestMethod, requestUri, formParams, @@ -182,18 +186,30 @@ private static void postFormExample(YopRequestMethod requestMethod, String reque String encryptKey) throws Exception { // 请求参数 - String paramKey = "apiUri"; - String paramPlainValue = "/rest/v1.0/test/product/find/lookatdoc", - paramEncryptValue = encryptParam(encryptKey, paramPlainValue); Multimap params = ArrayListMultimap.create(); - // 参数不加密 -// params.put(paramKey, paramPlainValue); - - // 参数加密 - params.put(paramKey, paramEncryptValue); Set encryptHeaders = Collections.emptySet(); Set encryptParams = Sets.newHashSet(); - encryptParams.add(paramKey); + + // 注意:以下两个代码块分别展示了加密、不加密两种示例,请根据情况选择其一 + + // 参数不加密-------------->开始 +// params.put("parentMerchant", "10080041523"); +// params.put("merchantNo", "10080041523"); +// params.put("smsCode", "11111131"); +// params.put("merchantFlowId", "11111131"); + // 参数不加密-------------->结束 + + + // 参数加密-------------->开始 + params.put("parentMerchant", encryptParam(encryptKey , "10080041523" )); + params.put("merchantNo", encryptParam(encryptKey , "10080041523" )); + params.put("smsCode", encryptParam(encryptKey , "11111131" )); + params.put("merchantFlowId", encryptParam(encryptKey , "111111" )); + encryptParams.add("parentMerchant"); + encryptParams.add("merchantNo"); + encryptParams.add("smsCode"); + encryptParams.add("merchantFlowId"); + // 参数加密-------------->结束 // 请求头 Map headers = buildHeaders(requestMethod, requestUri, params, @@ -219,12 +235,20 @@ private static void postMultipartFormExample(YopRequestMethod requestMethod, Str // 普通参数参与签名,clientId Multimap params = ArrayListMultimap.create(); - params.put("clientId", APP_KEY); - - // 参数不加密 Set encryptHeaders = Collections.emptySet(); Set encryptParams = Sets.newHashSet(); + // 注意:以下两个代码块分别展示了加密、不加密两种示例,请根据情况选择其一 + + // 参数不加密--------------->开始 +// params.put("clientId", APP_KEY); + // 参数不加密--------------->结束 + + // 参数加密--------------->开始 + params.put("clientId", encryptParam(encryptKey, APP_KEY)); + encryptParams.add("clientId"); + // 参数加密--------------->结束 + // 请求头 Map headers = buildHeaders(requestMethod, requestUri, params, requestContentType, "", encryptKey, encryptHeaders, encryptParams); @@ -242,6 +266,7 @@ private static void postMultipartFormExample(YopRequestMethod requestMethod, Str } } + // 注意:以下两个代码块分别展示了本地文件、远程文件两种示例,请根据情况选择其一 // 添加文件参数,merQual // 方式一:本地文件流 // builder.addBinaryBody("merQual", new FileInputStream("abc.txt"), @@ -271,8 +296,10 @@ private static void postMultipartFormExample(YopRequestMethod requestMethod, Str private static void postJsonExample(YopRequestMethod requestMethod, String requestUri, YopRequestContentType requestContentType, String encryptKey) throws Exception { - // 请求参数 - String plainJsonContent = ("{\n" + + Set encryptHeaders = Collections.emptySet(); + Set encryptParams = Sets.newHashSet(); + + String plainJsonContent = "{\n" + " \"arg1\" : {\n" + " \"appId\" : \"app_1111111111\",\n" + " \"customerNo\" : \"333333333\"\n" + @@ -281,17 +308,20 @@ private static void postJsonExample(YopRequestMethod requestMethod, String reque " \"string\" : \"hello\",\n" + " \"array\" : [ \"test\" ]\n" + " }\n" + - "}"), + "}", encryptJsonContent = encryptParam(encryptKey, plainJsonContent); + // 注意:以下两个代码块分别展示了加密、不加密两种示例,请根据情况选择其一 String finalJsonContent - // 不加密 -// = plainJsonContent; - // 加密 - = encryptJsonContent; - Set encryptHeaders = Collections.emptySet(); - Set encryptParams = Sets.newHashSet(); - encryptParams.add("$");// 目前json推荐整体加密 + // 不加密------------------->开始 +// = plainJsonContent; + // 不加密------------------->结束 + + + // 加密-------------------->开始 + = encryptJsonContent; + encryptParams.add("$"); // 目前json推荐整体加密,$代表整体加密 + // 加密-------------------->结束 // 请求头 // 目前不允许json接口带有form参数,置空即可 @@ -316,21 +346,23 @@ private static void postJsonExample(YopRequestMethod requestMethod, String reque private static void downloadExample(YopRequestMethod requestMethod, String requestUri, YopRequestContentType requestContentType, String encryptKey) throws Exception { - // 根据实际情况来,也可能是post方法,请求报文可能是form,也可能是json,可参考其他方式的入参处理 + // 根据实际情况来,【文件下载接口】可能是post方法,请求报文可能是form,也可能是json,可参考其他方式的入参处理 // 此处仅演示文件响应流的处理 + // 请求参数 - String paramKey = "fileName"; - String paramPlainValue = "wym-test.txt", - paramEncryptValue = encryptParam(encryptKey, paramPlainValue); Multimap params = ArrayListMultimap.create(); - // 不加密 -// params.put(paramKey, paramPlainValue); - - // 加密 - params.put(paramKey, paramEncryptValue); Set encryptHeaders = Collections.emptySet(); Set encryptParams = Sets.newHashSet(); - encryptParams.add(paramKey); + + // 注意:以下两个代码块分别展示了加密、不加密两种示例,请根据情况选择其一 + // 不加密------------------->开始 +// params.put("fileName", "wym-test.txt"); + // 不加密------------------->结束 + + // 加密--------------------->开始 + params.put("fileName", encryptParam(encryptKey, "wym-test.txt")); + encryptParams.add("fileName"); + // 加密--------------------->结束 // 请求头 Map headers = buildHeaders(requestMethod, requestUri, params, @@ -406,6 +438,40 @@ private static byte[] sign(byte[] data) { } } + private static void verifyResponseSign(String signHeader, String content) throws UnsupportedEncodingException { + if (StringUtils.isBlank(signHeader) || StringUtils.isBlank(content)) { + System.out.println("Response Sign Skip, signature:" + signHeader + ", content:" + content); + return; + } + String signContent = content.replaceAll("[ \t\n]", ""); + System.out.println("Response Sign Begin, signature:" + signHeader + ", content:" + signContent); + if (!verifySign(signContent.getBytes(DEFAULT_ENCODING), decodeBase64(signHeader), string2PublicKey(YOP_PUBLIC_KEY))) { + System.out.println("Response Sign Verify Fail"); + } else { + System.out.println("Response Sign Verify Success"); + } + } + + /** + * 验证签名 + * + * @param data 数据 + * @param sign 签名 + * @param publicKey 公钥 + * @return boolean + */ + public static boolean verifySign(byte[] data, byte[] sign, PublicKey publicKey) { + try { + Signature signature = Signature.getInstance(DEFAULT_SIGN_ALG); + signature.initVerify(publicKey); + signature.update(data); + return signature.verify(sign); + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + private static PrivateKey string2PrivateKey(String priKey) { try { return KeyFactory.getInstance(RSA).generatePrivate( @@ -777,6 +843,7 @@ private static void handleResponse(YopRequestType requestType, if (isJsonResponse(httpResponse)) { String content = IOUtils.toString(entity.getContent(), DEFAULT_ENCODING); System.out.println("Request success, response:" + content); + verifyResponseSign(signHeader, content); final JsonNode bizData = OBJECT_MAPPER.readTree(content).get("result"); if (isEncryptResponse) { System.out.println("Response decrypt success, bizData:" + decryptParam(encryptKey, bizData.asText())); @@ -801,6 +868,7 @@ private static void handleResponse(YopRequestType requestType, if (entity.getContent() != null) { String content = IOUtils.toString(entity.getContent(), DEFAULT_ENCODING); System.out.println("Request Fail, response:" + content); + verifyResponseSign(signHeader, content); return; } else { throw new RuntimeException("ResponseError, Empty Content, httpStatusCode:" + httpResponse.getStatusLine().getStatusCode()); diff --git a/yop-java-sdk-test/src/test/java/com/yeepay/yop/sdk/example/YopSM2EncryptExample.java b/yop-java-sdk-test/src/test/java/com/yeepay/yop/sdk/example/YopSM2EncryptExample.java new file mode 100644 index 00000000..37f095a9 --- /dev/null +++ b/yop-java-sdk-test/src/test/java/com/yeepay/yop/sdk/example/YopSM2EncryptExample.java @@ -0,0 +1,1171 @@ +/* + * Copyright: Copyright (c)2014 + * Company: 易宝支付(YeePay) + */ +package com.yeepay.yop.sdk.example; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.Joiner; +import com.google.common.collect.*; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.codec.binary.Hex; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.Header; +import org.apache.http.HttpEntity; +import org.apache.http.HttpStatus; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.client.methods.RequestBuilder; +import org.apache.http.client.utils.HttpClientUtils; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.InputStreamEntity; +import org.apache.http.entity.mime.HttpMultipartMode; +import org.apache.http.entity.mime.MultipartEntityBuilder; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.bouncycastle.asn1.*; +import org.bouncycastle.crypto.CipherParameters; +import org.bouncycastle.crypto.CryptoException; +import org.bouncycastle.crypto.engines.SM2Engine; +import org.bouncycastle.crypto.params.*; +import org.bouncycastle.crypto.signers.SM2Signer; +import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey; +import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.jce.spec.ECParameterSpec; +import org.bouncycastle.math.ec.ECPoint; +import org.bouncycastle.math.ec.custom.gm.SM2P256V1Curve; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.joda.time.format.DateTimeFormatter; +import org.joda.time.format.ISODateTimeFormat; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.crypto.Cipher; +import javax.crypto.CipherInputStream; +import javax.crypto.KeyGenerator; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.io.*; +import java.math.BigInteger; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.file.Files; +import java.security.*; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.*; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * title: SM2 加密示例
+ * description: 描述
+ * Copyright: Copyright (c)2014
+ * Company: 易宝支付(YeePay)
+ * + * @author wdc + * @version 1.0.0 + * @since 2023/9/27 + */ +public class YopSM2EncryptExample { + + private static final Logger LOGGER = LoggerFactory.getLogger(YopSM2EncryptExample.class); + static { + try { + if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) { + Security.addProvider(new BouncyCastleProvider()); + } + LOGGER.debug("BouncyCastleProvider added"); + } catch (Exception e) { + LOGGER.warn("error when add BouncyCastleProvider", e); + } + } + + private static final CloseableHttpClient httpClient; + static { + // 自定义连接池大小、默认超时时间 + httpClient = HttpClients.custom() + .setMaxConnPerRoute(100) + .setMaxConnTotal(200) + .setDefaultRequestConfig( + RequestConfig.custom() + .setConnectionRequestTimeout(3000) + .setConnectTimeout(3000) + .setSocketTimeout(30000) + .build()) + .build(); + } + + private static final String APP_KEY = "sandbox_sm_10080041523"; + private static final BCECPrivateKey ISV_PRIVATE_KEY = string2PrivateKey("MIGTAgEAMBMGByqGSM49AgEGCCqBHM9VAYItBHkwdwIBAQQgr0mQ3/jjQOczWI6bnJFdqF4D/DFYHaqXftqXU/jGKpCgCgYIKoEcz1UBgi2hRANCAAQPpkZNnOnXTCXOIHJbfR+i6ea1QkM8HxkdO8KSWK8IgltHZxr5xlxiqR8inOREmmrxUQQagOH5i3oELWgXZz8G"); + private static final String YOP_PUBLIC_KEY_SERIAL_NO = "4059376239"; + // 注意:此处为测试环境国密证书,生产环境请联系技术支持获取 + private static final BCECPublicKey YOP_PUBLIC_KEY = string2PublicKey("MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEStsxeNDxhJzM61Uy0rCmnW9Zs4Ze7oIKX27dgFTB7FsTsiCEzvxD7OTCKd7F17Xa1vpJ07C+2+H2OOFBSyadZA=="); + private static final Map YOP_PUBLIC_KEY_MAP = Collections.singletonMap(YOP_PUBLIC_KEY_SERIAL_NO, YOP_PUBLIC_KEY); + private static final String SERVER_ROOT = "https://sandbox.yeepay.com/yop-center"; + private static final String SLASH = "/"; + private static final String UNDER_LINE = "_"; + private static final String SEMICOLON = ";"; + private static final String EMPTY = ""; + + + + private static final String SM4_CBC_PKCS5PADDING = "SM4/CBC/PKCS5Padding"; + private static final byte[] SM4_IV = new SecureRandom().generateSeed(16); + + private static final String STREAM = "stream"; + private static final String DEFAULT_ENCODING = "UTF-8"; + private static final String DEFAULT_YOP_PROTOCOL_VERSION = "yop-auth-v3"; + public static final String DEFAULT_AUTH_PREFIX_SM2 = "YOP-SM2-SM3"; + private static final String DEFAULT_DIGEST_ALG = "SM3"; + private static final Joiner QUERY_STRING_JOINER = Joiner.on('&'); + + // mode 指定密文结构,旧标准的为C1C2C3,新的[《SM2密码算法使用规范》 GM/T 0009-2012]标准为C1C3C2 + // 注意:我们采用C1C3C2,如果bcprov-jdk15on包版本太低(小于1.62),则【不支持报文参数加密】 + private static final boolean BIZ_PARAM_ENCRYPT_SUPPORTED = false;// 当前环境是否支持参数加密 + // 根据mode不同,输出的密文C1C2C3排列顺序不同。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。 + private static final ThreadLocal engineThreadLocal = new ThreadLocal() { + @Override + protected SM2Engine initialValue() { + // bcprov-jdk15on包版本太低(小于1.62时),不支持报文参数加密 + if (!BIZ_PARAM_ENCRYPT_SUPPORTED) { + return new SM2Engine(); + } else { + //return new SM2Engine(SM2Engine.Mode.C1C3C2); + } + return null; + } + }; + + + + + public static void main(String[] args) throws Exception { + // 加密会话密钥,可每笔调用都生成,也可以多笔公用一个,建议定时更换 + String encryptKey = encodeUrlSafeBase64(generateRandomKey()); + + // 注意:以下分别展示了不同请求方式的参数构造、加密、解密、验签的完整流程,请根据接口实际情况进行选择 + + // get请求,form参数 + getFormExample(YopRequestMethod.GET, "/rest/v1.0/test/errorcode2", + YopRequestContentType.FORM_URL_ENCODE, encryptKey); + + //post请求,form参数 + postFormExample(YopRequestMethod.POST, "/rest/v1.0/frontcashier/agreement/sign/confirm", + YopRequestContentType.FORM_URL_ENCODE, encryptKey); + + // post请求,文件参数 + postMultipartFormExample(YopRequestMethod.POST, "/yos/v1.0/sys/merchant/qual/upload", + YopRequestContentType.MULTIPART_FORM, encryptKey); + + // post请求,json参数 + postJsonExample(YopRequestMethod.POST, "/rest/v1.0/test-wdc/test/http-json/test", + YopRequestContentType.JSON, encryptKey); + + // get请求,form参数,下载文件 + downloadExample(YopRequestMethod.GET, "/yos/v1.0/test/test/ceph-download", + YopRequestContentType.FORM_URL_ENCODE, encryptKey); + } + + private static void getFormExample(YopRequestMethod requestMethod, String requestUri, + YopRequestContentType requestContentType, + String encryptKey) throws Exception { + // 请求参数 + Multimap formParams = ArrayListMultimap.create(); + Set encryptHeaders = Collections.emptySet(); + Set encryptParams = Sets.newHashSet(); + + // 注意:以下两个代码块分别展示了加密、不加密两种示例,请根据情况选择其一 + + // 参数不加密-------------->开始 +// formParams.put("errorCode", "000027"); + // 参数不加密-------------->结束 + + // 参数加密-------------->开始 + formParams.put("errorCode", encryptParam(encryptKey, "000027")); + encryptParams.add("errorCode"); + // 参数加密-------------->结束 + + // 请求头 + Map headers = buildHeaders(requestMethod, requestUri, formParams, + requestContentType, "", encryptKey, encryptHeaders, encryptParams); + + // 构造http请求 + HttpUriRequest request = buildHttpRequest(SERVER_ROOT + requestUri, + requestMethod, headers, formParams, null); + + // 发起http调用 + CloseableHttpResponse response = null; + try { + response = httpClient.execute(request); + handleResponse(YopRequestType.WEB, response, encryptKey); + } finally { + HttpClientUtils.closeQuietly(response); + } + } + + + private static Map buildHeaders(YopRequestMethod httpMethod, String apiUri, + Multimap params, + YopRequestContentType contentType, String content, + String encryptKey, Set encryptHeaders, Set encryptParams) throws Exception { + Map headers = new HashMap<>(); + headers.put(YOP_SDK_LANGS, "java"); + headers.put(YOP_SDK_VERSION, "4.3.0"); + headers.put(YOP_APPKEY, APP_KEY); + headers.put(YOP_REQUEST_ID, UUID.randomUUID().toString()); + + // 加密头:请求参数不加密的情况, 也需设置加密头,用于响应结果加密 + if (BIZ_PARAM_ENCRYPT_SUPPORTED) { + headers.put(YOP_ENCRYPT, buildEncryptHeader(encryptHeaders, encryptParams, encryptKey)); + } + + // 摘要头 + headers.put(YOP_CONTENT_SM3, calculateContentHash(httpMethod, contentType, params, content)); + + // 签名头 + headers.put(AUTHORIZATION, signRequest(httpMethod, apiUri, headers, params)); + return headers; + } + + private static void postFormExample(YopRequestMethod requestMethod, String requestUri, + YopRequestContentType requestContentType, + String encryptKey) throws Exception { + + // 请求参数 + Multimap params = ArrayListMultimap.create(); + Set encryptHeaders = Collections.emptySet(); + Set encryptParams = Sets.newHashSet(); + + // 注意:以下两个代码块分别展示了加密、不加密两种示例,请根据情况选择其一 + + // 参数不加密-------------->开始 + params.put("parentMerchant", "10080041523"); + params.put("merchantNo", "10080041523"); + params.put("smsCode", "11111131"); + params.put("merchantFlowId", "11111131"); + // 参数不加密-------------->结束 + + + // 参数加密-------------->开始 +// params.put("parentMerchant", encryptParam(encryptKey , "10080041523" )); +// params.put("merchantNo", encryptParam(encryptKey , "10080041523" )); +// params.put("smsCode", encryptParam(encryptKey , "11111131" )); +// params.put("merchantFlowId", encryptParam(encryptKey , "111111" )); +// encryptParams.add("parentMerchant"); +// encryptParams.add("merchantNo"); +// encryptParams.add("smsCode"); +// encryptParams.add("merchantFlowId"); + // 参数加密-------------->结束 + + // 请求头 + Map headers = buildHeaders(requestMethod, requestUri, params, + requestContentType, "", encryptKey, encryptHeaders, encryptParams); + + // 构造http请求 + HttpUriRequest request = buildHttpRequest(SERVER_ROOT + requestUri, + requestMethod, headers, params, null); + + // 发起http调用 + CloseableHttpResponse response = null; + try { + response = httpClient.execute(request); + handleResponse(YopRequestType.WEB, response, encryptKey); + } finally { + HttpClientUtils.closeQuietly(response); + } + } + + private static void postMultipartFormExample(YopRequestMethod requestMethod, String requestUri, + YopRequestContentType requestContentType, String encryptKey) throws Exception { + + + // 普通参数参与签名,clientId + Multimap params = ArrayListMultimap.create(); + Set encryptHeaders = Collections.emptySet(); + Set encryptParams = Sets.newHashSet(); + + // 注意:以下两个代码块分别展示了加密、不加密两种示例,请根据情况选择其一 + + // 参数不加密--------------->开始 + params.put("clientId", APP_KEY); + // 参数不加密--------------->结束 + + // 参数加密--------------->开始 +// params.put("clientId", encryptParam(encryptKey, APP_KEY)); +// encryptParams.add("clientId"); + // 参数加密--------------->结束 + + // 请求头 + Map headers = buildHeaders(requestMethod, requestUri, params, + requestContentType, "", encryptKey, encryptHeaders, encryptParams); + + // 构造http请求 + HttpPost postMethod = new HttpPost(SERVER_ROOT + requestUri); + MultipartEntityBuilder builder = MultipartEntityBuilder.create(); + builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE); + + // 添加普通参数 + for (Map.Entry> entry : params.asMap().entrySet()) { + String paramKey = entry.getKey(); + for (String value : entry.getValue()) { + builder.addTextBody(normalize(paramKey), normalize(value)); + } + } + + // 注意:以下两个代码块分别展示了本地文件、远程文件两种示例,请根据情况选择其一 + // 添加文件参数,merQual + // 方式一:本地文件流 +// builder.addBinaryBody("merQual", new FileInputStream("abc.txt"), +// ContentType.DEFAULT_BINARY, "abc.txt"); + // 方式二:远程文件流 + builder.addBinaryBody("merQual", new URL("https://open.yeepay.com/apis/docs/apis/common/ALL.json").openStream(), + ContentType.DEFAULT_BINARY, "abc.txt"); + + postMethod.setEntity(builder.build()); + + // 添加请求头 + for (Map.Entry entry : headers.entrySet()) { + postMethod.addHeader(entry.getKey(), entry.getValue()); + } + + // 发起http调用 + CloseableHttpResponse response = null; + try { + response = httpClient.execute(postMethod); + handleResponse(YopRequestType.WEB, response, encryptKey); + } finally { + HttpClientUtils.closeQuietly(response); + } + + } + + private static void postJsonExample(YopRequestMethod requestMethod, String requestUri, + YopRequestContentType requestContentType, + String encryptKey) throws Exception { + Set encryptHeaders = Collections.emptySet(); + Set encryptParams = Sets.newHashSet(); + + String plainJsonContent = "{\n" + + " \"arg1\" : {\n" + + " \"appId\" : \"app_1111111111\",\n" + + " \"customerNo\" : \"333333333\"\n" + + " },\n" + + " \"arg0\" : {\n" + + " \"string\" : \"hello\",\n" + + " \"array\" : [ \"test\" ]\n" + + " }\n" + + "}", + encryptJsonContent = encryptParam(encryptKey, plainJsonContent); + + // 注意:以下两个代码块分别展示了加密、不加密两种示例,请根据情况选择其一 + String finalJsonContent + // 不加密------------------->开始 + = plainJsonContent; + // 不加密------------------->结束 + + + // 加密-------------------->开始 +// = encryptJsonContent; +// encryptParams.add("$"); // 目前json推荐整体加密,$代表整体加密 + // 加密-------------------->结束 + + // 请求头 + // 目前不允许json接口带有form参数,置空即可 + final ArrayListMultimap params = ArrayListMultimap.create(); + Map headers = buildHeaders(requestMethod, requestUri, params, + requestContentType, finalJsonContent, encryptKey, encryptHeaders, encryptParams); + + // 构造http请求 + HttpUriRequest request = buildHttpRequest(SERVER_ROOT + requestUri, + requestMethod, headers, params, finalJsonContent); + + // 发起http调用 + CloseableHttpResponse response = null; + try { + response = httpClient.execute(request); + handleResponse(YopRequestType.WEB, response, encryptKey); + } finally { + HttpClientUtils.closeQuietly(response); + } + } + + private static void downloadExample(YopRequestMethod requestMethod, String requestUri, + YopRequestContentType requestContentType, + String encryptKey) throws Exception { + + // 根据实际情况来,【文件下载接口】可能是post方法,请求报文可能是form,也可能是json,可参考其他方式的入参处理 + // 此处仅演示文件响应流的处理 + + // 请求参数 + Multimap params = ArrayListMultimap.create(); + Set encryptHeaders = Collections.emptySet(); + Set encryptParams = Sets.newHashSet(); + + // 注意:以下两个代码块分别展示了加密、不加密两种示例,请根据情况选择其一 + // 不加密------------------->开始 + params.put("fileName", "wym-test.txt"); + // 不加密------------------->结束 + + // 加密--------------------->开始 +// params.put("fileName", encryptParam(encryptKey, "wym-test.txt")); +// encryptParams.add("fileName"); + // 加密--------------------->结束 + + // 请求头 + Map headers = buildHeaders(requestMethod, requestUri, params, + requestContentType, "", encryptKey, encryptHeaders, encryptParams); + + // 构造http请求 + HttpUriRequest request = buildHttpRequest(SERVER_ROOT + requestUri, + requestMethod, headers, params, ""); + + // 发起请求 + CloseableHttpResponse response = null; + try { + response = httpClient.execute(request); + handleResponse(YopRequestType.FILE_DOWNLOAD, response, encryptKey); + } finally { + HttpClientUtils.closeQuietly(response); + } + } + + private static final Set DEFAULT_HEADERS_TO_SIGN = Sets.newHashSet(); + private static final Joiner HEADER_JOINER = Joiner.on('\n'); + private static final Joiner SIGNED_HEADER_STRING_JOINER = Joiner.on(';'); + + private static final String YOP_SDK_VERSION = "x-yop-sdk-version"; + private static final String YOP_SDK_LANGS = "x-yop-sdk-langs"; + private static final String YOP_REQUEST_ID = "x-yop-request-id"; + private static final String YOP_APPKEY = "x-yop-appkey"; + private static final String YOP_CONTENT_SM3 = "x-yop-content-sm3"; + private static final String YOP_ENCRYPT = "x-yop-encrypt"; + private static final String AUTHORIZATION = "Authorization"; + private static final String CONTENT_DISPOSITION = "Content-Disposition"; + private static final String CONTENT_TYPE = "Content-Type"; + + static { + DEFAULT_HEADERS_TO_SIGN.add(YOP_REQUEST_ID); + DEFAULT_HEADERS_TO_SIGN.add(YOP_APPKEY); + DEFAULT_HEADERS_TO_SIGN.add(YOP_CONTENT_SM3); + DEFAULT_HEADERS_TO_SIGN.add(YOP_ENCRYPT); + } + + private static String signRequest(YopRequestMethod requestMethod, String requestUri, + Map headers, Multimap params) throws Exception { + // A.构造认证字符串 + String authString = buildAuthString(); + + // B.获取规范请求串 + SortedMap headersToSign = getHeadersToSign(headers, DEFAULT_HEADERS_TO_SIGN); + String canonicalRequest = buildCanonicalRequest(requestMethod, requestUri, params, authString, headersToSign); + + // C.计算签名 + String signature = encodeUrlSafeBase64(sign(canonicalRequest.getBytes(DEFAULT_ENCODING))) + "$" + DEFAULT_DIGEST_ALG; + + // D.添加认证头 + return buildAuthHeader(authString, headersToSign, signature); + } + + private static String buildAuthHeader(String authString, + SortedMap headersToSign, + String signature) { + String signedHeaders = SIGNED_HEADER_STRING_JOINER.join(headersToSign.keySet()); + signedHeaders = signedHeaders.trim().toLowerCase(); + return DEFAULT_AUTH_PREFIX_SM2 + " " + authString + "/" + signedHeaders + "/" + signature; + } + + // SM2 + public static final SM2P256V1Curve CURVE = new SM2P256V1Curve(); + public final static BigInteger SM2_ECC_N = CURVE.getOrder(); + public final static BigInteger SM2_ECC_H = CURVE.getCofactor(); + public final static BigInteger SM2_ECC_GX = new BigInteger( + "32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C7", 16); + public final static BigInteger SM2_ECC_GY = new BigInteger( + "BC3736A2F4F6779C59BDCEE36B692153D0A9877CC62A474002DF32E52139F0A0", 16); + public static final ECPoint G_POINT = CURVE.createPoint(SM2_ECC_GX, SM2_ECC_GY); + public static final ECDomainParameters DOMAIN_PARAMS = new ECDomainParameters(CURVE, G_POINT, + SM2_ECC_N, SM2_ECC_H); + public static final int CURVE_LEN = getCurveLength(DOMAIN_PARAMS); + public static int getCurveLength(ECDomainParameters domainParams) { + return (domainParams.getCurve().getFieldSize() + 7) / 8; + } + + private static byte[] sign(byte[] data) { + try { + ECParameterSpec parameterSpec = ISV_PRIVATE_KEY.getParameters(); + ECDomainParameters domainParameters = new ECDomainParameters(parameterSpec.getCurve(), parameterSpec.getG(), + parameterSpec.getN(), parameterSpec.getH()); + ECPrivateKeyParameters priKeyParameters = new ECPrivateKeyParameters(ISV_PRIVATE_KEY.getD(), domainParameters); + //der编码后的签名值 + byte[] derSign = sign(priKeyParameters, null, data); + + //der解码过程 + ASN1Sequence as = DERSequence.getInstance(derSign); + byte[] rBytes = ((ASN1Integer) as.getObjectAt(0)).getValue().toByteArray(); + byte[] sBytes = ((ASN1Integer) as.getObjectAt(1)).getValue().toByteArray(); + //由于大数的补0规则,所以可能会出现33个字节的情况,要修正回32个字节 + rBytes = fixToCurveLengthBytes(rBytes); + sBytes = fixToCurveLengthBytes(sBytes); + byte[] rawSign = new byte[rBytes.length + sBytes.length]; + System.arraycopy(rBytes, 0, rawSign, 0, rBytes.length); + System.arraycopy(sBytes, 0, rawSign, rBytes.length, sBytes.length); + return rawSign; + } catch (Exception e) { + throw new RuntimeException("SystemError, Sign Fail, key:" + ISV_PRIVATE_KEY + ", ex:", e); + } + } + + private static boolean verifySign(String content, String signature, BCECPublicKey publicKey) { + try { + byte[] srcData = content.getBytes(DEFAULT_ENCODING); + byte[] signData = decodeBase64(signature); + ECParameterSpec parameterSpec = publicKey.getParameters(); + ECDomainParameters domainParameters = new ECDomainParameters(parameterSpec.getCurve(), parameterSpec.getG(), + parameterSpec.getN(), parameterSpec.getH()); + ECPublicKeyParameters pubKeyParameters = new ECPublicKeyParameters(publicKey.getQ(), domainParameters); + return verify(pubKeyParameters, null, srcData, encodeSM2SignToDER(signData)); + } catch (IOException e) { + throw new RuntimeException("UnexpectedError, VerifySign Fail, data:" + + content + ", sign:" + signature + ", key:" + publicKey + ", ex:", e); + } + } + + /** + * 把64字节的纯R+S字节数组编码成DER编码 + * + * @param rawSign 64字节数组形式的SM2签名值,前32字节为R,后32字节为S + * @return DER编码后的SM2签名值 + * @throws IOException + */ + private static byte[] encodeSM2SignToDER(byte[] rawSign) throws IOException { + //要保证大数是正数 + BigInteger r = new BigInteger(1, extractBytes(rawSign, 0, 32)); + BigInteger s = new BigInteger(1, extractBytes(rawSign, 32, 32)); + ASN1EncodableVector v = new ASN1EncodableVector(); + v.add(new ASN1Integer(r)); + v.add(new ASN1Integer(s)); + return new DERSequence(v).getEncoded(ASN1Encoding.DER); + } + + private static byte[] extractBytes(byte[] src, int offset, int length) { + byte[] result = new byte[length]; + System.arraycopy(src, offset, result, 0, result.length); + return result; + } + + /** + * 验签 + * + * @param pubKeyParameters 公钥 + * @param withId 可以为null,若为null,则默认withId为字节数组:"1234567812345678".getBytes() + * @param srcData 原文 + * @param sign DER编码的签名值 + * @return 验签成功返回true,失败返回false + */ + public static boolean verify(ECPublicKeyParameters pubKeyParameters, byte[] withId, byte[] srcData, byte[] sign) { + SM2Signer signer = new SM2Signer(); + CipherParameters param; + if (withId != null) { + param = new ParametersWithID(pubKeyParameters, withId); + } else { + param = pubKeyParameters; + } + signer.init(false, param); + signer.update(srcData, 0, srcData.length); + return signer.verifySignature(sign); + } + + private static byte[] fixToCurveLengthBytes(byte[] src) { + if (src.length == CURVE_LEN) { + return src; + } + + byte[] result = new byte[CURVE_LEN]; + if (src.length > CURVE_LEN) { + System.arraycopy(src, src.length - result.length, result, 0, result.length); + } else { + System.arraycopy(src, 0, result, result.length - src.length, src.length); + } + return result; + } + + /** + * 签名 + * + * @param priKeyParameters 私钥 + * @param withId 可以为null,若为null,则默认withId为字节数组:"1234567812345678".getBytes() + * @param srcData 源数据 + * @return DER编码后的签名值 + * @throws CryptoException + */ + public static byte[] sign(ECPrivateKeyParameters priKeyParameters, byte[] withId, byte[] srcData) + throws CryptoException { + SM2Signer signer = new SM2Signer(); + CipherParameters param; + ParametersWithRandom pwr = new ParametersWithRandom(priKeyParameters, new SecureRandom()); + if (withId != null) { + param = new ParametersWithID(pwr, withId); + } else { + param = pwr; + } + signer.init(true, param); + signer.update(srcData, 0, srcData.length); + return signer.generateSignature(); + } + + private static BCECPrivateKey string2PrivateKey(String priKey) { + try { + return (BCECPrivateKey)KeyFactory.getInstance("EC", BouncyCastleProvider.PROVIDER_NAME).generatePrivate( + new PKCS8EncodedKeySpec(decodeBase64(priKey))); + } catch (Exception e) { + throw new RuntimeException("ConfigProblem, IsvPrivateKey ParseFail, value:" + priKey + ", ex:", e); + } + } + + private static byte[] decodeBase64(String input) { + return Base64.decodeBase64(input); + } + + private static String buildCanonicalRequest(YopRequestMethod httpMethod, String apiUri, Multimap params, + String authString, + SortedMap headersToSign) { + String canonicalQueryString; + if (YopRequestMethod.GET.equals(httpMethod) && null != params) { + canonicalQueryString = getCanonicalQueryString(params, true); + } else { + canonicalQueryString = "";// post from 与json时,均为空,此处先简单处理 + } + String canonicalHeaders = getCanonicalHeaders(headersToSign); + + String canonicalURI = getCanonicalURIPath(apiUri); + return authString + "\n" + + httpMethod.name() + "\n" + + canonicalURI + "\n" + + canonicalQueryString + "\n" + + canonicalHeaders; + } + + private static String getCanonicalURIPath(String path) { + if (path == null) { + return "/"; + } else if (path.startsWith("/")) { + return normalizePath(path); + } else { + return "/" + normalizePath(path); + } + } + + private static String normalizePath(String path) { + return normalize(path).replace("%2F", "/"); + } + + private static SortedMap getHeadersToSign(Map headers, Set headersToSign) { + SortedMap ret = Maps.newTreeMap(); + if (headersToSign != null) { + Set tempSet = Sets.newHashSet(); + for (String header : headersToSign) { + tempSet.add(header.trim().toLowerCase()); + } + headersToSign = tempSet; + } + for (Map.Entry entry : headers.entrySet()) { + String key = entry.getKey(); + if (entry.getValue() != null && !entry.getValue().isEmpty()) { + if ((headersToSign != null && headersToSign.contains(key.toLowerCase()) + && !AUTHORIZATION.equalsIgnoreCase(key))) { + ret.put(key, entry.getValue()); + } + } + } + return ret; + } + + private static String getCanonicalHeaders(SortedMap headers) { + if (headers.isEmpty()) { + return ""; + } + + List headerStrings = Lists.newArrayList(); + for (Map.Entry entry : headers.entrySet()) { + String key = entry.getKey(); + if (key == null) { + continue; + } + String value = entry.getValue(); + if (value == null) { + value = ""; + } + headerStrings.add(normalize(key.trim().toLowerCase()) + ':' + normalize(value.trim())); + } + Collections.sort(headerStrings); + + return HEADER_JOINER.join(headerStrings); + } + + private static final DateTimeFormatter alternateIso8601DateFormat = + ISODateTimeFormat.dateTimeNoMillis().withZone(DateTimeZone.UTC); + + private static String buildAuthString() { + Date timestamp = new Date(); + return DEFAULT_YOP_PROTOCOL_VERSION + "/" + + APP_KEY + "/" + + alternateIso8601DateFormat.print(new DateTime(timestamp)) + "/" + + "1800"; + } + + private static String calculateContentHash(String jsonContent) throws Exception { + InputStream contentStream = getContentStream(jsonContent); + return Hex.encodeHexString((digest(contentStream))); + } + + private static String calculateContentHash(YopRequestMethod requestMethod, YopRequestContentType requestContentType, + Multimap params, String content) throws Exception { + String digestSource; + + if (requestMethod.equals(YopRequestMethod.GET)) { + digestSource = ""; + } else if (requestMethod.equals(YopRequestMethod.POST) + && requestContentType.equals(YopRequestContentType.JSON)) { + digestSource = content; + } else { + digestSource = getCanonicalQueryString(params, true); + } + InputStream contentStream = getContentStream(digestSource); + return Hex.encodeHexString((digest(contentStream))); + } + + private static ByteArrayInputStream getContentStream(Multimap params) throws Exception { + return getContentStream(getCanonicalQueryString(params, true)); + } + + private static ByteArrayInputStream getContentStream(String paramStr) throws Exception { + byte[] bytes; + if (StringUtils.isEmpty(paramStr)) { + bytes = new byte[0]; + } else { + bytes = paramStr.getBytes(DEFAULT_ENCODING); + } + return new ByteArrayInputStream(bytes); + } + + public static String getCanonicalQueryString(Multimap params, boolean forSignature) { + Map> parameters; + if (null == params || (parameters = params.asMap()).isEmpty()) { + return ""; + } + + List parameterStrings = Lists.newArrayList(); + for (Map.Entry> entry : parameters.entrySet()) { + if (forSignature && AUTHORIZATION.equalsIgnoreCase(entry.getKey())) { + continue; + } + String key = entry.getKey(); + checkNotNull(key, "parameter key should not be null"); + Collection value = entry.getValue(); + if (value == null) { + if (forSignature) { + parameterStrings.add(normalize(key) + '='); + } else { + parameterStrings.add(normalize(key)); + } + } else { + for (String item : value) { + parameterStrings.add(normalize(key) + '=' + normalize(item)); + } + } + } + Collections.sort(parameterStrings); + + return QUERY_STRING_JOINER.join(parameterStrings); + } + + private static final BitSet URI_UNRESERVED_CHARACTERS = new BitSet(); + private static final String[] PERCENT_ENCODED_STRINGS = new String[256]; + + static { + /* + * StringBuilder pattern = new StringBuilder(); + * + * pattern .append(Pattern.quote("+")) .append("|") .append(Pattern.quote("*")) .append("|") + * .append(Pattern.quote("%7E")) .append("|") .append(Pattern.quote("%2F")); + * + * ENCODED_CHARACTERS_PATTERN = Pattern.compile(pattern.toString()); + */ + for (int i = 'a'; i <= 'z'; i++) { + URI_UNRESERVED_CHARACTERS.set(i); + } + for (int i = 'A'; i <= 'Z'; i++) { + URI_UNRESERVED_CHARACTERS.set(i); + } + for (int i = '0'; i <= '9'; i++) { + URI_UNRESERVED_CHARACTERS.set(i); + } + URI_UNRESERVED_CHARACTERS.set('-'); + URI_UNRESERVED_CHARACTERS.set('.'); + URI_UNRESERVED_CHARACTERS.set('_'); + URI_UNRESERVED_CHARACTERS.set('~'); + + for (int i = 0; i < PERCENT_ENCODED_STRINGS.length; ++i) { + PERCENT_ENCODED_STRINGS[i] = String.format("%%%02X", i); + } + } + + public static String normalize(String value) { + try { + StringBuilder builder = new StringBuilder(); + for (byte b : value.getBytes(DEFAULT_ENCODING)) { + if (URI_UNRESERVED_CHARACTERS.get(b & 0xFF)) { + builder.append((char) b); + } else { + builder.append(PERCENT_ENCODED_STRINGS[b & 0xFF]); + } + } + return builder.toString(); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + + private static byte[] digest(InputStream input) { + try { + MessageDigest md = MessageDigest.getInstance(DEFAULT_DIGEST_ALG, BouncyCastleProvider.PROVIDER_NAME); + DigestInputStream digestInputStream = new DigestInputStream(input, md); + byte[] buffer = new byte[1024]; + while (digestInputStream.read(buffer) > -1) {} + return digestInputStream.getMessageDigest().digest(); + } catch (Exception e) { + throw new RuntimeException("SystemError, Digest Fail, alg:" + DEFAULT_DIGEST_ALG + ", ex:", e); + } + } + + /** + * 加密协议头(请求): + * + * yop-encrypt-v1/{服务端证书序列号}/{密钥类型(必填)}_{分组模式(必填)}_{填充算法(必填)}/{加密密钥值(必填)}/{IV}{;}{附加信息}/{客户端支持的大参数加密模式(必填)}/{encryptHeaders}/{encryptParams} + */ + public static String buildEncryptHeader(Set encryptHeaders, Set encryptParams, + String sm4Key) throws Exception { + + return "yop-encrypt-v1" + SLASH + + YOP_PUBLIC_KEY_SERIAL_NO + SLASH + //平台SM2证书序列号 + StringUtils.replace(SM4_CBC_PKCS5PADDING, SLASH, UNDER_LINE) + SLASH + + encodeUrlSafeBase64(encryptKey(decodeBase64(sm4Key))) + SLASH + + encodeUrlSafeBase64(SM4_IV) + SEMICOLON + EMPTY + SLASH + + STREAM + SLASH + + StringUtils.join(encryptHeaders, SEMICOLON) + SLASH + + encodeUrlSafeBase64(StringUtils.join(encryptParams, SEMICOLON).getBytes(DEFAULT_ENCODING)); + } + + public static BCECPublicKey string2PublicKey(String pubKey) { + try { + return (BCECPublicKey) KeyFactory.getInstance("EC").generatePublic( + new X509EncodedKeySpec(decodeBase64(pubKey))); + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + throw new RuntimeException("ConfigProblem, YopPublicKey ParseFail, value:" + pubKey + ", ex:", e); + } + } + + private static byte[] encryptKey(byte[] sm4Key) { + try { + SM2Engine engine = engineThreadLocal.get(); + ECPublicKeyParameters pubKeyParameters = convertPublicKeyToParameters(); + ParametersWithRandom pwr = new ParametersWithRandom(pubKeyParameters, new SecureRandom()); + engine.init(true, pwr); + return engine.processBlock(sm4Key, 0, sm4Key.length); + } catch (Throwable e) { + throw new RuntimeException("SystemError, Encrypt Fail, publicKey:" + YOP_PUBLIC_KEY, e); + } + } + + private static ECPublicKeyParameters convertPublicKeyToParameters() { + ECParameterSpec parameterSpec = YOP_PUBLIC_KEY.getParameters(); + ECDomainParameters domainParameters = new ECDomainParameters(parameterSpec.getCurve(), parameterSpec.getG(), + parameterSpec.getN(), parameterSpec.getH()); + return new ECPublicKeyParameters(YOP_PUBLIC_KEY.getQ(), domainParameters); + } + + private static String encryptParam(String sm4Key, String plain) throws Exception { + if (BIZ_PARAM_ENCRYPT_SUPPORTED) { + final Cipher cipher = getInitializedCipher(Cipher.ENCRYPT_MODE, sm4Key); + return encodeUrlSafeBase64(cipher.doFinal(plain.getBytes("UTF-8"))); + } else { + return plain; + } + } + + private static String decryptParam(String sm4Key, String encryptContent) throws Exception { + final Cipher cipher = getInitializedCipher(Cipher.DECRYPT_MODE, sm4Key); + return new String(cipher.doFinal(decodeBase64(encryptContent)), "UTF-8"); + } + + private static InputStream decryptStream(String sm4Key, InputStream encryptStream) throws Exception { + final Cipher cipher = getInitializedCipher(Cipher.DECRYPT_MODE, sm4Key); + return new CipherInputStream(encryptStream, cipher); + } + + private static Cipher getInitializedCipher(int mode, String sm4Key) { + try { + byte[] key = decodeBase64(sm4Key); + Cipher cipher = Cipher.getInstance(SM4_CBC_PKCS5PADDING, BouncyCastleProvider.PROVIDER_NAME); + IvParameterSpec spec = new IvParameterSpec(SM4_IV); + Key secretKey = new SecretKeySpec(key, "SM4"); + cipher.init(mode, secretKey, spec); + return cipher; + } catch (Throwable throwable) { + throw new RuntimeException("error happened when initialize cipher", throwable); + } + } + + private static String encodeUrlSafeBase64(byte[] input) { + return Base64.encodeBase64URLSafeString(input); + } + + private static byte[] generateRandomKey() throws NoSuchAlgorithmException, NoSuchProviderException { + //实例化 + KeyGenerator generator = KeyGenerator.getInstance("SM4", BouncyCastleProvider.PROVIDER_NAME); + //设置密钥长度,SM4算法目前只支持128位(即密钥16字节) + generator.init(128); + //生成密钥 + return generator.generateKey().getEncoded(); + } + + protected static HttpUriRequest buildHttpRequest(String requestUrl, YopRequestMethod requestMethod, + Map headers, Multimap params, + String content) throws UnsupportedEncodingException { + RequestBuilder requestBuilder; + if (YopRequestMethod.POST == requestMethod) { + requestBuilder = RequestBuilder.post(); + } else if (YopRequestMethod.GET == requestMethod) { + requestBuilder = RequestBuilder.get(); + } else { + throw new RuntimeException("unsupported http method"); + } + requestBuilder.setUri(requestUrl); + + // header + for (Map.Entry entry : headers.entrySet()) { + requestBuilder.addHeader(entry.getKey(), entry.getValue()); + } + + // body + try { + if (null != params) { + for (Map.Entry> entry : params.asMap().entrySet()) { + String paramKey = entry.getKey(); + for (String value : entry.getValue()) { + requestBuilder.addParameter(paramKey, URLEncoder.encode(value, DEFAULT_ENCODING)); + } + } + } + } catch (IOException ex) { + throw new RuntimeException("unable to create http request.", ex); + } + if (YopRequestMethod.GET.equals(requestMethod)) { + requestBuilder.addHeader(CONTENT_TYPE, "application/x-www-form-urlencoded;charset=UTF-8"); + return requestBuilder.build(); + } + // json 请求 + if (StringUtils.isNotBlank(content)) { + final byte[] contentBytes = content.getBytes(DEFAULT_ENCODING); + requestBuilder.setEntity(new InputStreamEntity(new ByteArrayInputStream(contentBytes), contentBytes.length)); + requestBuilder.addHeader(CONTENT_TYPE, "application/json;charset=UTF-8"); + } + return requestBuilder.build(); + } + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + private static void handleResponse(YopRequestType requestType, + CloseableHttpResponse httpResponse, + String encryptKey) throws Exception { + // header + Map headers = Maps.newHashMap(); + for (Header header : httpResponse.getAllHeaders()) { + headers.put(header.getName(), header.getValue()); + } + String encryptHeader = headers.get("x-yop-encrypt"),// 加密头 + signHeader = headers.get("x-yop-sign"), //签名头 + signSerialNo = headers.get("x-yop-sign-serial-no"); //签名序列号,可能会变 + + boolean isEncryptResponse = StringUtils.isNotBlank(encryptHeader); + + // body + HttpEntity entity = httpResponse.getEntity(); + int statusCode = httpResponse.getStatusLine().getStatusCode(); + if (statusCode / 100 == HttpStatus.SC_OK / 100 && statusCode != HttpStatus.SC_NO_CONTENT) { + //not a error + if (null != entity && entity.getContent() != null) { + if (isJsonResponse(httpResponse)) { + String content = IOUtils.toString(entity.getContent(), DEFAULT_ENCODING); + System.out.println("Request success, response:" + content); + verifyResponseSign(signHeader, content, signSerialNo); + final JsonNode bizData = OBJECT_MAPPER.readTree(content).get("result"); + if (isEncryptResponse) { + System.out.println("Response decrypt success, bizData:" + decryptParam(encryptKey, bizData.asText())); + } + return; + } else if (YopRequestType.FILE_DOWNLOAD.equals(requestType) || isDownloadResponse(httpResponse)) { + InputStream fileContent = entity.getContent(); + if (isEncryptResponse) { + fileContent = decryptStream(encryptKey, fileContent); + System.out.println("Response file decrypt success"); + } + final File file = saveFile(fileContent, headers); + System.out.println("Request success, file downloaded:" + file); + return; + } else { + throw new RuntimeException("Response Error, contentType:" + httpResponse.getEntity().getContentType()); + } + } else { + throw new RuntimeException("Response Error, contentType:" + httpResponse.getEntity().getContentType()); + } + } else if (statusCode >= HttpStatus.SC_INTERNAL_SERVER_ERROR && statusCode != HttpStatus.SC_BAD_GATEWAY) { + if (entity.getContent() != null) { + String content = IOUtils.toString(entity.getContent(), DEFAULT_ENCODING); + System.out.println("Request Fail, response:" + content); + verifyResponseSign(signHeader, content, signSerialNo); + return; + } else { + throw new RuntimeException("ResponseError, Empty Content, httpStatusCode:" + httpResponse.getStatusLine().getStatusCode()); + } + } else if (statusCode == HttpStatus.SC_BAD_GATEWAY || statusCode == HttpStatus.SC_NOT_FOUND) { + throw new RuntimeException("Response Error, statusCode:" + statusCode); + } + throw new RuntimeException("ReqParam Illegal, Bad Request, statusCode:" + statusCode); + } + + private static void verifyResponseSign(String signHeader, String content, String signSerialNo) { + if (StringUtils.isBlank(signHeader) || StringUtils.isBlank(content)|| StringUtils.isBlank(signSerialNo)) { + System.out.println("Response Sign Skip, signature:" + signHeader + ", content:" + content + ", signSerialNo:" + signSerialNo); + return; + } + String signContent = content.replaceAll("[ \t\n]", ""); + System.out.println("Response Sign Begin, signature:" + signHeader + ", content:" + signContent + ", signSerialNo:" + signSerialNo); + if (!YOP_PUBLIC_KEY_MAP.containsKey(signSerialNo)) { + System.out.println("Response Sign Skip verify, No YopPublicKey Found"); + } + if (!verifySign(signContent, signHeader, YOP_PUBLIC_KEY_MAP.get(signSerialNo))) { + System.out.println("Response Sign Verify Fail"); + } else { + System.out.println("Response Sign Verify Success"); + } + } + + /** + * 保存文件到本地 + * + * @param content + * @param headers + * @return File + */ + public static File saveFile(InputStream content, Map headers) { + InputStream fileContent = content; + try { + String filePrefix = "yos-", fileSuffix = ".tmp"; + final String contentDisposition = headers.get(CONTENT_DISPOSITION); + try { + String fileName = getFileNameFromHeader(contentDisposition); + if (StringUtils.isNotBlank(fileName)) { + final String[] split = fileName.split("\\."); + if (split.length == 2) { + if (StringUtils.length(split[0]) > 3) { + filePrefix = split[0]; + } + if (StringUtils.isNotBlank(split[1])) { + fileSuffix = "." + split[1]; + } + } + } + } catch (Exception e) { + System.out.println(("parse Content-Disposition fail, value:" + contentDisposition + ", ex:" + e)); + } + File tmpFile = File.createTempFile(filePrefix, fileSuffix); + IOUtils.copy(fileContent, Files.newOutputStream(tmpFile.toPath())); + return tmpFile; + } catch (Throwable ex) { + throw new RuntimeException("fail to save file"); + } finally { + closeQuietly(fileContent); + } + } + + public static void closeQuietly(Closeable closeable) { + try { + if (null != closeable) { + closeable.close(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + private static String getFileNameFromHeader(String contentDisposition) { + if (StringUtils.isBlank(contentDisposition)) { + return null; + } + + final String[] parts = contentDisposition.split( "filename="); + if (parts.length == 2) { + return StringUtils.trim(parts[1]); + } + return null; + } + + private static final String CONTENT_TYPE_JSON = "application/json"; + private static final String CONTENT_TYPE_STREAM = "application/octet-stream"; + + private static boolean isJsonResponse(CloseableHttpResponse response) { + return null != response.getEntity() && StringUtils.startsWith(response.getEntity().getContentType().getValue(), CONTENT_TYPE_JSON); + } + + private static boolean isDownloadResponse(CloseableHttpResponse response) { + return null != response.getEntity() && StringUtils.startsWith(response.getEntity().getContentType().getValue(), CONTENT_TYPE_STREAM); + } + + public enum YopRequestMethod { + GET, + POST + } + + public enum YopRequestType { + WEB, + FILE_DOWNLOAD, + MULTI_FILE_UPLOAD, + } + + private static final String YOP_HTTP_CONTENT_TYPE_JSON = "application/json"; + private static final String YOP_HTTP_CONTENT_TYPE_FORM = "application/x-www-form-urlencoded"; + private static final String YOP_HTTP_CONTENT_TYPE_MULTIPART_FORM = "multipart/form-data"; + private static final String YOP_HTTP_CONTENT_TYPE_STREAM = "application/octet-stream"; + private static final String YOP_HTTP_CONTENT_TYPE_TEXT = "text/plain;charset=UTF-8"; + + public enum YopRequestContentType { + FORM_URL_ENCODE(YOP_HTTP_CONTENT_TYPE_FORM), + MULTIPART_FORM(YOP_HTTP_CONTENT_TYPE_MULTIPART_FORM), + JSON(YOP_HTTP_CONTENT_TYPE_JSON), + OCTET_STREAM(YOP_HTTP_CONTENT_TYPE_STREAM), + TEXT_PLAIN(YOP_HTTP_CONTENT_TYPE_TEXT); + + private String value; + + YopRequestContentType(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + } +} From 7019a396c55a427c1234a41d708865b3ada4caca Mon Sep 17 00:00:00 2001 From: wdc Date: Fri, 24 Jan 2025 13:53:56 +0800 Subject: [PATCH 5/5] =?UTF-8?q?feat=EF=BC=9A=E9=99=84=E5=BD=95=EF=BC=8Cjar?= =?UTF-8?q?=E4=BE=9D=E8=B5=96=E6=83=85=E5=86=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yop/sdk/example/YopRsaEncryptExample.java | 50 +++++++++++++++ .../yop/sdk/example/YopSM2EncryptExample.java | 61 +++++++++++++++++++ 2 files changed, 111 insertions(+) diff --git a/yop-java-sdk-test/src/test/java/com/yeepay/yop/sdk/example/YopRsaEncryptExample.java b/yop-java-sdk-test/src/test/java/com/yeepay/yop/sdk/example/YopRsaEncryptExample.java index 7fa80160..6ba1a65a 100644 --- a/yop-java-sdk-test/src/test/java/com/yeepay/yop/sdk/example/YopRsaEncryptExample.java +++ b/yop-java-sdk-test/src/test/java/com/yeepay/yop/sdk/example/YopRsaEncryptExample.java @@ -988,4 +988,54 @@ public void setValue(String value) { this.value = value; } } + + // 附录:jar包依赖情况 + /** + * + * org.apache.httpcomponents + * httpcore + * 4.4.13 + * + * + * org.apache.httpcomponents + * httpclient + * 4.5.13 + * + * + * org.apache.httpcomponents + * httpmime + * 4.5.13 + * + * + * + * commons-codec + * commons-codec + * 1.10 + * + * + * commons-io + * commons-io + * 2.8.0 + * + * + * com.google.guava + * guava + * 30.0-jre + * + * + * joda-time + * joda-time + * 2.9.4 + * + * + * org.apache.commons + * commons-lang3 + * 3.6 + * + * + * com.fasterxml.jackson.core + * jackson-databind + * 2.13.2 + * + */ } diff --git a/yop-java-sdk-test/src/test/java/com/yeepay/yop/sdk/example/YopSM2EncryptExample.java b/yop-java-sdk-test/src/test/java/com/yeepay/yop/sdk/example/YopSM2EncryptExample.java index 37f095a9..444119aa 100644 --- a/yop-java-sdk-test/src/test/java/com/yeepay/yop/sdk/example/YopSM2EncryptExample.java +++ b/yop-java-sdk-test/src/test/java/com/yeepay/yop/sdk/example/YopSM2EncryptExample.java @@ -1168,4 +1168,65 @@ public void setValue(String value) { this.value = value; } } + + + // 附录:jar包依赖情况 + /** + * + * org.apache.httpcomponents + * httpcore + * 4.4.13 + * + * + * org.apache.httpcomponents + * httpclient + * 4.5.13 + * + * + * org.apache.httpcomponents + * httpmime + * 4.5.13 + * + * + * + * commons-codec + * commons-codec + * 1.10 + * + * + * commons-io + * commons-io + * 2.8.0 + * + * + * com.google.guava + * guava + * 30.0-jre + * + * + * joda-time + * joda-time + * 2.9.4 + * + * + * org.apache.commons + * commons-lang3 + * 3.6 + * + * + * com.fasterxml.jackson.core + * jackson-databind + * 2.13.2 + * + * + * org.bouncycastle + * bcprov-jdk15on + * 1.67 + * + * + * org.bouncycastle + * bcpkix-jdk15on + * 1.67 + * + */ }