diff --git a/server/dfp-open-sdk/pom.xml b/server/dfp-open-sdk/pom.xml new file mode 100644 index 00000000..51d53a2c --- /dev/null +++ b/server/dfp-open-sdk/pom.xml @@ -0,0 +1,49 @@ + + + + + org.mdd + likeadmin-java + 1.0.0 + + + 4.0.0 + dfp-open-sdk + jar + dfp-open-sdk + 兴业数金开放银行平台 Java SDK(源码由银行 demo 迁入,版本参见 demo Readme) + + + + + org.bouncycastle + bcprov-jdk15on + + + org.bouncycastle + bcpkix-jdk15on + + + + com.alibaba + fastjson + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.10.0 + + 1.8 + 1.8 + ${project.build.sourceEncoding} + + + + + diff --git a/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/Example.java b/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/Example.java new file mode 100644 index 00000000..dc1c77dc --- /dev/null +++ b/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/Example.java @@ -0,0 +1,230 @@ +package com.cib.fintech.dfp.open.sdk; + +import com.cib.fintech.dfp.open.sdk.config.Configure; +import com.cib.fintech.dfp.open.sdk.config.KeyConfigure; +import com.cib.fintech.dfp.open.sdk.enums.KeySignTypeEnum; +import com.cib.fintech.dfp.open.sdk.enums.ReqMethodEnum; +import com.cib.fintech.dfp.open.sdk.enums.RespSignAlgorithmEnum; + +import java.util.HashMap; +import java.util.Map; + +/** + * 示例类 + *

+ * 注意:该类仅供参考学习,请不要将该类直接包含在开发者代码中 + * 重要:各传入参数SDK都不作任何检查、过滤,请务必在传入前进行安全检查或过滤,务必保证传入参数的安全性,否则会导致安全问题 + */ +public class Example { + + public static void main(String[] args) { + Example example = new Example(); + + // 【1.设置SDK中应用密钥信息,需在项目启动时完成赋值】 + example.keyConfigureInit(); + + // 设置是否测试环境,true连接测试环境,false连接生产环境 + Configure.setDevEnv(true); + + // 按需求调用对应方法,传入keyId区分应用配置 + String response = example.execSdk("KYONLYFORTEST012345678"); + + System.out.println(response); + } + + /** + * 设置SDK中应用密钥信息 + *

+ * 注意:需在项目启动时完成赋值 + * 本代码值仅为示例,实际使用请替换为开发者申请的keyId及密钥 + */ + public void keyConfigureInit() { + + Map keyConfigures = Configure.getKeyConfigures(); + + // 应用ID,为KY字母开头的字符串,区分大小写 + String keyId = "KYONLYFORTEST012345678"; + + // 请求报文签名私钥,需严格保密,建议使用配置文件或其他方式进行存储 + String priKey = "AK7JUorRStFAwM1+BXiHefwmCOZXjVU42b6Oa5JFf5Y7"; + + // 响应报文验签公钥 + String respPubKey = "BAQl3uq+FxAGi1/vAZpWyBhdTqBUcenUm8jdPl59XHOeyFkSU3obseTJOTms7lfl0rFALHVTrOmV39N97Z2ZOVI="; + + // 字段加密密钥 + String reqParamEncryptKey = "9A9A4QyNz/pc8kZZXzhXOQ=="; + + // keySignTypeEnum 请求签名算法 + // respSignSwitch 响应报文验签开关,true将进行验签,false不进行验签 + // respSignAlgorithmEnum 响应报文验签算法 + KeyConfigure keyConfigure = new KeyConfigure(); + keyConfigure.setKeyId(keyId); + keyConfigure.setPriKey(priKey); + keyConfigure.setRespPubKey(respPubKey); + keyConfigure.setReqParamEncryptKey(reqParamEncryptKey); + keyConfigure.setKeySignType(KeySignTypeEnum.SM3WITHSM2); + keyConfigure.setRespSignSwitch(true); + keyConfigure.setRespSignAlgorithm(RespSignAlgorithmEnum.SM3WITHSM2); + + // 灰度发布配置 + // GrayConfigure grayConfigure = new GrayConfigure(); + // grayConfigure.setKey("wkroute"); + // grayConfigure.setValue("graytest"); + // keyConfigure.setGrayConfigure(grayConfigure); + + // CFCA证书配置(设置CFCA私钥证书全路径文件名、私钥保护密码以及签名算法) + // keyConfigure + // .setCfcaProperties("C:/Users/cibfintech/Desktop/test.pfx", "WjXcgOQr", KeySignTypeEnum + // .SHA256WITHRSA); + + // 用户token认证源为“企业客户授权认证”时配置 +// keyConfigure.setEnterpriseBearer("1117", "PD4gW2jjoeDzeEdwLbPMFGTN", "123456789"); + + keyConfigures.put(keyId, keyConfigure); + + Configure.setKeyConfigures(keyConfigures); + } + + /** + * 通用请求接口(非文件) + * + * @param keyId 应用ID + * @return 响应报文 + */ + public String execSdk(String keyId) { + + // 【2.准备请求参数】 + // 开发者根据API文档,准备不同的参数集合。headParams-头部信息请求参数、urlParams-URL请求参数、bodyParams-BODY请求参数 + Map bodyParams = new HashMap(8); + bodyParams.put("stringParam", "Hello World"); + bodyParams.put("intParam", "111"); + bodyParams.put("byteParam", "1"); + bodyParams.put("shortParam", "1111"); + bodyParams.put("longParam", "111111111"); + bodyParams.put("floatParam", "111.111"); + bodyParams.put("doubleParam", "111.111111"); + bodyParams.put("boolParam", "false"); + bodyParams.put("charParam", "A"); + + bodyParams.put("mockBean[0].intParam", "111"); + bodyParams.put("mockBean[0].stringParam", "ascb"); + + bodyParams.put("stringList[0]", "stringList0"); + bodyParams.put("stringList[1]", "stringList1"); + + bodyParams.put("mockBeanMap[0].intParam", "111"); + bodyParams.put("mockBeanMap[0].doubleParam", "111.111"); + bodyParams.put("mockBeanMap[0].stringParam", "AAAAA"); + bodyParams.put("mockBeanMap[1].intParam", "222"); + bodyParams.put("mockBeanMap[1].doubleParam", "222.222"); + bodyParams.put("mockBeanMap[1].stringParam", "BBBBB"); + + bodyParams.put("mockBeanList[0].intParam", "111"); + bodyParams.put("mockBeanList[0].doubleParam", "111.111"); + bodyParams.put("mockBeanList[0].stringParam", "group1/M00/97/68/CmAFGVpd3CGAREPDAAAECO1Nr3I123.txt"); + bodyParams.put("mockBeanList[1].intParam", "222"); + bodyParams.put("mockBeanList[1].doubleParam", "222.222"); + bodyParams.put("mockBeanList[1].stringParam", "group2/M00/97/68/CmAFGVpd3CGAREPDAAAECO1Nr3I123.txt"); + + // 需要加密的字段值处理示例(请参阅对应接口文档,并按实际情况使用) + // try { + // bodyParams.put("key", SymmetricEncrypt.aesEncrypt("value","9A9A4QyNz/pc8kZZXzhXOQ==")); + // } catch (Exception e) { + // // 如果字段加密密钥或字段值设置错误,需要处理情况 + // e.printStackTrace(); + // } + + // 【3.调用SDK方法】 + // 传入请求资源URI、请求方法,以及各位置的参数 + String response = OpenSdk.gatewayWithKeyId("/api/testApi", ReqMethodEnum.POST, null, null, bodyParams, keyId); + + return response; + } + + /** + * 通用请求接口(非文件),json + * + * @param keyId 应用ID + * @return 响应报文 + */ + public String execJsonSdk(String keyId) { + + // 【2.准备请求参数】 + // 开发者根据API文档,准备不同的参数集合。headParams-头部信息请求参数、urlParams-URL请求参数、bodyJson-BODY请求参数JSON字符串 + String bodyJson = "{\"key\":\"value\"}"; + + // 需要加密的字段值处理示例(请参阅对应接口文档,并按实际情况使用) + // try { + // JSONObject bodyJson = new JSONObject(); + // bodyJson.put("key", SymmetricEncrypt.aesEncrypt("value","9A9A4QyNz/pc8kZZXzhXOQ==")); + // } catch (Exception e) { + // // 如果字段加密密钥或字段值设置错误,需要处理情况 + // e.printStackTrace(); + // } + + // 【3.调用SDK方法】 + // 传入请求资源URI、请求方法,以及各位置的参数 + String response = OpenSdk.gatewayJsonWithKeyId("/api/testApi", ReqMethodEnum.POST, null, null, bodyJson, keyId); + + return response; + } + + /** + * 文件下载接口 + *

+ * 该请求用于下载文件,filePath 文件存储路径 + * + * @param keyId 应用ID + * @return 响应报文 + */ + public String downloadFile(String keyId) { + + // 【2.准备请求参数】 + // 文件ID + String fileId = "group1/M00/A2/19/CmAFGVppoA2AII5mAAAECO1Nr3I.a914b6"; + + // 文件存储路径 + String filePath = "C:\\Users\\cibfintech\\Desktop\\test.txt"; + + // 【3.调用SDK方法】 + String response = OpenSdk.downloadFileWithKeyId(fileId, filePath, keyId); + + return response; + } + + /** + * 文件上传接口 + *

+ * 该请求用于上传文件,filePath 文件路径 + * + * @param keyId 应用ID + * @return 响应报文 + */ + public String uploadFile(String keyId) { + + // 【2.准备请求参数】 + // 待上传文件路径 + String filePath = "C:\\Users\\cibfintech\\Desktop\\test.txt"; + + // 【3.调用SDK方法】 + String response = OpenSdk.uploadFileWithKeyId(filePath, keyId); + + return response; + } + + /** + * 公钥查询接口 + *

+ * 该请求用于查询公钥(如遇验签失败问题,可尝试调用该接口用于排除密钥、算法不一致问题) + * + * @param keyId 应用ID + * @return 响应报文 + */ + public String queryPubKey(String keyId) { + + // 【调用SDK方法】 + String response = OpenSdk.gatewayWithKeyId("/api/open/queryPubKey", ReqMethodEnum.GET, null, null, null, keyId); + + return response; + } +} \ No newline at end of file diff --git a/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/OpenSdk.java b/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/OpenSdk.java new file mode 100644 index 00000000..cd8162ac --- /dev/null +++ b/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/OpenSdk.java @@ -0,0 +1,385 @@ +package com.cib.fintech.dfp.open.sdk; + +import com.cib.fintech.dfp.open.sdk.config.Configure; +import com.cib.fintech.dfp.open.sdk.config.KeyConfigure; +import com.cib.fintech.dfp.open.sdk.enums.ReqMethodEnum; +import com.cib.fintech.dfp.open.sdk.exception.SdkExType; +import com.cib.fintech.dfp.open.sdk.exception.SdkException; +import com.cib.fintech.dfp.open.sdk.service.AbstractTransService; +import com.cib.fintech.dfp.open.sdk.util.Const; +import com.cib.fintech.dfp.open.sdk.util.FileUtil; +import com.cib.fintech.dfp.open.sdk.util.SdkUtil; + +import java.io.File; +import java.io.FileOutputStream; +import java.util.HashMap; +import java.util.Map; + +/** + * 开放银行平台统一接口类 + *

+ * 注意:需要先配置Configure类中的值,示例参见Example.java文件 + * 该SDK中代码仅供学习和参考,开发者可以自己根据接口文档进行编码实现,而不使用该SDK中的实现 + * 各字段长度、类型等请参考具体接口文档中的定义,SDK不检查这些长度,而直接发送给服务端 + * 重要:各传入参数SDK都不作任何检查、过滤,请务必在传入前进行安全检查或过滤,否则可能会导致安全问题 + * + * @author zengminghua + */ +public class OpenSdk { + + /** + * 文件ID + */ + private static final String FILE_ID = "fileId"; + + /** + * 文件特征值算法 + */ + private static final String ALGORITHM = "SHA-1"; + + /** + * 文件哈希值key + */ + private static final String FILE_HASH_VALUE = "fileHashValue"; + + /** + * SDK请求入口 + *

+ * [重要]单应用配置方式,调用此方法前,请确保已经配置了Configure类中的变量,如keyId等 + * [重要]各传入参数SDK都不作任何检查、过滤,请务必在传入前进行安全检查或过滤,保证传入参数的安全性。 + * + * @param reqUri 请求资源URI + * @param reqMethod 请求方法 + * @param headParams 头部信息请求参数 + * @param urlParams URL请求参数 + * @param bodyParams BODY请求参数 + * @return 请求结果 + */ + public static String gateway(String reqUri, ReqMethodEnum reqMethod, Map headParams, + Map urlParams, Map bodyParams) { + try { + Object result = AbstractTransService.getInstance() + .exec(reqUri, reqMethod, headParams, urlParams, bodyParams, null, Configure.getKeyConfigure(), + null); + return (String) result; + } catch (Exception e) { + SdkException ex = new SdkException(SdkExType.UNKNOWN); + return ex.toString(); + } + } + + /** + * SDK请求入口,form格式 + *

+ * [重要]多应用配置方式,调用此方法前,请确保已经配置了Configure类中的keyConfigures + * [重要]各传入参数SDK都不作任何检查、过滤,请务必在传入前进行安全检查或过滤,保证传入参数的安全性。 + * + * @param reqUri 请求资源URI + * @param reqMethod 请求方法 + * @param headParams 头部信息请求参数 + * @param urlParams URL请求参数 + * @param bodyParams BODY请求参数 + * @param keyId 应用ID + * @return 请求结果 + */ + public static String gatewayWithKeyId(String reqUri, ReqMethodEnum reqMethod, Map headParams, + Map urlParams, Map bodyParams, String keyId) { + try { + KeyConfigure keyConfigure = Configure.getKeyConfigures().get(keyId); + if (null == keyConfigure) { + return new SdkException(SdkExType.KEY_CONFIGURE_ERR).toString(); + } + Object result = AbstractTransService.getInstance() + .exec(reqUri, reqMethod, headParams, urlParams, bodyParams, null, keyConfigure, null); + return (String) result; + } catch (Exception e) { + SdkException ex = new SdkException(SdkExType.UNKNOWN); + return ex.toString(); + } + } + + /** + * SDK请求入口,json格式 + *

+ * [重要]多应用配置方式,调用此方法前,请确保已经配置了Configure类中的keyConfigures + * [重要]各传入参数SDK都不作任何检查、过滤,请务必在传入前进行安全检查或过滤,保证传入参数的安全性。 + * + * @param reqUri 请求资源URI + * @param reqMethod 请求方法 + * @param headParams 头部信息请求参数 + * @param urlParams URL请求参数 + * @param bodyJson BODY请求参数JSON字符串 + * @param keyId 应用ID + * @return 请求结果 + */ + public static String gatewayJsonWithKeyId(String reqUri, ReqMethodEnum reqMethod, Map headParams, + Map urlParams, String bodyJson, String keyId) { + try { + KeyConfigure keyConfigure = Configure.getKeyConfigures().get(keyId); + if (null == keyConfigure) { + return new SdkException(SdkExType.KEY_CONFIGURE_ERR).toString(); + } + if (!SdkUtil.isJSONString(bodyJson)) { + return new SdkException(SdkExType.PARAM_ERR).toString(); + } + Object result = AbstractTransService.getInstance() + .exec(reqUri, reqMethod, headParams, urlParams, null, null, keyConfigure, bodyJson); + return (String) result; + } catch (Exception e) { + SdkException ex = new SdkException(SdkExType.UNKNOWN); + return ex.toString(); + } + } + + /** + * SDK请求入口,form格式 + *

+ * [重要]多应用配置方式,调用此方法前,请确保已经配置了Configure类中的keyConfigures + * [重要]各传入参数SDK都不作任何检查、过滤,请务必在传入前进行安全检查或过滤,保证传入参数的安全性。 + * + * @param reqUri 请求资源URI + * @param reqMethod 请求方法 + * @param headParams 头部信息请求参数 + * @param urlParams URL请求参数 + * @param bodyParams BODY请求参数 + * @param keyId 应用ID + * @return 请求结果 + */ + public static String gatewayNetSignWithKeyId(String reqUri, ReqMethodEnum reqMethod, Map headParams, + Map urlParams, Map bodyParams, String bodyJson, String keyId) { + try { + KeyConfigure keyConfigure = Configure.getKeyConfigures().get(keyId); + if (null == keyConfigure) { + return new SdkException(SdkExType.KEY_CONFIGURE_ERR).toString(); + } + Object result = AbstractTransService.getInstance() + .execNetSign(reqUri, reqMethod, null, null, bodyParams, null, keyConfigure, bodyJson); + return (String) result; + } catch (Exception e) { + SdkException ex = new SdkException(SdkExType.UNKNOWN); + return ex.toString(); + } + } + + /** + * SDK请求入口,文件下载 + *

+ * [重要]单应用配置方式,调用此方法前,请确保已经配置了Configure类中的变量,如keyId等 + * [重要]各传入参数SDK都不作任何检查、过滤,请务必在传入前进行安全检查或过滤,保证传入参数的安全性。 + * + * @param fileId 文件ID + * @return 若下载成功,返回下载的文件的字节数组(byte[]类型) + */ + public static Object downloadFile(String fileId) { + try { + Map urlParams = new HashMap(8); + urlParams.put(FILE_ID, fileId); + + Object result = AbstractTransService.getInstance() + .exec(Const.FILE_DOWNLOAD_URI, ReqMethodEnum.GET, null, urlParams, null, null, + Configure.getKeyConfigure(), null); + /** + * 将下载文件byte[]类型转为String并返回 + */ + /*String fileString = new String((byte[]) result); + return fileString;*/ + return result; + } catch (Exception e) { + SdkException ex = new SdkException(SdkExType.UNKNOWN); + return ex.toString(); + } + } + + /** + * SDK请求入口,文件下载 + *

+ * [重要]多应用配置方式,调用此方法前,请确保已经配置了Configure类中的keyConfigures + * [重要]各传入参数SDK都不作任何检查、过滤,请务必在传入前进行安全检查或过滤,保证传入参数的安全性。 + * + * @param fileId 文件ID + * @param keyId 应用ID + * @return 若下载成功,返回下载的文件的字节数组(byte[]类型) + */ + public static Object downloadFileWithKeyId(String fileId, String keyId) { + try { + KeyConfigure keyConfigure = Configure.getKeyConfigures().get(keyId); + if (null == keyConfigure) { + return new SdkException(SdkExType.KEY_CONFIGURE_ERR).toString(); + } + Map urlParams = new HashMap(8); + urlParams.put(FILE_ID, fileId); + Object result = AbstractTransService.getInstance() + .exec(Const.FILE_DOWNLOAD_URI, ReqMethodEnum.GET, null, urlParams, null, null, keyConfigure, null); + /** + * 将下载文件byte[]类型转为String并返回 + */ + /*String fileString = new String((byte[]) result); + return fileString;*/ + return result; + } catch (Exception e) { + SdkException ex = new SdkException(SdkExType.UNKNOWN); + return ex.toString(); + } + } + + /** + * SDK请求入口,文件下载 + *

+ * [重要]单应用配置方式,调用此方法前,请确保已经配置了Configure类中的变量,如keyId等 + * [重要]各传入参数SDK都不作任何检查、过滤,请务必在传入前进行安全检查或过滤,保证传入参数的安全性。 + * + * @param fileId 文件ID + * @param filePath 文件存储路径 + * @return 若下载成功,返回下载成功json提示字符串(String类型) + */ + public static String downloadFile(String fileId, String filePath) { + try { + Map urlParams = new HashMap(8); + urlParams.put(FILE_ID, fileId); + Object result = AbstractTransService.getInstance() + .exec(Const.FILE_DOWNLOAD_URI, ReqMethodEnum.GET, null, urlParams, null, null, + Configure.getKeyConfigure(), null); + if (result instanceof byte[]) { + FileOutputStream fos = null; + try { + File file = new File(filePath); + fos = new FileOutputStream(file); + fos.write((byte[]) result); + } catch (Exception e) { + SdkException ex = new SdkException(SdkExType.FILE_ERROR_RESULT); + return ex.toString(); + } finally { + if (fos != null) { + try { + fos.close(); + } catch (Exception e) { + + } + } + } + return Const.FILE_SUCCESS_RESULT; + } else { + return (String) result; + } + } catch (Exception e) { + SdkException ex = new SdkException(SdkExType.UNKNOWN); + return ex.toString(); + } + } + + /** + * SDK请求入口,文件下载 + *

+ * [重要]多应用配置方式,调用此方法前,请确保已经配置了Configure类中的keyConfigures + * [重要]各传入参数SDK都不作任何检查、过滤,请务必在传入前进行安全检查或过滤,保证传入参数的安全性。 + * + * @param fileId 文件ID + * @param filePath 文件存储路径 + * @param keyId 应用ID + * @return 若下载成功,返回下载成功json提示字符串(String类型) + */ + public static String downloadFileWithKeyId(String fileId, String filePath, String keyId) { + try { + KeyConfigure keyConfigure = Configure.getKeyConfigures().get(keyId); + if (null == keyConfigure) { + return new SdkException(SdkExType.KEY_CONFIGURE_ERR).toString(); + } + Map urlParams = new HashMap(8); + urlParams.put(FILE_ID, fileId); + Object result = AbstractTransService.getInstance() + .exec(Const.FILE_DOWNLOAD_URI, ReqMethodEnum.GET, null, urlParams, null, null, keyConfigure, null); + if (result instanceof byte[]) { + FileOutputStream fos = null; + try { + File file = new File(filePath); + fos = new FileOutputStream(file); + fos.write((byte[]) result); + } catch (Exception e) { + SdkException ex = new SdkException(SdkExType.FILE_ERROR_RESULT); + return ex.toString(); + } finally { + if (fos != null) { + try { + fos.close(); + } catch (Exception e) { + + } + } + } + return Const.FILE_SUCCESS_RESULT; + } else { + return (String) result; + } + } catch (Exception e) { + SdkException ex = new SdkException(SdkExType.UNKNOWN); + return ex.toString(); + } + } + + /** + * SDK请求入口,文件上传 + *

+ * [重要]单应用配置方式,调用此方法前,请确保已经配置了Configure类中的变量,如keyId等 + * [重要]各传入参数SDK都不作任何检查、过滤,请务必在传入前进行安全检查或过滤,保证传入参数的安全性。 + * [重要]SDK不校验文件大小,开发者请根据实际情况自行限制 + * + * @param filePath 文件路径 + * @return 文件ID + */ + public static String uploadFile(String filePath) { + try { + File file = new File(filePath); + if (!file.exists()) { + SdkException ex = new SdkException(SdkExType.FILE_ERROR_RESULT); + return ex.toString(); + } + // 计算文件的哈希值 + String fileHashValue = FileUtil.fileHash(filePath, ALGORITHM); + Map bodyParams = new HashMap(8); + bodyParams.put(FILE_HASH_VALUE, fileHashValue); + Object result = AbstractTransService.getInstance() + .exec(Const.FILE_UPLOAD_URI, ReqMethodEnum.POST, null, null, bodyParams, filePath, + Configure.getKeyConfigure(), null); + return (String) result; + } catch (Exception e) { + SdkException ex = new SdkException(SdkExType.UNKNOWN); + return ex.toString(); + } + } + + /** + * SDK请求入口,文件上传 + *

+ * [重要]多应用配置方式,调用此方法前,请确保已经配置了Configure类中的keyConfigures + * [重要]各传入参数SDK都不作任何检查、过滤,请务必在传入前进行安全检查或过滤,保证传入参数的安全性。 + * [重要]SDK不校验文件大小,开发者请根据实际情况自行限制 + * + * @param filePath 文件路径 + * @param keyId 应用ID + * @return 文件ID + */ + public static String uploadFileWithKeyId(String filePath, String keyId) { + try { + KeyConfigure keyConfigure = Configure.getKeyConfigures().get(keyId); + if (null == keyConfigure) { + return new SdkException(SdkExType.KEY_CONFIGURE_ERR).toString(); + } + File file = new File(filePath); + if (!file.exists()) { + SdkException ex = new SdkException(SdkExType.FILE_ERROR_RESULT); + return ex.toString(); + } + // 计算文件的哈希值 + String fileHashValue = FileUtil.fileHash(filePath, ALGORITHM); + Map bodyParams = new HashMap(8); + bodyParams.put(FILE_HASH_VALUE, fileHashValue); + Object result = AbstractTransService.getInstance() + .exec(Const.FILE_UPLOAD_URI, ReqMethodEnum.POST, null, null, bodyParams, filePath, keyConfigure, + null); + return (String) result; + } catch (Exception e) { + SdkException ex = new SdkException(SdkExType.UNKNOWN); + return ex.toString(); + } + } +} \ No newline at end of file diff --git a/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/common/HttpsPostRequest.java b/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/common/HttpsPostRequest.java new file mode 100644 index 00000000..df2f2072 --- /dev/null +++ b/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/common/HttpsPostRequest.java @@ -0,0 +1,374 @@ +package com.cib.fintech.dfp.open.sdk.common; + +import com.cib.fintech.dfp.open.sdk.config.Configure; +import com.cib.fintech.dfp.open.sdk.config.GrayConfigure; +import com.cib.fintech.dfp.open.sdk.config.KeyConfigure; +import com.cib.fintech.dfp.open.sdk.enums.ReqMethodEnum; +import com.cib.fintech.dfp.open.sdk.exception.SdkExType; +import com.cib.fintech.dfp.open.sdk.exception.SdkException; +import com.cib.fintech.dfp.open.sdk.service.AbstractTransService; +import com.cib.fintech.dfp.open.sdk.util.Const; +import com.cib.fintech.dfp.open.sdk.util.SdkUtil; +import com.cib.fintech.dfp.open.sdk.util.VerifyRespSignature; + +import javax.net.ssl.*; +import java.io.*; +import java.net.HttpURLConnection; +import java.net.URL; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +/** + * 使用URLConnection的通讯器 + * + * @author zengminghua, tangchongcan + */ +public class HttpsPostRequest extends AbstractTransService { + + private static final int BUFFER_SIZE = 2048; + + private static final String HEADER_TIMESTAMP = "Timestamp"; + + private static final String HEADER_SIGNATURE = "Signature"; + + private static final String HEADER_NONCE = "Nonce"; + + private static final String HTTPS = "https"; + + /** + * request头和上传文件内容的分隔符 + */ + private static final String BOUNDARY = "---------------------------543279124964751"; + + private static final String APPLICATION_OCTET_STREAM = "application/octet-stream"; + + private static TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() { + @Override + public java.security.cert.X509Certificate[] getAcceptedIssuers() { + return null; + } + + @Override + public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) { + } + + @Override + public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) { + } + }}; + + private static HostnameVerifier hv = new HostnameVerifier() { + @Override + public boolean verify(String hostname, SSLSession session) { + return true; + } + }; + + private static SSLContext sc; + private static SSLSocketFactory sf; + + static { + try { + sc = SSLContext.getInstance("SSL"); + sc.init(null, trustAllCerts, new java.security.SecureRandom()); + sf = sc.getSocketFactory(); + } catch (NoSuchAlgorithmException | KeyManagementException e) { + } + } + + private static Map SFS = new HashMap<>(); + private static Object LOCK = new Object(); + private static boolean SSL_INIT_STATUS = false; + + private void initSSLContents() { + if (!SSL_INIT_STATUS) { + synchronized (LOCK) { + if (!SSL_INIT_STATUS) { + Map keyConfigures = Configure.getKeyConfigures(); + for (Map.Entry entry : keyConfigures.entrySet()) { + String keyId = entry.getKey(); + FileInputStream fis = null; + try { + SSLContext sslContext = SSLContext.getInstance("SSL"); + KeyStore keyStore = KeyStore.getInstance("PKCS12"); + fis = new FileInputStream(new File(Configure.getCertPath(), keyId + ".p12")); + char[] password = entry.getValue().getCertProtectionPwd() == null ? + null : + entry.getValue().getCertProtectionPwd().toCharArray(); + keyStore.load(fis, password); + KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); + kmf.init(keyStore, password); + sslContext.init(kmf.getKeyManagers(), null, new SecureRandom()); + SFS.put(keyId, sslContext.getSocketFactory()); + } catch (Exception e) { + + } finally { + if (fis != null) { + try { + fis.close(); + } catch (IOException e) { + + } + } + } + } + SSL_INIT_STATUS = true; + } + } + } + } + + @Override + public Object send(String url, ReqMethodEnum method, String authInfo, Map headParams, + String bodyParamString, String filePath, Map bodyParamMap, KeyConfigure keyConfigure, + String bodyJson) throws Exception { + + // 通讯公共部分 + URL connUrl = null; + HttpURLConnection conn = null; + Object retn = null; + + // 打开URL连接 + connUrl = new URL(url); + conn = (HttpURLConnection) connUrl.openConnection(); + + // 测试环境忽略SSL证书验证 + if (Configure.isDevEnv() && HTTPS.equals(connUrl.getProtocol())) { + ignoreSSLVerify((HttpsURLConnection) conn); + } + if (Configure.getCertPath() != null) { + initSSLContents(); + SSLSocketFactory sslSocketFactory = SFS.get(keyConfigure.getKeyId()); + if (sslSocketFactory != null) { + ((HttpsURLConnection) conn).setSSLSocketFactory(sslSocketFactory); + } + } + + // 设置HEAD信息 + if (keyConfigure.getEnterpriseBearer() != null) { + authInfo = authInfo + "," + keyConfigure.getEnterpriseBearer(); + } + conn.setRequestProperty(Const.POST_HEAD_KEY, authInfo); + if (keyConfigure.getXCfcaBasic() != null) { + conn.setRequestProperty("X-Cfca-Basic", keyConfigure.getXCfcaBasic()); + } + if (headParams != null) { + for (String key : headParams.keySet()) { + conn.setRequestProperty(key, headParams.get(key)); + } + } + + // 灰度发布配置添加到请求头中 + GrayConfigure grayConfigure = keyConfigure.getGrayConfigure(); + if (null != grayConfigure && grayConfigure.notNull()) { + conn.setRequestProperty(grayConfigure.getKey(), grayConfigure.getValue()); + } + + // 响应验签相关数据 + String timestamp = ""; + String responseSignValue = ""; + String responseNonce = ""; + + // 非文件上传 + if (null == filePath) { + + OutputStream outStream = null; + InputStream inStream = null; + + if (SdkUtil.isJSONString(bodyJson)) { + bodyParamString = bodyJson; + conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8"); + } + + try { + switch (method) { + case POST: + conn.setRequestMethod(ReqMethodEnum.POST.toString()); + conn.setDoOutput(true); + outStream = conn.getOutputStream(); + outStream.write(bodyParamString.getBytes(Const.CHARSET)); + outStream.flush(); + break; + case PUT: + conn.setRequestMethod(ReqMethodEnum.PUT.toString()); + conn.setDoOutput(true); + outStream = conn.getOutputStream(); + outStream.write(bodyParamString.getBytes(Const.CHARSET)); + outStream.flush(); + break; + case DELETE: + conn.setRequestMethod(ReqMethodEnum.DELETE.toString()); + conn.setDoOutput(true); + outStream = conn.getOutputStream(); + outStream.write(bodyParamString.getBytes(Const.CHARSET)); + outStream.flush(); + break; + default: + break; + } + + // 获取返回结果 + int code = conn.getResponseCode(); + if (code >= HttpURLConnection.HTTP_OK && code < HttpURLConnection.HTTP_MULT_CHOICE && code != 0) { + inStream = conn.getInputStream(); + } else { + inStream = conn.getErrorStream(); + } + + byte[] bin = readInputStream(inStream); + if (APPLICATION_OCTET_STREAM.equals(conn.getContentType())) { + retn = bin; + } else { + retn = new String(bin, Const.CHARSET); + } + + timestamp = conn.getHeaderField(HEADER_TIMESTAMP); + responseSignValue = conn.getHeaderField(HEADER_SIGNATURE); + responseNonce = conn.getHeaderField(HEADER_NONCE); + } finally { + if (inStream != null) { + inStream.close(); + } + if (outStream != null) { + outStream.close(); + } + if (conn != null) { + conn.disconnect(); + } + } + + } else { + // 文件上传 + try { + conn.setRequestMethod(ReqMethodEnum.POST.toString()); + + conn.setConnectTimeout(5000); + conn.setReadTimeout(30000); + conn.setDoOutput(true); + conn.setDoInput(true); + conn.setUseCaches(false); + + conn.setRequestProperty("Connection", "Keep-Alive"); + conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + BOUNDARY); + + OutputStream out = new DataOutputStream(conn.getOutputStream()); + + // text + if (null != bodyParamMap) { + StringBuffer strBuf = new StringBuffer(); + Iterator> iter = bodyParamMap.entrySet().iterator(); + while (iter.hasNext()) { + Map.Entry entry = iter.next(); + String inputName = entry.getKey(); + String inputValue = entry.getValue(); + if (inputValue == null) { + continue; + } + strBuf.append("\r\n").append("--").append(BOUNDARY).append("\r\n"); + strBuf.append("Content-Disposition: form-data; name=\"" + inputName + "\"\r\n\r\n"); + strBuf.append(inputValue); + } + out.write(strBuf.toString().getBytes()); + } + + // file + File file = new File(filePath); + String filename = file.getName(); + String inputName = "file"; + + StringBuffer strBuf = new StringBuffer(); + strBuf.append("\r\n").append("--").append(BOUNDARY).append("\r\n"); + strBuf.append("Content-Disposition: form-data; name=\"" + inputName + "\"; filename=\"" + filename + + "\"\r\n\r\n"); + + out.write(strBuf.toString().getBytes()); + FileInputStream fileInputStream = new FileInputStream(file); + DataInputStream in = new DataInputStream(fileInputStream); + int bytes = 0; + byte[] bufferOut = new byte[1024]; + while ((bytes = in.read(bufferOut)) != -1) { + out.write(bufferOut, 0, bytes); + } + in.close(); + fileInputStream.close(); + + byte[] endData = ("\r\n--" + BOUNDARY + "--\r\n").getBytes(); + out.write(endData); + out.flush(); + out.close(); + + // 获取返回结果 + InputStream inStream = null; + int code = conn.getResponseCode(); + if (code >= HttpURLConnection.HTTP_OK && code < HttpURLConnection.HTTP_MULT_CHOICE && code != 0) { + inStream = conn.getInputStream(); + } else { + inStream = conn.getErrorStream(); + } + + // 读取返回数据 + strBuf = new StringBuffer(); + InputStreamReader inputStreamReader = new InputStreamReader(inStream); + BufferedReader reader = new BufferedReader(inputStreamReader); + String line = null; + while ((line = reader.readLine()) != null) { + strBuf.append(line); + } + retn = strBuf.toString(); + reader.close(); + reader = null; + inputStreamReader.close(); + inputStreamReader = null; + + timestamp = conn.getHeaderField(HEADER_TIMESTAMP); + responseSignValue = conn.getHeaderField(HEADER_SIGNATURE); + responseNonce = conn.getHeaderField(HEADER_NONCE); + } catch (FileNotFoundException e) { + SdkException ex = new SdkException(SdkExType.FILE_ERROR_RESULT); + return ex.toString(); + } catch (IOException e) { + SdkException ex = new SdkException(SdkExType.CONN_ERR); + return ex.toString(); + } finally { + if (conn != null) { + conn.disconnect(); + } + } + } + + // 响应报文验签开关非关闭,且响应报文不包含OPEN异常时,执行验签 + if (keyConfigure.isRespSignSwitch() && !retn.toString().contains(Const.RESP_EXCEPTION_CODE)) { + VerifyRespSignature.verify(timestamp, responseNonce, responseSignValue, retn, keyConfigure); + } + + return retn; + } + + private static byte[] readInputStream(InputStream inStream) throws IOException { + ByteArrayOutputStream outStream = new ByteArrayOutputStream(); + byte[] data = null; + byte[] buffer = new byte[BUFFER_SIZE]; + int len = 0; + try { + while ((len = inStream.read(buffer)) != -1) { + outStream.write(buffer, 0, len); + } + data = outStream.toByteArray(); + } finally { + if (outStream != null) { + outStream.close(); + } + } + return data; + } + + private static void ignoreSSLVerify(HttpsURLConnection conn) { + conn.setSSLSocketFactory(sf); + conn.setHostnameVerifier(hv); + } +} diff --git a/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/config/Configure.java b/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/config/Configure.java new file mode 100644 index 00000000..72f29d43 --- /dev/null +++ b/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/config/Configure.java @@ -0,0 +1,250 @@ +package com.cib.fintech.dfp.open.sdk.config; + +import com.cib.fintech.dfp.open.sdk.enums.KeySignTypeEnum; +import com.cib.fintech.dfp.open.sdk.enums.RespSignAlgorithmEnum; + +import java.util.HashMap; +import java.util.Map; + +/** + * 开放银行平台配置类 + *

+ * 这里存放所有配置,开发者可以扩展该类 + * 注意:请根据采用的配置方式调用OpenSdk类中对应的方法 + * 1.采用单应用配置方式时,请务必配置该类中的变量,如keyId等 + * 2.采用多应用配置方式时,请务必配置该类中的keyConfigures + * + * @author zengminghua + */ +public class Configure { + + /********************************************** ↓ 开发者不可修改 ↓ *************************************/ + + /** + * 开放银行平台生产环境地址,无需修改 + */ + public static final String PROD_URL = "https://open.cibfintech.com"; + + /*********************************************** ↓ 开发者可修改 ↓ **************************************/ + + /** + * 是否测试环境开关,true连接测试环境,false连接生产环境 + */ + private static boolean devEnv = true; + + /** + * 开放银行平台测试环境地址,可根据需要修改 + */ + private static String devUrl = "https://open.test.cibfintech.com"; + + /** + * 应用ID,为KY字母开头的字符串,区分大小写 + */ + private static String keyId = "KYONLYFORTEST012345678"; + + /** + * 请求报文签名私钥,需严格保密,建议使用配置文件或其他方式进行存储 + */ + private static String priKey = "AK7JUorRStFAwM1+BXiHefwmCOZXjVU42b6Oa5JFf5Y7"; + + /** + * 响应报文验签公钥 + */ + private static String respPubKey = "BAQl3uq+FxAGi1/vAZpWyBhdTqBUcenUm8jdPl59XHOeyFkSU3obseTJOTms7lfl0rFALHVTrOmV39N97Z2ZOVI="; + + /** + * 字段加密密钥 + */ + private static String reqParamEncryptKey = "9A9A4QyNz/pc8kZZXzhXOQ=="; + + /** + * 请求签名算法 + */ + private static KeySignTypeEnum keySignType = KeySignTypeEnum.SM3WITHSM2; + + /** + * 响应报文验签开关,true将进行验签,false不进行验签 + */ + private static boolean respSignSwitch = false; + + /** + * 响应报文验签算法 + */ + private static RespSignAlgorithmEnum respSignAlgorithm = RespSignAlgorithmEnum.SM3WITHSM2; + + /** + * HTTP通讯类,如需使用自己的类库,请修改该值 + */ + private static String httpsRequestClassName = "com.cib.fintech.dfp.open.sdk.common.HttpsPostRequest"; + + /** + * 多应用配置方式时,创建KeyConfigure对象存入map中。示例:map{key=keyId,value=keyConfigure} + */ + private static Map keyConfigures = new HashMap<>(8); + + /** + * 双向认证证书文件存放路径,不为空将开启双向认证 + * 该路径下证书文件名格式要求:{keyId}.p12,如KYONLYFORTEST012345678.p12 + */ + private static String certPath = null; + + /** + * 双向认证证书私钥的保护密码,用于提取证书私钥。双向认证开启时必须设置该项 + */ + private static String certProtectionPwd = null; + + /** + * 灰度发布标识参数 + */ + private static GrayConfigure grayConfigure = null; + + /*****************************************************************************************/ + + private static final Object GET_KEYCONFIGURE_LOCK = new Object(); + + public static String getKeyId() { + return keyId; + } + + public static void setKeyId(String keyId) { + Configure.keyId = keyId; + } + + public static String getPriKey() { + if (priKey.startsWith("-----BEGIN PRIVATE KEY-----")) { + priKey = priKey.replace("-----BEGIN PRIVATE KEY-----", "").replace("-----END PRIVATE KEY-----", ""); + } + return priKey; + } + + public static void setPriKey(String priKey) { + Configure.priKey = priKey; + } + + public static KeySignTypeEnum getKeySignType() { + return keySignType; + } + + public static void setKeySignType(KeySignTypeEnum keySignType) { + Configure.keySignType = keySignType; + } + + public static boolean isRespSignSwitch() { + return respSignSwitch; + } + + public static void setRespSignSwitch(boolean respSignSwitch) { + Configure.respSignSwitch = respSignSwitch; + } + + public static RespSignAlgorithmEnum getRespSignAlgorithm() { + return respSignAlgorithm; + } + + public static void setRespSignAlgorithm(RespSignAlgorithmEnum respSignAlgorithm) { + Configure.respSignAlgorithm = respSignAlgorithm; + } + + public static String getRespPubKey() { + return respPubKey; + } + + public static void setRespPubKey(String respPubKey) { + Configure.respPubKey = respPubKey; + } + + public static String getReqParamEncryptKey() { + return reqParamEncryptKey; + } + + public static void setReqParamEncryptKey(String reqParamEncryptKey) { + Configure.reqParamEncryptKey = reqParamEncryptKey; + } + + public static String getHttpsRequestClassName() { + return httpsRequestClassName; + } + + public static void setHttpsRequestClassName(String httpsRequestClassName) { + Configure.httpsRequestClassName = httpsRequestClassName; + } + + public static boolean isDevEnv() { + return devEnv; + } + + public static void setDevEnv(boolean devEnv) { + Configure.devEnv = devEnv; + } + + public static String getDevUrl() { + return devUrl; + } + + public static void setDevUrl(String devUrl) { + Configure.devUrl = devUrl; + } + + public static Map getKeyConfigures() { + return keyConfigures; + } + + public static void setKeyConfigures(Map keyConfigures) { + Configure.keyConfigures = keyConfigures; + } + + public static String getCertPath() { + return certPath; + } + + public static void setCertPath(String certPath) { + Configure.certPath = certPath; + } + + public static String getCertProtectionPwd() { + return certProtectionPwd; + } + + public static void setCertProtectionPwd(String certProtectionPwd) { + Configure.certProtectionPwd = certProtectionPwd; + } + + public static GrayConfigure getGrayConfigure() { + return grayConfigure; + } + + public static void setGrayConfigure(GrayConfigure grayConfigure) { + Configure.grayConfigure = grayConfigure; + } + + /** + * 采用单应用配置方式时,获取配置对象方法 + * + * @return keyConfigure + */ + public static KeyConfigure getKeyConfigure() { + KeyConfigure keyConfigure = keyConfigures.get(keyId); + if (null != keyConfigure) { + return keyConfigure; + } + synchronized (GET_KEYCONFIGURE_LOCK) { + keyConfigure = keyConfigures.get(keyId); + if (null != keyConfigure) { + return keyConfigure; + } + keyConfigure = new KeyConfigure(); + keyConfigure.setKeyId(keyId); + keyConfigure.setPriKey(priKey); + keyConfigure.setKeySignType(keySignType); + keyConfigure.setRespSignSwitch(respSignSwitch); + keyConfigure.setRespSignAlgorithm(respSignAlgorithm); + keyConfigure.setRespPubKey(respPubKey); + keyConfigure.setReqParamEncryptKey(reqParamEncryptKey); + keyConfigure.setCertProtectionPwd(certProtectionPwd); + keyConfigure.setGrayConfigure(grayConfigure); + keyConfigures.put(keyId, keyConfigure); + } + return keyConfigure; + } + +} diff --git a/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/config/GrayConfigure.java b/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/config/GrayConfigure.java new file mode 100644 index 00000000..c78a6bef --- /dev/null +++ b/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/config/GrayConfigure.java @@ -0,0 +1,50 @@ +package com.cib.fintech.dfp.open.sdk.config; + +/** + * 灰度发布配置对象 + * + * @author zengminghua + */ +public class GrayConfigure { + + /** + * 请求头中的灰度参数键 + */ + private String key; + + /** + * 请求头中的灰度参数值 + */ + private String value; + + public GrayConfigure() { + } + + public GrayConfigure(String key, String value) { + this.key = key; + this.value = value; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public boolean notNull() { + if (null != this.key && null != this.value) { + return true; + } + return false; + } +} diff --git a/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/config/KeyConfigure.java b/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/config/KeyConfigure.java new file mode 100644 index 00000000..58594f6a --- /dev/null +++ b/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/config/KeyConfigure.java @@ -0,0 +1,232 @@ +package com.cib.fintech.dfp.open.sdk.config; + +import com.cib.fintech.dfp.open.sdk.enums.KeySignTypeEnum; +import com.cib.fintech.dfp.open.sdk.enums.RespSignAlgorithmEnum; +import com.cib.fintech.dfp.open.sdk.util.Base64; + +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.text.SimpleDateFormat; +import java.util.Enumeration; + +/** + * 应用配置对象 + * + * @author zengminghua + */ +public class KeyConfigure { + + /** + * 应用ID,为KY字母开头的字符串,区分大小写 + */ + private String keyId; + + /** + * 请求报文签名私钥,需严格保密,建议使用配置文件或其他方式进行存储 + */ + private String priKey; + + /** + * 响应报文验签公钥 + */ + private String respPubKey; + + /** + * 字段加密密钥 + */ + private String reqParamEncryptKey; + + /** + * 请求签名算法 + */ + private KeySignTypeEnum keySignType = KeySignTypeEnum.SM3WITHSM2; + + /** + * 响应报文验签开关,true将进行验签,false不进行验签 + */ + private boolean respSignSwitch = false; + + /** + * 响应报文验签算法 + */ + private RespSignAlgorithmEnum respSignAlgorithm = RespSignAlgorithmEnum.SM3WITHSM2; + + /** + * 双向认证证书私钥的保护密码,用于提取证书私钥。双向认证开启时必须设置该项 + */ + private String certProtectionPwd = null; + + /** + * 灰度发布标识参数 + */ + private GrayConfigure grayConfigure = null; + + /** + * CFCA证书基本信息头 + */ + private String xCfcaBasic = null; + + /** + * 企业客户授权信息 + */ + private String enterpriseBearer = null; + + public String getKeyId() { + return keyId; + } + + public void setKeyId(String keyId) { + this.keyId = keyId; + } + + public String getPriKey() { + return priKey; + } + + public void setPriKey(String priKey) { + this.priKey = priKey; + } + + public KeySignTypeEnum getKeySignType() { + return keySignType; + } + + public void setKeySignType(KeySignTypeEnum keySignType) { + this.keySignType = keySignType; + } + + public boolean isRespSignSwitch() { + return respSignSwitch; + } + + public void setRespSignSwitch(boolean respSignSwitch) { + this.respSignSwitch = respSignSwitch; + } + + public RespSignAlgorithmEnum getRespSignAlgorithm() { + return respSignAlgorithm; + } + + public void setRespSignAlgorithm(RespSignAlgorithmEnum respSignAlgorithm) { + this.respSignAlgorithm = respSignAlgorithm; + } + + public String getRespPubKey() { + return respPubKey; + } + + public void setRespPubKey(String respPubKey) { + this.respPubKey = respPubKey; + } + + public String getReqParamEncryptKey() { + return reqParamEncryptKey; + } + + public void setReqParamEncryptKey(String reqParamEncryptKey) { + this.reqParamEncryptKey = reqParamEncryptKey; + } + + public String getCertProtectionPwd() { + return certProtectionPwd; + } + + public void setCertProtectionPwd(String certProtectionPwd) { + this.certProtectionPwd = certProtectionPwd; + } + + public KeyConfigure() { + } + + public KeyConfigure(String keyId, String priKey, KeySignTypeEnum keySignType, boolean respSignSwitch, + RespSignAlgorithmEnum respSignAlgorithm, String respPubKey, String reqParamEncryptKey, + String certProtectionPwd, GrayConfigure grayConfigure) { + this.keyId = keyId; + this.priKey = priKey; + this.keySignType = keySignType; + this.respSignSwitch = respSignSwitch; + this.respSignAlgorithm = respSignAlgorithm; + this.respPubKey = respPubKey; + this.reqParamEncryptKey = reqParamEncryptKey; + this.certProtectionPwd = certProtectionPwd; + this.grayConfigure = grayConfigure; + } + + public GrayConfigure getGrayConfigure() { + return grayConfigure; + } + + public void setGrayConfigure(GrayConfigure grayConfigure) { + this.grayConfigure = grayConfigure; + } + + /** + * @param cfcaCertFilename CFCA证书文件名(全路径) + * @param cfcaProtectionPwd CFCA证书私钥的保护密码,用于提取CFCA证书私钥 + * @param keySignType 签名算法 + * @return + */ + public void setCfcaProperties(String cfcaCertFilename, String cfcaProtectionPwd, KeySignTypeEnum keySignType) { + KeyStore keyStore; + FileInputStream fis = null; + SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss"); + try { + keyStore = KeyStore.getInstance("PKCS12"); + fis = new FileInputStream(cfcaCertFilename); + char[] password = cfcaProtectionPwd == null ? null : cfcaProtectionPwd.toCharArray(); + keyStore.load(fis, password); + Enumeration aliases = keyStore.aliases(); + while (aliases.hasMoreElements()) { + String alias = aliases.nextElement(); + X509Certificate x509Certificate = (X509Certificate) keyStore.getCertificate(alias); + StringBuilder sb = new StringBuilder(); + sb.append(x509Certificate.getSerialNumber()).append("|"); + sb.append(x509Certificate.getSigAlgName()).append("|"); + sb.append(x509Certificate.getIssuerDN()).append("|"); + sb.append(x509Certificate.getSubjectDN()).append("|"); + sb.append(sdf.format(x509Certificate.getNotBefore())).append("-") + .append(sdf.format(x509Certificate.getNotAfter())); + this.xCfcaBasic = Base64.encode(sb.toString().getBytes(StandardCharsets.UTF_8)); + PrivateKey privateKey = (PrivateKey) keyStore.getKey(alias, password); + this.priKey = Base64.encode(privateKey.getEncoded()); + this.keySignType = keySignType; + break; + } + } catch (Exception e) { + + } finally { + if (fis != null) { + try { + fis.close(); + } catch (IOException e) { + + } + } + } + } + + public String getXCfcaBasic() { + return xCfcaBasic; + } + + public String getEnterpriseBearer() { + return enterpriseBearer; + } + + /** + * @param enterpriseId 企业ID + * @param productId 产品ID + * @param acctNo 企业账户号 + */ + public void setEnterpriseBearer(String enterpriseId, String productId, String acctNo) { + StringBuilder sb = new StringBuilder(); + sb.append(enterpriseId).append("&"); + sb.append(productId).append("&"); + sb.append(acctNo); + this.enterpriseBearer = "Bearer " + Base64.encode(sb.toString().getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/enums/KeySignTypeEnum.java b/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/enums/KeySignTypeEnum.java new file mode 100644 index 00000000..c335b3a5 --- /dev/null +++ b/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/enums/KeySignTypeEnum.java @@ -0,0 +1,60 @@ +package com.cib.fintech.dfp.open.sdk.enums; + +/** + * 应用请求报文签名算法枚举 + * + * @author tangchongcan, zengminghua + */ +public enum KeySignTypeEnum { + + /** + * SHA256WITHRSA("2", "SHA256withRSA") + */ + SHA256WITHRSA("2", "SHA256WithRSA"), + + /** + * SM3WITHSM2("3", "SM3WithSM2") + */ + SM3WITHSM2("3", "SM3WithSM2"); + + /** + * 根据编码找枚举 + * + * @param code 编码 + * @return KeyStatusEnum + */ + public static KeySignTypeEnum getByCode(String code) { + if (code == null) { + return null; + } + if (values() == null) { + return null; + } + for (KeySignTypeEnum t : values()) { + if (t.getCode().equals(code)) { + return t; + } + } + return null; + } + + private final String code; + private final String label; + + KeySignTypeEnum(String code, String label) { + this.code = code; + this.label = label; + } + + public String getCode() { + return code; + } + + public String getLabel() { + return label; + } + + public KeySignTypeEnum returnEnum(String persistedValue) { + return getByCode(persistedValue); + } +} diff --git a/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/enums/ReqMethodEnum.java b/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/enums/ReqMethodEnum.java new file mode 100644 index 00000000..e417daf0 --- /dev/null +++ b/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/enums/ReqMethodEnum.java @@ -0,0 +1,14 @@ +package com.cib.fintech.dfp.open.sdk.enums; + +/** + * HTTP请求方法枚举 + * + * @author zengminghua + */ +public enum ReqMethodEnum { + + /** + * HTTP请求方法枚举 + */ + POST, GET, PUT, DELETE; +} diff --git a/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/enums/RespSignAlgorithmEnum.java b/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/enums/RespSignAlgorithmEnum.java new file mode 100644 index 00000000..6f592ea6 --- /dev/null +++ b/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/enums/RespSignAlgorithmEnum.java @@ -0,0 +1,65 @@ +package com.cib.fintech.dfp.open.sdk.enums; + +/** + * 响应报文验签算法枚举 + * + * @author zengminghua + */ +public enum RespSignAlgorithmEnum { + + /** + * SHA1WITHRSA("1", "SHA1withRSA") + */ + SHA1WITHRSA("1", "SHA1WithRSA"), + + /** + * SHA256WITHRSA("2", "SHA256withRSA") + */ + SHA256WITHRSA("2", "SHA256WithRSA"), + + /** + * SM3WITHSM2("3", "SM3WithSM2") + */ + SM3WITHSM2("3", "SM3WithSM2"); + + /** + * 根据编码找枚举 + * + * @param code 编码 + * @return KeyStatusEnum + */ + public static RespSignAlgorithmEnum getByCode(String code) { + if (code == null) { + return null; + } + if (values() == null) { + return null; + } + for (RespSignAlgorithmEnum t : values()) { + if (t.getCode().equals(code)) { + return t; + } + } + return null; + } + + private final String code; + private final String label; + + private RespSignAlgorithmEnum(String code, String label) { + this.code = code; + this.label = label; + } + + public String getCode() { + return code; + } + + public String getLabel() { + return label; + } + + public RespSignAlgorithmEnum returnEnum(String persistedValue) { + return getByCode(persistedValue); + } +} diff --git a/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/exception/SdkExType.java b/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/exception/SdkExType.java new file mode 100644 index 00000000..14ca1af5 --- /dev/null +++ b/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/exception/SdkExType.java @@ -0,0 +1,75 @@ +package com.cib.fintech.dfp.open.sdk.exception; + +/** + * SDK异常枚举 + * + * @author zengminghua + */ +public enum SdkExType { + + /** + * 异常码枚举 + */ + CONN_ERR("OPEN25801", "通讯错误或超时,交易未决"), + + PARAM_ERR("OPEN25802", "参数校验错误"), + + UNKNOWN("OPEN25803", "未知错误,请检查是否为最新版本SDK或是否配置错误"), + + RESPONSE_SIGN_ERR("OPEN25804", "响应报文验签失败,请检查验签公钥或验签算法配置"), + + FILE_ERROR_RESULT("OPEN25805", "文件读取或写入失败"), + + REQ_PARAM_ENCRYPT_ERR("OPEN25806", "字段加/解密失败,请检查字段加密密钥或字段值"), + + KEY_CONFIGURE_ERR("OPEN25807", "当前keyId未找到配置对象信息,请检查keyConfigures集合中是否保存"), + + CALLBACK_REQUEST_VERIFY_ERR("OPEN25808", "回调请求报文验签失败,请检查验签公钥或验签算法配置"), + + CALLBACK_RESPONSE_SIGN_ERR("OPEN25809", "回调响应报文签名失败,请检查签名私钥或签名算法配置"), + + CALLBACK_BODY_ENCRYPT_ERR("OPEN25810", "回调报文加/解密失败,请检查报文加/解密钥或报文值"), + + NET_SIGN_ERR("OPEN25812", "数字信封制作失败,请检查签名/验签密钥或加密算法配置"); + + /** + * 根据编码找枚举 + * + * @param code 编码 + * @return SdkExType + */ + public static SdkExType getByCode(String code) { + if (code == null) { + return null; + } + + for (SdkExType t : values()) { + if (t.getCode().equals(code)) { + return t; + } + } + return null; + } + + private final String code; + + private final String msg; + + private SdkExType(String code, String msg) { + this.code = code; + this.msg = msg; + } + + public String getCode() { + return code; + } + + public String getMsg() { + return msg; + } + + public SdkExType returnEnum(String persistedValue) { + return getByCode(persistedValue); + } + +} diff --git a/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/exception/SdkException.java b/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/exception/SdkException.java new file mode 100644 index 00000000..69376dda --- /dev/null +++ b/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/exception/SdkException.java @@ -0,0 +1,62 @@ +package com.cib.fintech.dfp.open.sdk.exception; + +/** + * SDK异常 + * + * @author zengminghua + */ +public class SdkException extends Exception { + + private static final long serialVersionUID = 7634266621864608219L; + + /** + * 异常码 + */ + private String code; + + /** + * 异常信息 + */ + private String msg; + + /** + * 异常跟踪ID + */ + private String traceId; + + public SdkException(SdkExType code) { + this.code = code.getCode(); + this.msg = code.getMsg(); + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getMsg() { + return msg; + } + + public void setMsg(String msg) { + this.msg = msg; + } + + public String getTraceId() { + return traceId; + } + + public void setTraceId(String traceId) { + this.traceId = traceId; + } + + @Override + public String toString() { + return "{\"code\":\"" + code + "\",\"msg\":\"[" + code + "]" + msg + "\",\"traceId\":\"OPEN-00-LOCAL-" + code + .substring(code.length() - 3, code.length()) + "\"}"; + } + +} diff --git a/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/service/AbstractTransService.java b/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/service/AbstractTransService.java new file mode 100644 index 00000000..ba324dd0 --- /dev/null +++ b/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/service/AbstractTransService.java @@ -0,0 +1,368 @@ +package com.cib.fintech.dfp.open.sdk.service; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.cib.fintech.dfp.open.sdk.config.Configure; +import com.cib.fintech.dfp.open.sdk.config.KeyConfigure; +import com.cib.fintech.dfp.open.sdk.enums.KeySignTypeEnum; +import com.cib.fintech.dfp.open.sdk.enums.ReqMethodEnum; +import com.cib.fintech.dfp.open.sdk.exception.SdkExType; +import com.cib.fintech.dfp.open.sdk.exception.SdkException; +import com.cib.fintech.dfp.open.sdk.util.*; +import com.cib.fintech.dfp.open.sdk.util.Base64; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.*; + +/** + * 通用请求器接口 + *

+ * 如果需要使用自己的HTTP通讯库,请继承自该接口 + * + * @author zengminghua + */ +public abstract class AbstractTransService { + + /** + * 发送请求 + * + * @param url 请求地址 + * @param method 请求方法 + * @param authInfo 请求头部签名信息 + * @param headParams 头部请求参数集合 + * @param bodyParam BODY请求参数 + * @param filePath 文件路径 + * @param bodyParamMap BODY请求参数集合 + * @param keyConfigure 应用配置对象 + * @param bodyJson BODY请求参数JSON字符串 + * @return 通讯返回结果 + * @throws Exception + */ + public abstract Object send(String url, ReqMethodEnum method, String authInfo, Map headParams, + String bodyParam, String filePath, Map bodyParamMap, KeyConfigure keyConfigure, + String bodyJson) throws Exception; + + /** + * 获取Configure指定的通讯类实例 + * + * @return 通讯类实例 + * @throws ClassNotFoundException + * @throws InstantiationException + * @throws IllegalAccessException + */ + @SuppressWarnings("rawtypes") + public static AbstractTransService getInstance() + throws ClassNotFoundException, InstantiationException, IllegalAccessException { + Class c = Class.forName(Configure.getHttpsRequestClassName()); + return (AbstractTransService) c.newInstance(); + } + + /** + * 通讯接口 + * + * @param url 请求地址 + * @param method 请求方法 + * @param authInfo 请求头部签名信息 + * @param headParams 头部请求参数 + * @param bodyParamString BODY请求参数 + * @param filePath 文件路径 + * @param bodyParamMap BODY请求参数集合 + * @param keyConfigure 应用配置对象 + * @param bodyJson BODY请求参数JSON字符串 + * @return 通讯返回字符串 + */ + protected static Object txn(String url, ReqMethodEnum method, String authInfo, Map headParams, + String bodyParamString, String filePath, Map bodyParamMap, KeyConfigure keyConfigure, + String bodyJson) throws SdkException { + Object resp = null; + try { + AbstractTransService transService = AbstractTransService.getInstance(); + resp = transService + .send(url, method, authInfo, headParams, bodyParamString, filePath, bodyParamMap, keyConfigure, + bodyJson); + } catch (Exception e) { + if (e instanceof IOException) { + throw new SdkException(SdkExType.CONN_ERR); + } else if (e instanceof SdkException) { + throw (SdkException) e; + } else { + throw new SdkException(SdkExType.UNKNOWN); + } + } + return resp; + } + + /** + * 获取请求地址 + * + * @return + */ + protected String getPostUrl() { + String url = Configure.isDevEnv() ? Configure.getDevUrl() : Configure.PROD_URL; + return (SdkUtil.notBlank(url) && url.endsWith("/")) ? url.substring(0, url.length() - 1) : url; + } + + /** + * 计算头部签名信息 + * + * @param method 请求方法 + * @param uri 请求URI + * @param signParams 参数集合 + * @param keyConfigure 应用配置对象 + * @return 头部认证信息内容 + */ + protected String getAuthInfo(ReqMethodEnum method, String uri, Map signParams, + KeyConfigure keyConfigure) { + String timestamp = SdkUtil.getDateTime(); + + // 待签参数拼接 + String nonce = UUID.randomUUID().toString().replaceAll("-", ""); + StringBuilder builder = new StringBuilder(); + builder.append(keyConfigure.getKeyId()); + builder.append("&"); + builder.append(timestamp); + builder.append("&"); + builder.append(nonce); + builder.append("&"); + builder.append(method); + builder.append("&"); + builder.append(uri); + if (signParams != null && signParams.size() != 0) { + List keyList = new ArrayList(signParams.keySet()); + Collections.sort(keyList); + for (String key : keyList) { + builder.append("&"); + builder.append(key); + builder.append("="); + builder.append(signParams.get(key)); + } + } + + // 组装请求头部签名信息 + String user = keyConfigure.getKeyId() + "_" + timestamp + "_" + nonce; + String pwd; + if (KeySignTypeEnum.SHA256WITHRSA.equals(keyConfigure.getKeySignType())) { + pwd = Signature.signatureByRSA(builder.toString(), keyConfigure.getPriKey(), Const.CHARSET); + } else { + pwd = Signature.signatureBySM2(builder.toString(), keyConfigure.getPriKey(), Const.CHARSET); + } + + return "Basic " + Base64.encode((user + ":" + pwd).getBytes()); + } + + /** + * 计算头部签名信息(数字信封) + * + * @param method 请求方法 + * @param uri 请求URI + * @param signParams 参数集合 + * @param keyConfigure 应用配置对象 + * @return 头部认证信息内容 + */ + protected String getNetSignAuthInfo(ReqMethodEnum method, String uri, String commKey, Map signParams, + KeyConfigure keyConfigure) throws Exception { + String timestamp = SdkUtil.getDateTime(); + + // 待签参数拼接 + String nonce = UUID.randomUUID().toString().replaceAll("-", ""); + StringBuilder builder = new StringBuilder(); + builder.append(keyConfigure.getKeyId()); + builder.append("&"); + builder.append(timestamp); + builder.append("&"); + builder.append(nonce); + builder.append("&"); + builder.append(method); + builder.append("&"); + builder.append(uri); + builder.append("&"); + builder.append(commKey); + + if (signParams != null && signParams.size() != 0) { + List keyList = new ArrayList(signParams.keySet()); + Collections.sort(keyList); + for (String key : keyList) { + builder.append("&"); + builder.append(key); + builder.append("="); + builder.append(signParams.get(key)); + } + } + String encCommKey = Signature.encryptBySM2PublicKey(keyConfigure.getRespPubKey(), commKey.getBytes(StandardCharsets.UTF_8)); + // 组装请求头部信息 + String user = keyConfigure.getKeyId() + "_" + timestamp + "_" + nonce + "_" + encCommKey; + String pwd; + if (KeySignTypeEnum.SHA256WITHRSA.equals(keyConfigure.getKeySignType())) { + pwd = Signature.signatureByRSA(builder.toString(), keyConfigure.getPriKey(), Const.CHARSET); + } else { + pwd = Signature.signatureBySM2(builder.toString(), keyConfigure.getPriKey(), Const.CHARSET); + } + + return "Basic " + Base64.encode((user + ":" + pwd).getBytes()); + } + + /** + * 加密请求参数 + * + * @param bodyParamMap + * @param bodyParamJson + * @param commKey + * @return + */ + public Map getEncryptBody(Map bodyParamMap, String commKey) throws SdkException { + String bodyParamJsonString = JSON.toJSONString(bodyParamMap); + Map encryptBodyParamMap = new HashMap<>(2); + try { + encryptBodyParamMap.put("ENC_DATA", SymmetricEncrypt.sm4Encrypt(bodyParamJsonString, commKey)); + return encryptBodyParamMap; + } catch (Exception e) { + throw new SdkException(SdkExType.NET_SIGN_ERR); + } + } + + /** + * 执行请求处理 + * + * @param reqUri 请求资源URI + * @param reqMethod 请求方法 + * @param headParams 头部信息请求参数 + * @param urlParams URL请求参数 + * @param bodyParamMap BODY请求参数集合 + * @param filePath 文件路径 + * @param keyConfigure 应用配置对象 + * @param bodyJson BODY请求参数JSON字符串 + * @return + */ + public Object exec(String reqUri, ReqMethodEnum reqMethod, Map headParams, + Map urlParams, Map bodyParamMap, String filePath, KeyConfigure keyConfigure, + String bodyJson) { + + // 准备签名参数 + Map signParams = new HashMap(64); + if (SdkUtil.notEmpty(headParams)) { + signParams.putAll(headParams); + } + if (SdkUtil.notEmpty(urlParams)) { + signParams.putAll(urlParams); + } + if (SdkUtil.notEmpty(bodyParamMap)) { + signParams.putAll(bodyParamMap); + } + if (SdkUtil.isJSONString(bodyJson)) { + signParams.put("BODY", bodyJson); + } + + // 计算签名值 + reqUri = SdkUtil.checkReqUri(reqUri); + String authInfo = this.getAuthInfo(reqMethod, reqUri, signParams, keyConfigure); + + // 拼接请求参数 + String urlParam = SdkUtil.jointMap(urlParams); + String bodyParamString = SdkUtil.jointMap(bodyParamMap); + + // 拼接请求URL + StringBuilder urlBuilder = new StringBuilder(); + urlBuilder.append(this.getPostUrl()); + urlBuilder.append(reqUri); + switch (reqMethod) { + case GET: + case POST: + if (SdkUtil.notBlank(urlParam)) { + urlBuilder.append(urlBuilder.toString().contains("?") ? "&" : "?"); + urlBuilder.append(urlParam); + } + break; + default: + break; + } + + // 执行通讯请求 + Object response = ""; + try { + response = txn(urlBuilder.toString(), reqMethod, authInfo, headParams, bodyParamString, filePath, + bodyParamMap, keyConfigure, bodyJson); + } catch (SdkException ex) { + + if (SdkExType.CONN_ERR.getCode().equals(ex.getCode())) { + String url = Configure.isDevEnv() ? Configure.getDevUrl() : Configure.PROD_URL; + response = ex.toString().replace("通讯错误", url + "通讯错误"); + } else { + response = ex.toString(); + } + } + return response; + } + + /** + * 执行请求处理 + * + * @param reqUri 请求资源URI + * @param reqMethod 请求方法 + * @param bodyParamMap BODY请求参数集合 + * @param filePath 文件路径 + * @param keyConfigure 应用配置对象 + * @param bodyJson BODY请求参数JSON字符串 + * @return + */ + public Object execNetSign(String reqUri, ReqMethodEnum reqMethod, Map headParams, Map urlParams, + Map bodyParamMap, String filePath, KeyConfigure keyConfigure, + String bodyJson) throws Exception { + + // 生成通讯对称秘钥 + String commKey = SymmetricEncrypt.generateSM4Key(); + // 准备签名参数 + Map signParams = new HashMap(64); + if (SdkUtil.notEmpty(headParams)) { + signParams.putAll(headParams); + } + if (SdkUtil.notEmpty(urlParams)) { + signParams.putAll(urlParams); + } + if (SdkUtil.notEmpty(bodyParamMap)) { + signParams.putAll(bodyParamMap); + } + if (SdkUtil.isJSONString(bodyJson)) { + signParams.put("BODY", bodyJson); + } + + // 计算签名值 + reqUri = SdkUtil.checkReqUri(reqUri); + String authInfo = this.getNetSignAuthInfo(reqMethod, reqUri, commKey, signParams, keyConfigure); + // 加密签名请求体 + Map encryptBodyParamMap = this.getEncryptBody(bodyParamMap, commKey); + + // 拼接请求参数 +// String bodyParamString = SdkUtil.jointMap(encryptBodyParamMap); + + // 拼接请求URL + StringBuilder urlBuilder = new StringBuilder(); + urlBuilder.append(this.getPostUrl()); + urlBuilder.append(reqUri); + String bodyParamString = SdkUtil.jointMap(encryptBodyParamMap); + // 执行通讯请求 + Object response = ""; + try { + response = txn(urlBuilder.toString(), reqMethod, authInfo, null, bodyParamString, filePath, + encryptBodyParamMap, keyConfigure, bodyJson); + //响应报文解密 + JSONObject responseObject = JSON.parseObject((String) response); + String encData = responseObject.getString("ENC_DATA"); + if (encData == null || encData.equals("")) { + return response; + } + response = SymmetricEncrypt.sm4Decrypt(encData, commKey); + } catch (SdkException ex) { + + if (SdkExType.CONN_ERR.getCode().equals(ex.getCode())) { + String url = Configure.isDevEnv() ? Configure.getDevUrl() : Configure.PROD_URL; + response = ex.toString().replace("通讯错误", url + "通讯错误"); + } else { + response = ex.toString(); + } + } + + return response; + } + +} diff --git a/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/util/Base64.java b/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/util/Base64.java new file mode 100644 index 00000000..7091d9e5 --- /dev/null +++ b/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/util/Base64.java @@ -0,0 +1,293 @@ +package com.cib.fintech.dfp.open.sdk.util; + +/** + * Base64工具类 + * + * @author cibfintech + */ +public final class Base64 { + + static private final int BASELENGTH = 128; + static private final int LOOKUPLENGTH = 64; + static private final int TWENTYFOURBITGROUP = 24; + static private final int EIGHTBIT = 8; + static private final int SIXTEENBIT = 16; + static private final int FOURBYTE = 4; + static private final int SIGN = -128; + static private final char PAD = '='; + static private final boolean F_DEBUG = false; + static final private byte[] BASE64_ALPHABET = new byte[BASELENGTH]; + static final private char[] LOOKUP_BASE64_ALPHABET = new char[LOOKUPLENGTH]; + + static { + for (int i = 0; i < BASELENGTH; ++i) { + BASE64_ALPHABET[i] = -1; + } + for (int i = 'Z'; i >= 'A'; i--) { + BASE64_ALPHABET[i] = (byte) (i - 'A'); + } + for (int i = 'z'; i >= 'a'; i--) { + BASE64_ALPHABET[i] = (byte) (i - 'a' + 26); + } + + for (int i = '9'; i >= '0'; i--) { + BASE64_ALPHABET[i] = (byte) (i - '0' + 52); + } + + BASE64_ALPHABET['+'] = 62; + BASE64_ALPHABET['/'] = 63; + + for (int i = 0; i <= 25; i++) { + LOOKUP_BASE64_ALPHABET[i] = (char) ('A' + i); + } + + for (int i = 26, j = 0; i <= 51; i++, j++) { + LOOKUP_BASE64_ALPHABET[i] = (char) ('a' + j); + } + + for (int i = 52, j = 0; i <= 61; i++, j++) { + LOOKUP_BASE64_ALPHABET[i] = (char) ('0' + j); + } + LOOKUP_BASE64_ALPHABET[62] = (char) '+'; + LOOKUP_BASE64_ALPHABET[63] = (char) '/'; + + } + + private static boolean isWhiteSpace(char octect) { + return (octect == 0x20 || octect == 0xd || octect == 0xa || octect == 0x9); + } + + private static boolean isPad(char octect) { + return (octect == PAD); + } + + private static boolean isData(char octect) { + return (octect < BASELENGTH && BASE64_ALPHABET[octect] != -1); + } + + /** + * Encodes hex octects into Base64 + * + * @param binaryData Array containing binaryData + * @return Encoded Base64 array + */ + public static String encode(byte[] binaryData) { + + if (binaryData == null) { + return null; + } + + int lengthDataBits = binaryData.length * EIGHTBIT; + if (lengthDataBits == 0) { + return ""; + } + + int fewerThan24bits = lengthDataBits % TWENTYFOURBITGROUP; + int numberTriplets = lengthDataBits / TWENTYFOURBITGROUP; + int numberQuartet = fewerThan24bits != 0 ? numberTriplets + 1 : numberTriplets; + char[] encodedData = null; + + encodedData = new char[numberQuartet * 4]; + + byte k = 0, l = 0, b1 = 0, b2 = 0, b3 = 0; + + int encodedIndex = 0; + int dataIndex = 0; + if (F_DEBUG) { + System.out.println("number of triplets = " + numberTriplets); + } + + for (int i = 0; i < numberTriplets; i++) { + b1 = binaryData[dataIndex++]; + b2 = binaryData[dataIndex++]; + b3 = binaryData[dataIndex++]; + + if (F_DEBUG) { + System.out.println("b1= " + b1 + ", b2= " + b2 + ", b3= " + b3); + } + + l = (byte) (b2 & 0x0f); + k = (byte) (b1 & 0x03); + + byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0); + byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) : (byte) ((b2) >> 4 ^ 0xf0); + byte val3 = ((b3 & SIGN) == 0) ? (byte) (b3 >> 6) : (byte) ((b3) >> 6 ^ 0xfc); + + if (F_DEBUG) { + System.out.println("val2 = " + val2); + System.out.println("k4 = " + (k << 4)); + System.out.println("vak = " + (val2 | (k << 4))); + } + + encodedData[encodedIndex++] = LOOKUP_BASE64_ALPHABET[val1]; + encodedData[encodedIndex++] = LOOKUP_BASE64_ALPHABET[val2 | (k << 4)]; + encodedData[encodedIndex++] = LOOKUP_BASE64_ALPHABET[(l << 2) | val3]; + encodedData[encodedIndex++] = LOOKUP_BASE64_ALPHABET[b3 & 0x3f]; + } + + // form integral number of 6-bit groups + if (fewerThan24bits == EIGHTBIT) { + b1 = binaryData[dataIndex]; + k = (byte) (b1 & 0x03); + if (F_DEBUG) { + System.out.println("b1=" + b1); + System.out.println("b1<<2 = " + (b1 >> 2)); + } + byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0); + encodedData[encodedIndex++] = LOOKUP_BASE64_ALPHABET[val1]; + encodedData[encodedIndex++] = LOOKUP_BASE64_ALPHABET[k << 4]; + encodedData[encodedIndex++] = PAD; + encodedData[encodedIndex++] = PAD; + } else if (fewerThan24bits == SIXTEENBIT) { + b1 = binaryData[dataIndex]; + b2 = binaryData[dataIndex + 1]; + l = (byte) (b2 & 0x0f); + k = (byte) (b1 & 0x03); + + byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0); + byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) : (byte) ((b2) >> 4 ^ 0xf0); + + encodedData[encodedIndex++] = LOOKUP_BASE64_ALPHABET[val1]; + encodedData[encodedIndex++] = LOOKUP_BASE64_ALPHABET[val2 | (k << 4)]; + encodedData[encodedIndex++] = LOOKUP_BASE64_ALPHABET[l << 2]; + encodedData[encodedIndex++] = PAD; + } + + return new String(encodedData); + } + + /** + * Decodes Base64 data into octects + * + * @param encoded string containing Base64 data + * @return Array containind decoded data. + */ + public static byte[] decode(String encoded) { + + if (encoded == null) { + return null; + } + + char[] base64Data = encoded.toCharArray(); + + /** + * remove white spaces + */ + int len = removeWhiteSpace(base64Data); + + /** + * should be divisible by four + */ + if (len % FOURBYTE != 0) { + return null; + } + + int numberQuadruple = (len / FOURBYTE); + + if (numberQuadruple == 0) { + return new byte[0]; + } + + byte[] decodedData = null; + byte b1 = 0, b2 = 0, b3 = 0, b4 = 0; + char d1 = 0, d2 = 0, d3 = 0, d4 = 0; + + int i = 0; + int encodedIndex = 0; + int dataIndex = 0; + decodedData = new byte[(numberQuadruple) * 3]; + + for (; i < numberQuadruple - 1; i++) { + + if (!isData((d1 = base64Data[dataIndex++])) || !isData((d2 = base64Data[dataIndex++])) + || !isData((d3 = base64Data[dataIndex++])) || !isData((d4 = base64Data[dataIndex++]))) { + return null; + }// if found "no data" just return null + + b1 = BASE64_ALPHABET[d1]; + b2 = BASE64_ALPHABET[d2]; + b3 = BASE64_ALPHABET[d3]; + b4 = BASE64_ALPHABET[d4]; + + decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4); + decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); + decodedData[encodedIndex++] = (byte) (b3 << 6 | b4); + } + + /** + * if found "no data" just return null + */ + if (!isData((d1 = base64Data[dataIndex++])) || !isData((d2 = base64Data[dataIndex++]))) { + return null; + } + + b1 = BASE64_ALPHABET[d1]; + b2 = BASE64_ALPHABET[d2]; + + d3 = base64Data[dataIndex++]; + d4 = base64Data[dataIndex++]; + /** + * Check if they are PAD characters + */ + if (!isData((d3)) || !isData((d4))) { + if (isPad(d3) && isPad(d4)) { + /** + * last 4 bits should be zero + */ + if ((b2 & 0xf) != 0) { + return null; + } + byte[] tmp = new byte[i * 3 + 1]; + System.arraycopy(decodedData, 0, tmp, 0, i * 3); + tmp[encodedIndex] = (byte) (b1 << 2 | b2 >> 4); + return tmp; + } else if (!isPad(d3) && isPad(d4)) { + b3 = BASE64_ALPHABET[d3]; + /** + * last 2 bits should be zero + */ + if ((b3 & 0x3) != 0) { + return null; + } + byte[] tmp = new byte[i * 3 + 2]; + System.arraycopy(decodedData, 0, tmp, 0, i * 3); + tmp[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4); + tmp[encodedIndex] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); + return tmp; + } else { + return null; + } + } else { // No PAD e.g 3cQl + b3 = BASE64_ALPHABET[d3]; + b4 = BASE64_ALPHABET[d4]; + decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4); + decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); + decodedData[encodedIndex++] = (byte) (b3 << 6 | b4); + + } + + return decodedData; + } + + /** + * remove WhiteSpace from MIME containing encoded Base64 data. + * + * @param data the byte array of base64 data (with WS) + * @return the new length + */ + private static int removeWhiteSpace(char[] data) { + if (data == null) { + return 0; + } + + // count characters that's not whitespace + int newSize = 0; + int len = data.length; + for (int i = 0; i < len; i++) { + if (!isWhiteSpace(data[i])) { + data[newSize++] = data[i]; + } + } + return newSize; + } +} diff --git a/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/util/CallbackUtil.java b/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/util/CallbackUtil.java new file mode 100644 index 00000000..1142e420 --- /dev/null +++ b/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/util/CallbackUtil.java @@ -0,0 +1,157 @@ +package com.cib.fintech.dfp.open.sdk.util; + + +import com.cib.fintech.dfp.open.sdk.exception.SdkExType; +import com.cib.fintech.dfp.open.sdk.exception.SdkException; + +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +/** + * 回调相关工具类 + * + * @author wenjunxin + * @version 4.4.0 + */ +public class CallbackUtil { + + /** + * 对回调请求进行解密验签 + * + * @param keyId 应用ID(请求头中获得,字段名:Keyid,示例:KYONLYFORTEST012345678) + * @param timestamp 时间戳(请求头中获得,字段名:Timestamp,示例:20210810185848 其中时间格式为yyyyMMddHHmmss) + * @param nonce 随机数(请求头中获得,字段名:Nonce,示例:CGYHt6YaCn7aFm8B76yXTW1Q) + * @param signValue 签名值(请求头中获得,字段名:Signature,示例:T3J8o4zY5eGvG4to7xkGfqTZN3Nk4w8NSKG/ADdxhFy+OhhebHUS8t4Wk+w3HSA4K2UXJA5o8AUXm/QAa98FFg==) + * @param body 回调请求数据(请求体中获得,参数名:ciphertext,加密内容,示例:{"ciphertext":"xq1hzkLPC4JQC6FVE7nCzroDjWK+3WtYGrYuwpgGsX4="} ) + * @param decryptKey 解密秘钥(sm4加密,示例:iij8IGO+6+ZVpRshrug3IQ==) + * @param publicKey 验签公钥(sm2加密,示例:BCfPH6Alxb18eLXup8zF4nw3IZrf44FOZYSIlEaI1ePmWijbrBHiEGSWZzMcBbbcytRoRFLftlp/g45Y/m2b8zE=) + * @return 解密验签后的body(sm4解密sm2签名,Json字符串,解密后示例:{xxx:xxx,xxx:xxx}) + */ + public static String decryptAndVerify(String keyId, String timestamp, String nonce, String signValue, String body, + String decryptKey, String publicKey) throws SdkException { + + //解密后的body + String decryptedBody; + // 解密 + try { + decryptedBody = SymmetricEncrypt.sm4Decrypt(body, decryptKey); + } catch (Exception e) { + throw new SdkException(SdkExType.CALLBACK_BODY_ENCRYPT_ERR); + } + + // 验签 + if (SdkUtil.notBlank(signValue)) { + StringBuilder signParams = new StringBuilder().append(keyId).append("&").append(timestamp).append("&") + .append(nonce).append("&").append(decryptedBody); + try { + byte[] bytes = signParams.toString().getBytes(Const.CHARSET); + if (!VerifyRespSignature.verifyBySM2(bytes, signValue, publicKey)) { + throw new SdkException(SdkExType.CALLBACK_REQUEST_VERIFY_ERR); + } + } catch (Exception e) { + throw new SdkException(SdkExType.CALLBACK_REQUEST_VERIFY_ERR); + } + } else { + throw new SdkException(SdkExType.CALLBACK_REQUEST_VERIFY_ERR); + } + + return decryptedBody; + } + + /** + * 对回调请求进行解密验签(V2版本) + * + * @param keyId 应用ID(请求头中获得,字段名:Keyid,示例:KYONLYFORTEST012345678) + * @param timestamp 时间戳(请求头中获得,字段名:Timestamp,示例:20210810185848 其中时间格式为yyyyMMddHHmmss) + * @param nonce 随机数(请求头中获得,字段名:Nonce,示例:CGYHt6YaCn7aFm8B76yXTW1Q) + * @param pwd 对称秘钥(请求头中获得,字段名:Pwd) + * @param signValue 签名值(请求头中获得,字段名:Signature,示例:T3J8o4zY5eGvG4to7xkGfqTZN3Nk4w8NSKG/ADdxhFy+OhhebHUS8t4Wk+w3HSA4K2UXJA5o8AUXm/QAa98FFg==) + * @param body 回调请求数据(请求体中获得,参数名:ciphertext,加密内容,示例:{"ciphertext":"xq1hzkLPC4JQC6FVE7nCzroDjWK+3WtYGrYuwpgGsX4="} ) + * @param priKey 解密秘钥 + * @param respPubKey 验签公钥 + * @return 解密验签后的body(sm4解密sm2签名,Json字符串,解密后示例:{xxx:xxx,xxx:xxx}) + */ + public static Map decryptAndVerifyV2(String keyId, String timestamp, String nonce, String pwd, String signValue, String body, + String priKey, String respPubKey) throws SdkException { + + Map resultMap = new HashMap<>(2); + try { + // 解密对称秘钥 + byte[] pwdByte = Signature.decryptBySM2PrivateKey(priKey, Base64.decode(pwd)); + assert pwdByte != null; + pwd = new String(pwdByte, StandardCharsets.UTF_8); + resultMap.put("pwd", pwd); + } catch (Exception e) { + throw new SdkException(SdkExType.CALLBACK_BODY_ENCRYPT_ERR); + } + + //解密后的body + String decryptedBody; + // 解密 + try { + decryptedBody = SymmetricEncrypt.sm4Decrypt(body, pwd); + } catch (Exception e) { + throw new SdkException(SdkExType.CALLBACK_BODY_ENCRYPT_ERR); + } + + // 验签 + if (SdkUtil.notBlank(signValue)) { + StringBuilder signParams = new StringBuilder().append(keyId).append("&").append(timestamp).append("&") + .append(nonce).append("&").append(pwd).append("&").append(decryptedBody); + try { + byte[] bytes = signParams.toString().getBytes(Const.CHARSET); + if (!VerifyRespSignature.verifyBySM2(bytes, signValue, respPubKey)) { + throw new SdkException(SdkExType.CALLBACK_REQUEST_VERIFY_ERR); + } + resultMap.put("body", decryptedBody); + } catch (Exception e) { + throw new SdkException(SdkExType.CALLBACK_REQUEST_VERIFY_ERR); + } + } else { + throw new SdkException(SdkExType.CALLBACK_REQUEST_VERIFY_ERR); + } + + return resultMap; + } + + /** + * 回调响应签名 + * + * @param keyId 应用ID + * @param timestamp 时间戳 + * @param nonce 随机数 + * @param body 响应内容 + * @param privateKey 私钥 + * @return 签名值 + */ + public static String signature(String keyId, String timestamp, String nonce, + String body, String privateKey) throws SdkException { + StringBuilder signParams; + if (SdkUtil.notBlank(keyId) && SdkUtil.notBlank(timestamp) && + SdkUtil.notBlank(nonce) && SdkUtil.notBlank(body)) { + signParams = new StringBuilder().append(keyId).append("&").append(timestamp).append("&") + .append(nonce).append("&").append(body); + } else { + throw new SdkException(SdkExType.CALLBACK_RESPONSE_SIGN_ERR); + } + return Signature.signatureBySM2(signParams.toString(), privateKey, Const.CHARSET); + } + + /** + * 回调响应加密 + * + * @param body 需要加密的内容 + * @param encryptKey 加密密钥 + * @return 密文(Base64 encode) + */ + public static String encrypt(String body, String encryptKey) throws SdkException { + String encryptedBody; + try { + encryptedBody = SymmetricEncrypt.sm4Encrypt(body, encryptKey); + } catch (Exception e) { + throw new SdkException(SdkExType.CALLBACK_BODY_ENCRYPT_ERR); + } + return encryptedBody; + } +} diff --git a/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/util/Const.java b/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/util/Const.java new file mode 100644 index 00000000..931cd375 --- /dev/null +++ b/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/util/Const.java @@ -0,0 +1,67 @@ +package com.cib.fintech.dfp.open.sdk.util; + +import java.text.SimpleDateFormat; + +/** + * 全局静态常量 + * + * @author zengminghua + */ +public final class Const { + + /** + * 编码字符集:UTF-8 + */ + public static final String CHARSET = "UTF-8"; + + /** + * 发送请求时head中的认证信息KEY + */ + public static final String POST_HEAD_KEY = "Authorization"; + + /** + * 时间格式化器 + */ + public static final SimpleDateFormat FORMATTER_TIME = new SimpleDateFormat("yyyyMMddHHmmss"); + + /** + * 签名加密类型:RSA + */ + public static final String SIGN_TYPE = "RSA"; + + /** + * 加密算法 + */ + public static final String SIGN_ALGORITHMS = "SHA256WithRSA"; + + /** + * EC算法 + */ + public static final String ALGORITHM_EC = "EC"; + + /** + * SM3WithSM2算法 + */ + public static final String ALGORITHM_SM3_WITH_SM2 = "SM3WithSM2"; + + /** + * 下载成功返回消息 + */ + public static final String FILE_SUCCESS_RESULT = "{\"code\":\"OPEN25800\",\"msg\":\"下载成功\",\"traceId\":\"OPEN-00-LOCAL-800\"}"; + + /** + * 文件下载的uri + */ + public static final String FILE_DOWNLOAD_URI = "/api/open/downloadFile"; + + /** + * 文件上传的uri + */ + public static final String FILE_UPLOAD_URI = "/api/open/uploadFile"; + + /** + * 异常返回码前缀 + */ + public static final String RESP_EXCEPTION_CODE = "\"code\":\"OPEN"; + +} diff --git a/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/util/FileUtil.java b/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/util/FileUtil.java new file mode 100644 index 00000000..d014ed6f --- /dev/null +++ b/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/util/FileUtil.java @@ -0,0 +1,98 @@ +package com.cib.fintech.dfp.open.sdk.util; + +import com.cib.fintech.dfp.open.sdk.exception.SdkExType; +import com.cib.fintech.dfp.open.sdk.exception.SdkException; + +import java.io.*; +import java.security.MessageDigest; + +/** + * 文件工具 + * + * @author zengminghua + */ +public class FileUtil { + + public static final char[] HEX_CHAR = { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + + /** + * @param filePath 文件路径 + * @return file bytes + * @throws IOException 读取文件错误 + */ + public static byte[] readFileByBytes(String filePath) throws IOException { + File file = new File(filePath); + if (!file.exists()) { + throw new FileNotFoundException(filePath); + } + + ByteArrayOutputStream baos = new ByteArrayOutputStream(((int) file.length())); + BufferedInputStream in = null; + try { + in = new BufferedInputStream(new FileInputStream(file)); + int bufSize = 1024; + byte[] buffer = new byte[bufSize]; + int len; + while (-1 != (len = in.read(buffer, 0, bufSize))) { + baos.write(buffer, 0, len); + } + return baos.toByteArray(); + } finally { + try { + if (in != null) { + in.close(); + } + } catch (IOException e) { + // e.printStackTrace(); + } + baos.close(); + } + } + + /** + * 计算文件的特征值 + *

+ * 支持的算法如:"MD2", "MD5", "SHA-1", "SHA-256", "SHA-384", "SHA-512" + * + * @param filePath 文件路径 + * @param algorithm 算法 + * @return hashValue 特征值 + */ + public static String fileHash(String filePath, String algorithm) throws Exception { + MessageDigest messagedigest = null; + try { + messagedigest = MessageDigest.getInstance(algorithm); + + InputStream fis = null; + try { + fis = new FileInputStream(filePath); + byte[] buffer = new byte[1024]; + int numRead = 0; + while ((numRead = fis.read(buffer)) > 0) { + messagedigest.update(buffer, 0, numRead); + } + } catch (Exception ex) { + // ex.printStackTrace(); + } finally { + if (fis != null) { + fis.close(); + } + } + } catch (Exception e) { + SdkException ex = new SdkException(SdkExType.FILE_ERROR_RESULT); + return ex.toString(); + } + return toHexString(messagedigest.digest()); + } + + public static String toHexString(byte[] b) { + StringBuilder sb = new StringBuilder(b.length * 2); + for (int i = 0; i < b.length; i++) { + sb.append(HEX_CHAR[(b[i] & 0xf0) >>> 4]); + sb.append(HEX_CHAR[b[i] & 0x0f]); + } + return sb.toString(); + } + +} diff --git a/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/util/SdkUtil.java b/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/util/SdkUtil.java new file mode 100644 index 00000000..04ee07df --- /dev/null +++ b/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/util/SdkUtil.java @@ -0,0 +1,111 @@ +package com.cib.fintech.dfp.open.sdk.util; + +import java.net.URLEncoder; +import java.util.Date; +import java.util.Map; + +/** + * SDK工具类 + * + * @author zengminghua + */ +public class SdkUtil { + + private static final String REQ_SEPARATOR = "/"; + + /** + * 检查请求URI + * + * @param reqUri 请求URI + * @return 处理后的URI + */ + public static String checkReqUri(String reqUri) { + if (reqUri == null || reqUri.length() == 0) { + return reqUri; + } + if (!reqUri.startsWith(REQ_SEPARATOR)) { + reqUri = REQ_SEPARATOR + reqUri; + } + if (reqUri.endsWith(REQ_SEPARATOR)) { + reqUri = reqUri.substring(0, reqUri.length() - 1); + } + return reqUri; + } + + /** + * 将Map拼接为字符串 + * + * @param paramsMap 参数集 + * @return 拼接后的参数 + */ + public static String jointMap(Map paramsMap) { + try { + StringBuilder mapBuilder = new StringBuilder(); + if (paramsMap != null && paramsMap.size() != 0) { + for (String key : paramsMap.keySet()) { + mapBuilder.append(URLEncoder.encode(key, Const.CHARSET)); + mapBuilder.append("="); + mapBuilder.append(null == paramsMap.get(key) ? + null : + URLEncoder.encode(paramsMap.get(key), Const.CHARSET)); + mapBuilder.append("&"); + } + } + if (mapBuilder.length() > 1) { + mapBuilder.setLength(mapBuilder.length() - 1); + } + return mapBuilder.toString(); + } catch (Exception e) { + return ""; + } + } + + /** + * 获取格式化的时间:yyyyMMddHHmmss + * + * @return String + */ + public static String getDateTime() { + return Const.FORMATTER_TIME.format(new Date()); + } + + /** + * 判断字符串是否为空 + * + * @param str 字符串 + * @return 判断结果 + */ + public static boolean notBlank(String str) { + if (null != str && "".equals(str) == false && "".equals(str.trim()) == false) { + return true; + } + return false; + } + + /** + * 判断Map是否为空 + * + * @param map 集合 + * @return 判断结果 + */ + public static boolean notEmpty(Map map) { + if (map != null && map.size() != 0) { + return true; + } + return false; + } + + /** + * 判断字符串是否为JSON格式 + * + * @param str 字符串 + * @return 判断结果 + */ + public static boolean isJSONString(String str) { + if (null != str && str.startsWith("{") && str.endsWith("}")) { + return true; + } + return false; + } + +} diff --git a/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/util/Signature.java b/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/util/Signature.java new file mode 100644 index 00000000..5aa75cc7 --- /dev/null +++ b/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/util/Signature.java @@ -0,0 +1,264 @@ +package com.cib.fintech.dfp.open.sdk.util; + +import org.bouncycastle.asn1.ASN1Integer; +import org.bouncycastle.asn1.ASN1Sequence; +import org.bouncycastle.asn1.gm.GMNamedCurves; +import org.bouncycastle.asn1.x9.X9ECParameters; +import org.bouncycastle.crypto.InvalidCipherTextException; +import org.bouncycastle.crypto.engines.SM2Engine; +import org.bouncycastle.crypto.params.ECDomainParameters; +import org.bouncycastle.crypto.params.ECPrivateKeyParameters; +import org.bouncycastle.crypto.params.ECPublicKeyParameters; +import org.bouncycastle.crypto.params.ParametersWithRandom; +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.jce.spec.ECPrivateKeySpec; +import org.bouncycastle.jce.spec.ECPublicKeySpec; +import org.bouncycastle.util.BigIntegers; +import org.bouncycastle.util.encoders.Hex; + +import java.math.BigInteger; +import java.security.*; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Arrays; + +/** + * 签名验签工具类 + * + * @author zengminghua, tangchongcan + */ +public class Signature { + + private static final BouncyCastleProvider BC = new BouncyCastleProvider(); + + private static final int RS_LEN = 32; + + private static final String ALGORITHM_EC = "EC"; + + private static X9ECParameters x9ECParameters = GMNamedCurves.getByName("sm2p256v1"); + private static ECParameterSpec ecParameterSpec = new ECParameterSpec(x9ECParameters.getCurve(), + x9ECParameters.getG(), x9ECParameters.getN()); + private static ECDomainParameters ecDomainParameters = new ECDomainParameters(x9ECParameters.getCurve(), + x9ECParameters.getG(), x9ECParameters.getN()); + + /** + * RSA签名 + * + * @param content 待签内容 + * @param privateKey 开发者私钥 + * @param charset 字符编码 + * @return 签名值 + */ + public static String signatureByRSA(String content, String privateKey, String charset) { + try { + PKCS8EncodedKeySpec priPKCS8 = new PKCS8EncodedKeySpec(Base64.decode(privateKey)); + KeyFactory keyf = KeyFactory.getInstance(Const.SIGN_TYPE); + PrivateKey priKey = keyf.generatePrivate(priPKCS8); + + java.security.Signature signature = java.security.Signature.getInstance(Const.SIGN_ALGORITHMS); + + signature.initSign(priKey); + signature.update(content.getBytes(charset)); + + byte[] signed = signature.sign(); + return Base64.encode(signed); + } catch (Exception e) { + return null; + } + } + + /** + * SM2签名 + * + * @param content 待签内容 + * @param privateKey 开发者私钥 + * @param charset 字符编码 + * @return + */ + public static String signatureBySM2(String content, String privateKey, String charset) { + byte[] privateKeyBytes = Base64.decode(privateKey); + BCECPrivateKey bcecPrivateKey = getPrivateKeyFromD(BigIntegers.fromUnsignedByteArray(privateKeyBytes)); + try { + byte[] contentBytes = content.getBytes(charset); + byte[] signed = rsAsn1ToPlainByteArray(signSm3WithSm2Asn1Rs(contentBytes, bcecPrivateKey)); + return Base64.encode(signed); + } catch (Exception e) { + return null; + } + } + + /** + * 使用SM2公钥加密数据 + * + * @param encodedPublicKey Base64编码的公钥 + * @param contentBytes 待加密内容 + * @return Base64编码后的密文数据 + */ + public static String encryptBySM2PublicKey(String encodedPublicKey, byte[] contentBytes) throws Exception { + try { + byte[] publicKeyBytes = Base64.decode(encodedPublicKey); + if (publicKeyBytes.length != 64 && publicKeyBytes.length != 65) { + throw new Exception("err key length"); + } + + BigInteger x, y; + if (publicKeyBytes.length > 64) { + x = BigIntegers.fromUnsignedByteArray(publicKeyBytes, 1, 32); + y = BigIntegers.fromUnsignedByteArray(publicKeyBytes, 33, 32); + } else { + x = BigIntegers.fromUnsignedByteArray(publicKeyBytes, 0, 32); + y = BigIntegers.fromUnsignedByteArray(publicKeyBytes, 32, 32); + } + BCECPublicKey bcecPublicKey = getPublicKeyFromXY(x, y); + byte[] results = changeC1C2C3ToC1C3C2(encryptBySM2PublicKeyOld(contentBytes, bcecPublicKey)); + return Base64.encode(results); + } catch (Exception e) { + throw e; + } + } + + /** + * 用SM2私钥解密 + * + * @param encodedPrivateKey Base64编码的私钥 + * @param contentBytes 待解密内容 + * @return 明文数据(byte数组) + */ + public static byte[] decryptBySM2PrivateKey(String encodedPrivateKey, byte[] contentBytes) throws Exception { + try { + byte[] privateKeyBytes = Base64.decode(encodedPrivateKey); + BCECPrivateKey bcecPrivateKey = getPrivateKeyFromD(BigIntegers.fromUnsignedByteArray(privateKeyBytes)); + return decryptBySM2PrivateKeyOld(changeC1C3C2ToC1C2C3(contentBytes), bcecPrivateKey); + } catch (Exception e) { + return null; + } + } + + /** + * c1||c2||c3 + * + * @param data + * @param bcecPrivateKey + * @return + */ + public static byte[] decryptBySM2PrivateKeyOld(byte[] data, BCECPrivateKey bcecPrivateKey) + throws InvalidCipherTextException { + ECPrivateKeyParameters ecPrivateKeyParameters = new ECPrivateKeyParameters(bcecPrivateKey.getD(), + ecDomainParameters); + SM2Engine sm2Engine = new SM2Engine(); + sm2Engine.init(false, ecPrivateKeyParameters); + return sm2Engine.processBlock(data, 0, data.length); + } + + /** + * bc加解密使用旧标c1||c3||c2,此方法在解密前调用,将密文转化为c1||c2||c3再去解密 + * + * @param c1c3c2 + * @return + */ + private static byte[] changeC1C3C2ToC1C2C3(byte[] c1c3c2) { + //sm2p256v1的这个固定65 + final int c1Len = (x9ECParameters.getCurve().getFieldSize() + 7) / 8 * 2 + 1; + // 可看GMNamedCurves、ECCurve代码。 + final int c3Len = 32; + byte[] result = new byte[c1c3c2.length]; + //c1: 0->65 + System.arraycopy(c1c3c2, 0, result, 0, c1Len); + //c2 + System.arraycopy(c1c3c2, c1Len + c3Len, result, c1Len, c1c3c2.length - c1Len - c3Len); + //c3 + System.arraycopy(c1c3c2, c1Len, result, c1c3c2.length - c3Len, c3Len); + return result; + } + + /** + * c1||c2||c3 + * + * @param data + * @param bcecPublicKey + * @return + */ + public static byte[] encryptBySM2PublicKeyOld(byte[] data, BCECPublicKey bcecPublicKey) + throws InvalidCipherTextException { + ECPublicKeyParameters ecPublicKeyParameters = new ECPublicKeyParameters(bcecPublicKey.getQ(), + ecDomainParameters); + SM2Engine sm2Engine = new SM2Engine(); + sm2Engine.init(true, new ParametersWithRandom(ecPublicKeyParameters, new SecureRandom())); + return sm2Engine.processBlock(data, 0, data.length); + } + + /** + * bc加解密使用旧标c1||c2||c3,此方法在加密后调用,将结果转化为c1||c3||c2 + * + * @param c1c2c3 + * @return + */ + private static byte[] changeC1C2C3ToC1C3C2(byte[] c1c2c3) { + //sm2p256v1的这个固定65 + final int c1Len = (x9ECParameters.getCurve().getFieldSize() + 7) / 8 * 2 + 1; + // 可看GMNamedCurves、ECCurve代码。 + final int c3Len = 32; + byte[] result = new byte[c1c2c3.length]; + //c1 + System.arraycopy(c1c2c3, 0, result, 0, c1Len); + //c3 + System.arraycopy(c1c2c3, c1c2c3.length - c3Len, result, c1Len, c3Len); + //c2 + System.arraycopy(c1c2c3, c1Len, result, c1Len + c3Len, c1c2c3.length - c1Len - c3Len); + return result; + } + + private static BCECPrivateKey getPrivateKeyFromD(BigInteger d) { + ECPrivateKeySpec ecPrivateKeySpec = new ECPrivateKeySpec(d, ecParameterSpec); + return new BCECPrivateKey(Const.ALGORITHM_EC, ecPrivateKeySpec, BouncyCastleProvider.CONFIGURATION); + } + + private static BCECPublicKey getPublicKeyFromXY(BigInteger x, BigInteger y) { + ECPublicKeySpec ecPublicKeySpec = new ECPublicKeySpec(x9ECParameters.getCurve().createPoint(x, y), + ecParameterSpec); + return new BCECPublicKey(ALGORITHM_EC, ecPublicKeySpec, BouncyCastleProvider.CONFIGURATION); + } + + private static byte[] bigIntToFixedLengthBytes(BigInteger rOrS) { + byte[] rs = rOrS.toByteArray(); + if (rs.length == RS_LEN) { + return rs; + } else if (rs.length == RS_LEN + 1 && rs[0] == 0) { + return Arrays.copyOfRange(rs, 1, RS_LEN + 1); + } else if (rs.length < RS_LEN) { + byte[] result = new byte[RS_LEN]; + Arrays.fill(result, (byte) 0); + System.arraycopy(rs, 0, result, RS_LEN - rs.length, rs.length); + return result; + } else { + throw new RuntimeException("err rs: " + Hex.toHexString(rs)); + } + } + + /** + * BC的SM3withSM2签名得到的结果的rs是asn1格式的,这个方法转化成直接拼接r||s + * + * @param rsDer rs in asn1 format + * @return sign result in plain byte array + */ + private static byte[] rsAsn1ToPlainByteArray(byte[] rsDer) { + ASN1Sequence seq = ASN1Sequence.getInstance(rsDer); + byte[] r = bigIntToFixedLengthBytes(ASN1Integer.getInstance(seq.getObjectAt(0)).getValue()); + byte[] s = bigIntToFixedLengthBytes(ASN1Integer.getInstance(seq.getObjectAt(1)).getValue()); + byte[] result = new byte[RS_LEN * 2]; + System.arraycopy(r, 0, result, 0, r.length); + System.arraycopy(s, 0, result, RS_LEN, s.length); + return result; + } + + private static byte[] signSm3WithSm2Asn1Rs(byte[] msg, PrivateKey privateKey) + throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { + java.security.Signature signer = java.security.Signature.getInstance(Const.ALGORITHM_SM3_WITH_SM2, BC); + signer.initSign(privateKey, new SecureRandom()); + signer.update(msg, 0, msg.length); + byte[] sig = signer.sign(); + return sig; + } +} diff --git a/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/util/SymmetricEncrypt.java b/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/util/SymmetricEncrypt.java new file mode 100644 index 00000000..9898678c --- /dev/null +++ b/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/util/SymmetricEncrypt.java @@ -0,0 +1,289 @@ +package com.cib.fintech.dfp.open.sdk.util; + +import com.cib.fintech.dfp.open.sdk.exception.SdkExType; +import com.cib.fintech.dfp.open.sdk.exception.SdkException; +import org.bouncycastle.jce.provider.BouncyCastleProvider; + +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.security.Key; +import java.security.SecureRandom; + +/** + * 字段加密工具类 + *

+ * 支持对称加密算法 AES 的加密、解密,本类涉及的byte编码均为UTF-8 + * + * @author zengminghua + */ +public class SymmetricEncrypt { + + private static final BouncyCastleProvider BC = new BouncyCastleProvider(); + + /** + * 字段加密算法:SM4 + */ + private static final String ALGORITHM_SM4 = "SM4"; + + /** + * 密码器类型,算法/模式/补码方式 + */ + private static final String ALGORITHM_SM4_CBC_PKCS5 = "SM4/CBC/PKCS5Padding"; + + /** + * 字段加密算法:AES + */ + private static final String ALGORITHM_AES = "AES"; + + /** + * 密码器类型,算法/模式/补码方式 + */ + private static final String ALGORITHM_AES_CBC_PKCS5 = "AES/CBC/PKCS5Padding"; + + /** + * 密钥长度 + */ + private static final int SYMMETRIC_KEY_LENGTH = 128; + + /** + * 安全随机数算法 + */ + private static final String SECURE_RANDOM_ALGORITHMS = "SHA1PRNG"; + + /** + * 密钥长度 + */ + private static final int SM4_KEY_LENGTH = 128; + + /** + * AES对称加密 + * + * @param content 待加密内容 + * @param reqParamEncryptKey 字段加密密钥 + * @return result 密文(Base64 encode) + */ + public static String aesEncrypt(String content, String reqParamEncryptKey) throws Exception { + + try { + // 创建密码器 + Cipher cipher = Cipher.getInstance(ALGORITHM_AES_CBC_PKCS5); + + // 初始化向量值 + IvParameterSpec iv = new IvParameterSpec(initIv(ALGORITHM_AES_CBC_PKCS5)); + + // 使用密钥初始化,设置为 [加密模式] 的密码器 + cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(reqParamEncryptKey), iv); + byte[] result = cipher.doFinal(content.getBytes(Const.CHARSET)); + + // 通过Base64转码返回 + return Base64.encode(result); + } catch (Exception e) { + throw new SdkException(SdkExType.REQ_PARAM_ENCRYPT_ERR); + } + } + + /** + * AES解密操作 + * + * @param content 待解密内容 + * @param reqParamEncryptKey 字段加密密钥 + * @return result 明文 + */ + public static String aesDecrypt(String content, String reqParamEncryptKey) throws Exception { + + try { + // 创建密码器 + Cipher cipher = Cipher.getInstance(ALGORITHM_AES_CBC_PKCS5); + + // 初始化向量值 + IvParameterSpec iv = new IvParameterSpec(initIv(ALGORITHM_AES_CBC_PKCS5)); + + // 使用密钥初始化,设置为 [解密模式] 的密码器 + cipher.init(Cipher.DECRYPT_MODE, getSecretKey(reqParamEncryptKey), iv); + + byte[] result = cipher.doFinal(Base64.decode(content)); + return new String(result, Const.CHARSET); + } catch (Exception e) { + throw new SdkException(SdkExType.REQ_PARAM_ENCRYPT_ERR); + } + } + + /** + * 生成密钥对象 + * + * @param symmetricKey 密钥 + * @return + */ + private static SecretKeySpec getSecretKey(String symmetricKey) throws Exception { + + // 返回生成指定算法密钥生成器的 KeyGenerator 对象 + KeyGenerator keyGenerator = null; + + try { + // 指定为AES的KeyGenerator + keyGenerator = KeyGenerator.getInstance(ALGORITHM_AES); + + // 指定随机数 + SecureRandom random = SecureRandom.getInstance(SECURE_RANDOM_ALGORITHMS); + random.setSeed(symmetricKey.getBytes(Const.CHARSET)); + + keyGenerator.init(SYMMETRIC_KEY_LENGTH, random); + SecretKey secretKey = keyGenerator.generateKey(); + byte[] enCodeFormat = secretKey.getEncoded(); + + // 转换为AES专用密钥 + SecretKeySpec secretKeySpec = new SecretKeySpec(enCodeFormat, ALGORITHM_AES); + return secretKeySpec; + } catch (Exception e) { + throw new SdkException(SdkExType.REQ_PARAM_ENCRYPT_ERR); + } + } + + /** + * 生成SM4密钥,返回Base64编码后的密钥值(长度:24) + * + * @param + * @return + */ + public static String generateSM4Key() { + byte[] keyBytes = generateSM4KeyBytes(); + String stringKey = Base64.encode(keyBytes); + return stringKey; + } + + /** + * 生成SM4密钥,返回byte数组 + * + * @param + * @return + */ + public static byte[] generateSM4KeyBytes() { + KeyGenerator keyGenerator; + try { + keyGenerator = KeyGenerator.getInstance(ALGORITHM_SM4, BC); + } catch (Exception e) { + return null; + } + + keyGenerator.init(SM4_KEY_LENGTH, new SecureRandom()); + SecretKey secretKey = keyGenerator.generateKey(); + return secretKey.getEncoded(); + } + + /** + * 初始向量的方法, 全部为0 + *

+ * + * @param algorithm + * @return + */ + private static byte[] initIv(String algorithm) { + + try { + Cipher cipher = Cipher.getInstance(algorithm); + int blockSize = cipher.getBlockSize(); + byte[] iv = new byte[blockSize]; + for (int i = 0; i < blockSize; ++i) { + iv[i] = 0; + } + return iv; + } catch (Exception e) { + + int blockSize = 16; + byte[] iv = new byte[blockSize]; + for (int i = 0; i < blockSize; ++i) { + iv[i] = 0; + } + return iv; + } + } + + /** + * 初始向量的方法, 全部为0 + * + * @param algorithm + * @return + */ + private static byte[] initBCIv(String algorithm) { + + try { + Cipher cipher = Cipher.getInstance(algorithm, BC); + int blockSize = cipher.getBlockSize(); + byte[] iv = new byte[blockSize]; + for (int i = 0; i < blockSize; ++i) { + iv[i] = 0; + } + return iv; + } catch (Exception e) { + + int blockSize = 16; + byte[] iv = new byte[blockSize]; + for (int i = 0; i < blockSize; ++i) { + iv[i] = 0; + } + return iv; + } + } + + /** + * SM4加密 + * + * @param content 待加密内容 + * @param reqParamEncryptKey 加密密钥 + * @return 密文(Base64 encode) + */ + public static String sm4Encrypt(String content, String reqParamEncryptKey) throws Exception { + + try { + // 创建密码器 + Cipher cipher = Cipher.getInstance(ALGORITHM_SM4_CBC_PKCS5, BC); + + // 初始化向量值 + IvParameterSpec iv = new IvParameterSpec(initBCIv(ALGORITHM_SM4_CBC_PKCS5)); + Key sm4Key = new SecretKeySpec(Base64.decode(reqParamEncryptKey), ALGORITHM_SM4); + // 使用密钥初始化,设置为 [加密模式] 的密码器 + cipher.init(Cipher.ENCRYPT_MODE, sm4Key, iv); + + // 加密 + byte[] result = cipher.doFinal(content.getBytes(StandardCharsets.UTF_8)); + + // 通过Base64转码返回 + return Base64.encode(result); + } catch (Exception e) { + throw new SdkException(SdkExType.REQ_PARAM_ENCRYPT_ERR); + } + } + + /** + * SM4解密操作 + * + * @param content 待解密内容 + * @param reqParamEncryptKey 解密密钥 + * @return 明文 + */ + public static String sm4Decrypt(String content, String reqParamEncryptKey) throws Exception { + + try { + // 创建密码器 + Cipher cipher = Cipher.getInstance(ALGORITHM_SM4_CBC_PKCS5, BC); + + // 初始化向量值 + IvParameterSpec iv = new IvParameterSpec(initBCIv(ALGORITHM_SM4_CBC_PKCS5)); + Key sm4Key = new SecretKeySpec(Base64.decode(reqParamEncryptKey), ALGORITHM_SM4); + // 使用密钥初始化,设置为 [解密模式] 的密码器 + cipher.init(Cipher.DECRYPT_MODE, sm4Key, iv); + + // 解密 + byte[] result = cipher.doFinal(Base64.decode(content)); + + // 返回明文 + return new String(result, StandardCharsets.UTF_8); + } catch (Exception e) { + throw new SdkException(SdkExType.REQ_PARAM_ENCRYPT_ERR); + } + } +} diff --git a/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/util/VerifyRespSignature.java b/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/util/VerifyRespSignature.java new file mode 100644 index 00000000..92a7f1e9 --- /dev/null +++ b/server/dfp-open-sdk/src/main/java/com/cib/fintech/dfp/open/sdk/util/VerifyRespSignature.java @@ -0,0 +1,189 @@ +package com.cib.fintech.dfp.open.sdk.util; + +import com.cib.fintech.dfp.open.sdk.config.KeyConfigure; +import com.cib.fintech.dfp.open.sdk.enums.RespSignAlgorithmEnum; +import com.cib.fintech.dfp.open.sdk.exception.SdkExType; +import com.cib.fintech.dfp.open.sdk.exception.SdkException; +import org.bouncycastle.asn1.ASN1EncodableVector; +import org.bouncycastle.asn1.ASN1Integer; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.asn1.gm.GMNamedCurves; +import org.bouncycastle.asn1.x9.X9ECParameters; +import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.jce.spec.ECParameterSpec; +import org.bouncycastle.jce.spec.ECPublicKeySpec; +import org.bouncycastle.util.BigIntegers; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.KeyFactory; +import java.security.PublicKey; +import java.security.Signature; +import java.security.spec.X509EncodedKeySpec; +import java.util.Arrays; + +/** + * 响应报文验签工具类 + * + * @author zengminghua + */ +public class VerifyRespSignature { + + private static final BouncyCastleProvider BC = new BouncyCastleProvider(); + + private static final int RS_LEN = 32; + + private static X9ECParameters x9ECParameters = GMNamedCurves.getByName("sm2p256v1"); + private static ECParameterSpec ecParameterSpec = new ECParameterSpec(x9ECParameters.getCurve(), + x9ECParameters.getG(), x9ECParameters.getN()); + + /** + * 响应报文验签 + * + * @param timestamp 响应报文头部的时间戳 + * @param nonce 响应报文头部的随机值 + * @param responseSignValue 响应报文头部的签名值 + * @param retn 响应报文 + * @throws SdkException + */ + public static void verify(String timestamp, String nonce, String responseSignValue, Object retn, + KeyConfigure keyConfigure) throws SdkException { + + if (SdkUtil.notBlank(timestamp) && SdkUtil.notBlank(responseSignValue)) { + try { + String respPubKey = keyConfigure.getRespPubKey(); + String verifyAlgorithm = keyConfigure.getRespSignAlgorithm().getLabel(); + byte[] bodyContentBytes; + if (retn instanceof String) { + String content = (String) retn; + bodyContentBytes = content.getBytes(Const.CHARSET); + } else { + bodyContentBytes = (byte[]) retn; + } + + // 待验签bytes内容为时间戳bytes+报文bodyBytes + byte[] timestampBytes = timestamp.getBytes(Const.CHARSET); + byte[] nonceBytes = nonce.getBytes(Const.CHARSET); + byte[] contentBytes = new byte[timestampBytes.length + nonceBytes.length + bodyContentBytes.length]; + System.arraycopy(timestampBytes, 0, contentBytes, 0, timestampBytes.length); + System.arraycopy(nonceBytes, 0, contentBytes, timestampBytes.length, nonceBytes.length); + System.arraycopy(bodyContentBytes, 0, contentBytes, timestampBytes.length + nonceBytes.length, + bodyContentBytes.length); + + boolean flag; + if (RespSignAlgorithmEnum.SM3WITHSM2.equals(keyConfigure.getRespSignAlgorithm())) { + flag = verifyBySM2(contentBytes, responseSignValue, respPubKey); + } else { + flag = verifyByRSA(contentBytes, responseSignValue, respPubKey, verifyAlgorithm); + } + + if (!flag) { + throw new SdkException(SdkExType.RESPONSE_SIGN_ERR); + } + } catch (Exception e) { + throw new SdkException(SdkExType.RESPONSE_SIGN_ERR); + } + } else { + throw new SdkException(SdkExType.RESPONSE_SIGN_ERR); + } + } + + /** + * 验签,支持SHA1WithRSA、SHA256WithRSA算法 + * + * @param contentBytes 待验签名的内容 + * @param sign 签名值 + * @param publicKey 公钥 + * @param verifyAlgorithm 验签算法 + * @return 验签结果(true / false) + */ + private static boolean verifyByRSA(byte[] contentBytes, String sign, String publicKey, String verifyAlgorithm) { + try { + byte[] b = Base64.decode(publicKey); + X509EncodedKeySpec keySpec = new X509EncodedKeySpec(b); + KeyFactory factoty = KeyFactory.getInstance(Const.SIGN_TYPE); + PublicKey pubKey = factoty.generatePublic(keySpec); + java.security.Signature signature = java.security.Signature.getInstance(verifyAlgorithm); + signature.initVerify(pubKey); + signature.update(contentBytes); + + return signature.verify(Base64.decode(sign)); + } catch (Exception e) { + // e.printStackTrace(); + return false; + } + } + + /** + * SM3WithSM2验签 + * + * @param contentBytes 待验签名的内容 + * @param sign 签名值 + * @param publicKey 公钥 + * @return + */ + public static boolean verifyBySM2(byte[] contentBytes, String sign, String publicKey) { + byte[] publicKeyBytes = Base64.decode(publicKey); + if (publicKeyBytes.length != 64 && publicKeyBytes.length != 65) { + return false; + } + + BigInteger x, y; + if (publicKeyBytes.length > 64) { + x = BigIntegers.fromUnsignedByteArray(publicKeyBytes, 1, 32); + y = BigIntegers.fromUnsignedByteArray(publicKeyBytes, 33, 32); + } else { + x = BigIntegers.fromUnsignedByteArray(publicKeyBytes, 0, 32); + y = BigIntegers.fromUnsignedByteArray(publicKeyBytes, 32, 32); + } + BCECPublicKey bcecPublicKey = getPublicKeyFromXY(x, y); + byte[] rs = Base64.decode(sign); + return verifySm3WithSm2Asn1Rs(contentBytes, rsPlainByteArrayToAsn1(rs), bcecPublicKey); + } + + private static BCECPublicKey getPublicKeyFromXY(BigInteger x, BigInteger y) { + ECPublicKeySpec ecPublicKeySpec = new ECPublicKeySpec(x9ECParameters.getCurve().createPoint(x, y), + ecParameterSpec); + return new BCECPublicKey(Const.ALGORITHM_EC, ecPublicKeySpec, BouncyCastleProvider.CONFIGURATION); + } + + /** + * BC的SM3withSM2验签需要的rs是asn1格式的,这个方法将直接拼接r||s的字节数组转化成asn1格式 + * + * @param sign in plain byte array + * @return rs result in asn1 format + */ + public static byte[] rsPlainByteArrayToAsn1(byte[] sign) { + if (sign.length != RS_LEN * 2) { + throw new RuntimeException("err rs. "); + } + BigInteger r = new BigInteger(1, Arrays.copyOfRange(sign, 0, RS_LEN)); + BigInteger s = new BigInteger(1, Arrays.copyOfRange(sign, RS_LEN, RS_LEN * 2)); + ASN1EncodableVector v = new ASN1EncodableVector(); + v.add(new ASN1Integer(r)); + v.add(new ASN1Integer(s)); + try { + return new DERSequence(v).getEncoded("DER"); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * @param msg + * @param rs in asn1 format + * @param publicKey + * @return + */ + private static boolean verifySm3WithSm2Asn1Rs(byte[] msg, byte[] rs, PublicKey publicKey) { + try { + Signature signature = Signature.getInstance(Const.ALGORITHM_SM3_WITH_SM2, BC); + signature.initVerify(publicKey); + signature.update(msg, 0, msg.length); + return signature.verify(rs); + } catch (Exception e) { + return false; + } + } +} diff --git a/server/like-common/pom.xml b/server/like-common/pom.xml index 97e6a934..2ed6d68a 100644 --- a/server/like-common/pom.xml +++ b/server/like-common/pom.xml @@ -225,6 +225,16 @@ weixin-java-pay + + + org.bouncycastle + bcprov-jdk15on + + + org.bouncycastle + bcpkix-jdk15on + + com.google.zxing diff --git a/server/like-front/pom.xml b/server/like-front/pom.xml index 66e200c6..92810d20 100644 --- a/server/like-front/pom.xml +++ b/server/like-front/pom.xml @@ -23,6 +23,12 @@ like-common + + + org.mdd + dfp-open-sdk + + cn.dev33 diff --git a/server/pom.xml b/server/pom.xml index 03f0e37f..1ae1bae3 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -13,6 +13,7 @@ like-admin like-front like-common + dfp-open-sdk mozhe-enrollment like-generator @@ -56,6 +57,9 @@ 4.4.0 3.5.1 + 1.60 + + 1.2.83 @@ -263,12 +267,37 @@ ${zxing.version} + + org.bouncycastle + bcprov-jdk15on + ${jdk15on.version} + + + + org.bouncycastle + bcpkix-jdk15on + ${jdk15on.version} + + org.mdd like-common ${like.version} + + + + org.mdd + dfp-open-sdk + ${like.version} + + + + com.alibaba + fastjson + ${fastjson-legacy.version} +