调用CFCA金信反欺诈服务相关接口,很详细
- 一、准备
- 二、调用接口
- 1、查询接口文档
- 2、查看代码示例
- 3、测试调用接口
- 三、工具类
- 1、CFCA金信反欺诈服务接口码枚举类
- 2、CFCA金信反欺诈服务的公共参数配置
- 3、加密解密工具类
- 4、请求参数dto
- 5、调用接口工具类(关键是这个)
一、准备
之前对接过CFCA安心签相关的接口,以为这次对接也会很麻烦,现实是这次比想象中的要简单一点,起码加白名单就很快。
老规矩,先找CFCA对接的技术人员要相关资料,并让他们帮你的服务器加上白名单。
这是金信反欺诈服务的产品,放在Excel表中,看需要使用哪一个产品
找到目标产品后,找对接的技术人员要相关的文档和demo。
二、调用接口
我这次对接的是运营商风险识别 (三要素详版),也就是三要素核验接口,以下称为三要素核验
1、查询接口文档
这是三要素核验对应的技术文档,是一个PDF文件
先看请求参数
对应请求体示例:
{
"transcode": "209",
"sign": "...",
"version": "0100",
"ordersn": "...",
"dsorderid": "...",
"merchno": "...",
"timestamp": "1643170610550",
"data": {
"username": "用户名",
"idcard": "身份证",
"mobile": "手机号",
"idtype": "01",
"sceneCode": "0x",
"sCustomerName": "xx-xx-xx",
"scUsePurpose": "xxxxx",
"protocolVerNm": "xxxxxxx",
"serialNm": "xxxxxxxxx",
"reqIp": "",
"remark": ""
}
}
然后是响应参数
每个接口成功的响应码相同,失败的响应可能不一样,这是三要素核验的响应码
2、查看代码示例
三要素核验的代码示例在一个压缩包里,解压之后就可以找到对应的代码示例,所有的代码示例都在这里
解压之后可以看到所有的示例
不知道代码示例在那可以先看这里
基本上列举了所有产品接口
这是三要素核验的接口码
我要找的三要素核验示例代码在这
对应三要素核验的示例代码
package com.jinxin;
import com.jinxin.utils.BaseTest;
import com.jinxin.utils.TransCodeEnum;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* author: weidong
* create: 2021-06-13
**/
public class Test209 extends BaseTest {
public static void main(String[] args) throws Exception {
// 获取枚举类中指定业务的地址后缀部分,前缀需要在【商户信息.properties】填写
String suffix = TransCodeEnum.TRANS_CODE_209.getUrl();
// 请求最外层参数---------------------------------------------------------------------------------------
Map<String, Object> reqMap = new HashMap<>();
reqMap.put("transcode", TransCodeEnum.TRANS_CODE_209.getTranscode()); // 接口业务编码,具体业务请参考接口文档
reqMap.put("merchno", BaseTest.merchNo); // 商户号
reqMap.put("ordersn", UUID.randomUUID().toString().replaceAll("-", "")); //商户流水号
reqMap.put("dsorderid", UUID.randomUUID().toString().replaceAll("-", "")); //商户订单号
reqMap.put("version", "0100"); //api版本号
reqMap.put("timestamp", System.currentTimeMillis() + ""); // 时间戳
// 请求内层 data 参数====================================================================================
Map<String, Object> data = new HashMap<>();
data.put("username", "请输入姓名");
data.put("mobile", "请输入手机号");
data.put("idcard", "请输入身份证");
data.put("idtype", "01");
data.put("sCustomerName", ""); // 二级商户名称,例如:xxx商户名-yyy产品名称-zzz使用方法
data.put("sceneCode", ""); //真实场景 见接口文档枚举
data.put("scUsePurpose", ""); // 预留字段
data.put("protocolVerNm", ""); // 预留字段
data.put("serialNm", ""); // 预留字段
data.put("reqIp", ""); // 请求ip
data.put("remark", ""); // 备注
request(reqMap, data, suffix); // 调用此方法设置拼接请求地址、生成签名信息、对数据进行加密,后发起请求、解密响应数据。
}
}
示例里面只有请求参数,没有响应参数,需要查看接口文档
调用接口还需要对应的证书文件,在这里
“jks”文件夹下来的所有文件都需要。
3、测试调用接口
调用接口不需要导入jar包,直接将demo中的代码拿过来并且正确传入参数,那应该就可以调用成功了,但调用的过程中感觉demo中的代码繁琐了点,就做了一些简化,代码如下:
public static void main(String[] args) {
CFCAJINXINConfig config = new CFCAJINXINConfig();
config.setHost("请求地址前缀部分");
config.setKeystorePath("客户通信证书路径(证书需要申请)");
config.setKeystorePassword("客户通信证书密码");
config.setTruststorePath("金信通信证书信任库路径(证书由金信提供)");
config.setTruststorePassword("金信通信证书信任库密码");
config.setMerchSM2PrivateKey("商户私钥");
config.setJinXinSM2PublicKey("金信平台公钥");
config.setMerchNo("商户号");
config.setVersion("api版本号");
config.setTranscode(CFCAJINXINCodeEnum.CODE_209.getCode());
CFCAJINXINCode209ReqDto params = new CFCAJINXINCode209ReqDto();
params.setUsername("你的名字");
params.setMobile("你的手机号码");
params.setIdcard("你的身份证号码");
params.setIdtype("01");
try {
sendRequest(config, params);
} catch (Exception e) {
e.printStackTrace();
}
}
这里只是测试调用的代码,详情可以查看工具类那里,所有代码都放在那了。
调用成功后控制台打印是这样的:
返回结果格式化后是这样的:
三、工具类
1、CFCA金信反欺诈服务接口码枚举类
/**
* CFCA金信反欺诈服务接口码枚举类
*
* @author:gan
* @date: 2023-09-21 09:41
*/
public enum CFCAJINXINCodeEnum {
CODE_105("105", "银行卡风险识别(二要素简版))", "/jxdata/api/auth/jm/execute2.do", ""),
CODE_106("106", "银行卡风险识别(三要素简版))", "/jxdata/api/auth/jm/execute2.do", ""),
CODE_107("107", "银行卡风险识别(四要素简版))", "/jxdata/api/auth/jm/execute2.do", ""),
CODE_108("108", "运营商风险识别(二要素))", "/jxdata/api/auth/jm/execute2.do", ""),
CODE_109("109", "运营商风险识别(三要素)", "/jxdata/api/auth/jm/execute2.do", ""),
CODE_116("116", "银行卡风险识别(三要素详版)", "/jxdata/api/auth/jm/execute2.do", ""),
CODE_117("117", "银行卡风险识别(四要素详版-非身份证)", "/jxdata/api/auth/jm/execute2.do", ""),
CODE_118("118", "身份信息识别(国民二要素)", "/jxdata/api/auth/jm/execute2.do", ""),
CODE_209("209", "运营商风险识别(三要素详版)", "/jxdata/api/auth/jm/execute2.do", ""),
CODE_307("307", "身份信息识别(国民四要素)", "/jxdata/api/auth/jm/execute2.do", ""),
// 313 业务有两个url,请查看 Test313代码中的路径
CODE_313("313", "H5活体识别+人像比对", "", ""),
// 314 业务有两个url,请查看 Test314代码中的路径
CODE_314("314", "H5活体识别", "", ""),
CODE_405("405", "银行卡账户风险协查", "/jxdata/api/auth/jm/execute2.do", ""),
CODE_407("407", "账户单卡认证", "/jxdata/api/auth/jm/execute2.do", ""),
CODE_408("408", "账户三要素认证", "/jxdata/api/auth/jm/execute2.do", ""),
CODE_412("412", "银行卡账户风险协查(反欺诈识别)", "/jxdata/api/auth/jm/execute2.do", ""),
CODE_610("610", "不良银联卡识别", "/jxdata/api/auth/jm/execute2.do", ""),
CODE_611("611", "银联不良持卡人识别", "/jxdata/api/auth/jm/execute2.do", ""),
CODE_612("612", "银联风险电话识别", "/jxdata/api/auth/jm/execute2.do", ""),
CODE_901("901", "企业信息三要素认证", "/jxdata/api/auth/jm/execute2.do", ""),
CODE_902("902", "对公账户二要素", "/jxdata/api/auth/jm/execute2.do", ""),
CODE_906("906", "企业信息认证四要素", "/jxdata/api/auth/jm/execute2.do", ""),
CODE_121("121", "身份信息识别(实名人像认证)", "/jxdata/api/auth/living/execute2.do", ""),
CODE_124("124", "身份信息识别(身份证照片比对)", "/jxdata/api/auth/living/execute2.do", ""),
CODE_311("311", "静默活体认证", "/jxdata/api/auth/living/execute2.do", ""),
// 309 业务有两个url,请查看 Test309 代码中的路径
CODE_309("309", "读数活体认证", "", ""),
CODE_903("903", "企业工商信息查询", "/jxdata/api/auth/company/execute2.do", ""),
CODE_907("907", "LEI编码査企业信息", "/jxdata/api/auth/company/execute2.do", ""),
CODE_908("908", "企业四要素信息认证详版", "/jxdata/api/auth/company/execute2.do", ""),
CODE_910("910", "企业司法风险报告", "/jxdata/api/auth/company/execute2.do", ""),
CODE_911("911", "企业经营异常核查", "/jxdata/api/auth/company/execute2.do", ""),
CODE_912("912", "企业股权穿透识别", "/jxdata/api/auth/company/execute2.do", ""),
CODE_913("913", "企业经纬度查询", "/jxdata/api/auth/company/execute2.do", ""),
CODE_914("914", "税务登记号核验", "/jxdata/api/auth/company/execute2.do", ""),
CODE_915("915", "企业深度查验报告", "/jxdata/api/auth/company/execute2.do", ""),
CODE_916("916", "一址多照核查", "/jxdata/api/auth/company/execute2.do", ""),
CODE_917("917", "企业最终受益人查询", "/jxdata/api/auth/company/execute2.do", ""),
CODE_919("919", "纸质营业执照二维码查询", "/jxdata/api/auth/company/execute2.do", ""),
CODE_922("922", "严重违法核查", "/jxdata/api/auth/company/execute2.do", ""),
// 923 业务有两个url,请查看 Test923 代码中的路径
CODE_923_9231("923", "小额打款", "/jxdata/api/auth/company/execute2.do", ""),
CODE_925("925", "发票要素核查", "/jxdata/api/auth/company/execute2.do", ""),
CODE_926("926", "企业工商详细信息查询", "/jxdata/api/auth/company/execute2.do", ""),
CODE_927("927", "诉讼信息核查", "/jxdata/api/auth/company/execute2.do", ""),
CODE_928("928", "企业知识产权核查", "/jxdata/api/auth/company/execute2.do", ""),
CODE_929("929", "税务信用列表核查", "/jxdata/api/auth/company/execute2.do", ""),
CODE_930("930", "进出口信用列表核查", "/jxdata/api/auth/company/execute2.do", ""),
CODE_9301("9301", "电子营业执照", "/jxdata/api/auth/company/execute2.do", ""),
// TRANS_CODE_903("926", "企业工商信息详细查询", "/jxdata/api/auth/company/execute2.do", ""),
CODE_932("932", "企业经营状况报告", "/jxdata/api/auth/company/execute2.do", ""),
CODE_933("933", "证书状态查询", "/jxdata/api/auth/company/execute2.do", ""),
CODE_934("934", "证书所属机构查询", "/jxdata/api/auth/company/execute2.do", ""),
CODE_935("935", "企业证书信息验证", "/jxdata/api/auth/company/execute2.do", ""),
CODE_936("936", "企业多维智能反欺诈评分", "/jxdata/api/auth/company/execute2.do", ""),
CODE_937("937", "企业综合信用报告", "/jxdata/api/auth/company/execute2.do", ""),
CODE_938("938", "企业信用决策评分", "/jxdata/api/auth/company/execute2.do", ""),
CODE_939("939", "个人严重违法核查", "/risk/api/execute2.do", ""),
CODE_940("940", "电信诈骗识别(含账户涉诈/客户涉诈", "/risk/api/execute2.do", ""),
CODE_941("941", "涉金融风险黑名单", "/risk/api/execute2.do", ""),
CODE_942("942", "涉诉信息核查(详版)", "/risk/api/execute2.do", ""),
CODE_945("945", "企业经营风险核查", "/risk/api/execute2.do", ""),
CODE_946("946", "企业综合风险评分", "/risk/api/execute2.do", ""),
CODE_947("947", "企业经营信息", "/risk/api/execute2.do", ""),
CODE_948("948", "企业红名单", "/risk/api/execute2.do", ""),
CODE_949("949", "企业评价信息", "/risk/api/execute2.do", ""),
CODE_943("943", "企业失信核查", "/risk/api/execute2.do", ""),
CODE_944("944", "企业处罚核查", "/risk/api/execute2.do", ""),
CODE_957("957", "企业法人信息核验", "/jxdata/api/auth/jm/execute2.do", ""),
CODE_960("960", "企业信息核验", "/jxdata/api/auth/jm/execute2.do", ""),
CODE_953("953", "小微企业查询", "/jxdata/api/auth/company/execute2.do", ""),
CODE_951("951", "网站备案信息查询", "/jxdata/api/auth/company/execute2.do", ""),
CODE_955("955", "税务评级查询", "/jxdata/api/auth/company/execute2.do", ""),
CODE_954("954", "组织机构信息查询", "/jxdata/api/auth/company/execute2.do", ""),
CODE_950("950", "企业族谱查询", "/jxdata/api/auth/company/execute2.do", ""),
CODE_962("962", "企业名称模糊查询", "/jxdata/api/auth/company/execute2.do", ""),
CODE_961("961", "附近公司信息查询", "/jxdata/api/auth/company/execute2.do", ""),
CODE_956("956", "商圈企业信息查询", "/jxdata/api/auth/company/execute2.do", ""),
CODE_958("958", "企业信息查询(照面)", "/jxdata/api/auth/company/execute2.do", ""),
CODE_965("965", "企业信息验证(三要素)", "/jxdata/api/auth/company/execute2.do", ""),
CODE_966("966", "企业法人信息验证(四要素)", "/jxdata/api/auth/company/execute2.do", ""),
CODE_959("959", "企业信息查询(增值)", "/jxdata/api/auth/company/execute2.do", ""),
CODE_952("952", "企业人员信息查询", "/jxdata/api/auth/company/execute2.do", ""),
CODE_971("971", "企业人员信息查询", "/jxdata/api/auth/company/execute2.do", ""),
CODE_973("973", "IP风险核查", "/jxdata/api/auth/company/execute2.do", ""),
CODE_974("974", "IP归属地核查-IPv4版", "/jxdata/api/auth/company/execute2.do", ""),
CODE_975("975", "IP归属地核查-IPv6版", "/jxdata/api/auth/company/execute2.do", ""),
CODE_970("970", "工商风险核查报告", "/jxdata/api/auth/company/execute2.do", ""),
CODE_977("977", "电信诈骗识别-商户版", "/risk/api/execute2.do", ""),
CODE_978("978", "涉金融风险黑名单-商户版", "/risk/api/execute2.do", ""),
CODE_979("979", "电子营业执照申请授权二维码", "/jxdata/api/auth/company/execute2.do", ""),
CODE_980("980", "电子营业执照验证授权二维码", "/jxdata/api/auth/company/execute2.do", ""),
CODE_981("981", "电子营业执照执照信息查询", "/jxdata/api/auth/company/execute2.do", ""),
CODE_982("982", "电子营业执照执照有效性验证", "/jxdata/api/auth/company/execute2.do", ""),
CODE_983("983", "电子营业执照企业人员身份验证", "/jxdata/api/auth/company/execute2.do", ""),
CODE_984("984", "电子营业执照持照人身份验证", "/jxdata/api/auth/company/execute2.do", ""),;
private final String code;
private final String desc;
private final String url;
private final String params; // 备用属性,暂时无用
CFCAJINXINCodeEnum(String code, String desc, String url, String params) {
this.code = code;
this.desc = desc;
this.url = url;
this.params = params;
}
public String getCode() {
return code;
}
public String getUrl() {
return url;
}
public static String getUrl(String code) {
for (CFCAJINXINCodeEnum codeEnum : values()) {
if (codeEnum.getCode().equals(code)) {
return codeEnum.url;
}
}
throw new RuntimeException("未知CFCA金信接口码:" + code);
}
public String getDesc() {
return desc;
}
public static String getDesc(String code) {
for (CFCAJINXINCodeEnum codeEnum : values()) {
if (codeEnum.getCode().equals(code)) {
return codeEnum.desc;
}
}
throw new RuntimeException("未知CFCA金信接口码:" + code);
}
public String getParams() {
return params;
}
public static CFCAJINXINCodeEnum getByCode(String code) {
if (code == null || "".equals(code)) {
throw new NullPointerException("v 为空");
}
for (CFCAJINXINCodeEnum codeEnum : values()) {
if (codeEnum.getCode().equals(code)) {
return codeEnum;
}
}
throw new NullPointerException("没有此业务");
}
}
2、CFCA金信反欺诈服务的公共参数配置
import lombok.Data;
import java.io.Serializable;
/**
* CFCA 金信反欺诈服务的公共参数配置dto
*
* @author:gan
* @date: 2023-09-21 10:10
*/
@Data
public class CFCAJINXINConfig implements Serializable {
//请求地址前缀部分,格式:http://192.168.1.223:8091
private String host;
//商户号
private String merchNo;
//金信平台公钥
private String jinXinSM2PublicKey;
//商户私钥
private String merchSM2PrivateKey;
//客户通信证书路径(证书需要申请)
private String keystorePath;
//客户通信证书密码
private String keystorePassword;
//金信通信证书信任库路径(证书由金信提供)
private String truststorePath;
//金信通信证书信任库密码
private String truststorePassword;
//接口业务编码
private String transcode;
//商户流水号
private String ordersn;
//商户订单号
private String dsorderid;
//api版本号
private String version;
//时间戳
private String timestamp;
//二级商户名称,例如:xxx商户名-yyy产品名称-zzz使用方法
private String sCustomerName;
//真实场景 见接口文档枚举
private String sceneCode;
//预留字段
private String scUsePurpose;
//预留字段
private String protocolVerNm;
//预留字段
private String serialNm;
//请求ip
private String reqIp;
//备注
private String remark;
}
3、加密解密工具类
这是demo里面的
import cfca.sadk.org.bouncycastle.util.encoders.Hex;
import org.bouncycastle.asn1.gm.GMNamedCurves;
import org.bouncycastle.asn1.gm.GMObjectIdentifiers;
import org.bouncycastle.asn1.x9.X9ECParameters;
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.math.ec.ECPoint;
import org.bouncycastle.util.encoders.Base64;
//import org.bouncycastle.util.encoders.Hex;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.math.BigInteger;
import java.security.*;
import java.security.spec.ECGenParameterSpec;
import java.security.spec.InvalidKeySpecException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* bcprov-jdk15on 版本适用(1.61-1.68)
*/
public class BC_SM2 {
private BouncyCastleProvider provider;
private X9ECParameters parameters;
private ECParameterSpec ecParameterSpec;
private KeyFactory keyFactory;
static {
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
System.out.println("security provider BC not found");
// 注意:注册BouncyCastle是通过下面的语句实现的。注册只需要在启动时进行一次,后续就可以使用BouncyCastle提供的所有哈希算法和加密算法。
Security.addProvider(new BouncyCastleProvider());
}
}
public BC_SM2(){
try {
provider = new BouncyCastleProvider();
parameters = GMNamedCurves.getByName("sm2p256v1");
ecParameterSpec = new ECParameterSpec(parameters.getCurve(),
parameters.getG(), parameters.getN(), parameters.getH());
keyFactory = KeyFactory.getInstance("EC", provider);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 生成密钥对
*/
public KeyPair generateSm2KeyPair() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException {
final ECGenParameterSpec sm2Spec = new ECGenParameterSpec("sm2p256v1");
// 获取一个椭圆曲线类型的密钥对生成器
final KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC", provider);
SecureRandom random = new SecureRandom();
// 使用SM2的算法区域初始化密钥生成器
kpg.initialize(sm2Spec, random);
// 获取密钥对
KeyPair keyPair = kpg.generateKeyPair();
return keyPair;
}
/**
* 加密
*
* @param input 待加密文本
* @param pubKey 公钥
*/
public String encode(String input, String pubKey)
throws NoSuchPaddingException, NoSuchAlgorithmException,
BadPaddingException, IllegalBlockSizeException,
InvalidKeySpecException, InvalidKeyException {
// 获取SM2相关参数
X9ECParameters parameters = GMNamedCurves.getByName("sm2p256v1");
// 椭圆曲线参数规格
ECParameterSpec ecParameterSpec = new ECParameterSpec(parameters.getCurve(), parameters.getG(), parameters.getN(), parameters.getH());
// 将公钥HEX字符串转换为椭圆曲线对应的点
ECPoint ecPoint = parameters.getCurve().decodePoint(Hex.decode(pubKey));
// 获取椭圆曲线KEY生成器
KeyFactory keyFactory = KeyFactory.getInstance("EC", provider);
BCECPublicKey key = (BCECPublicKey) keyFactory.generatePublic(new ECPublicKeySpec(ecPoint, ecParameterSpec));
// 获取SM2加密器
Cipher cipher = Cipher.getInstance("SM2", provider);
// 初始化为加密模式
cipher.init(Cipher.ENCRYPT_MODE, key);
// 加密并编码为base64格式
return cfca.sadk.org.bouncycastle.util.encoders.Base64.toBase64String(cipher.doFinal(input.getBytes()));
}
/**
* 解密
*
* @param input 待解密文本
* @param prvKey 私钥
*/
public String decoder(String input, String prvKey) throws NoSuchPaddingException, NoSuchAlgorithmException,
InvalidKeySpecException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
// 获取SM2加密器
Cipher cipher = Cipher.getInstance("SM2", provider);
// 将私钥HEX字符串转换为X值
BigInteger bigInteger = new BigInteger(prvKey, 16);
BCECPrivateKey privateKey = (BCECPrivateKey) keyFactory.generatePrivate(new ECPrivateKeySpec(bigInteger,
ecParameterSpec));
// 初始化为解密模式
cipher.init(Cipher.DECRYPT_MODE, privateKey);
// 解密
//return cipher.doFinal(Base64.decode(input));
return new String(cipher.doFinal(org.bouncycastle.util.encoders.Base64.decode(input)));
}
/**
* 签名
*
* @param plainText 待签名文本
* @param prvKey 私钥
*/
public String sign(String plainText, String prvKey) throws NoSuchAlgorithmException, InvalidKeySpecException,
InvalidKeyException, SignatureException {
// 创建签名对象
Signature signature = Signature.getInstance(GMObjectIdentifiers.sm2sign_with_sm3.toString(), provider);
// 将私钥HEX字符串转换为X值
BigInteger bigInteger = new BigInteger(prvKey, 16);
BCECPrivateKey privateKey = (BCECPrivateKey) keyFactory.generatePrivate(new ECPrivateKeySpec(bigInteger,
ecParameterSpec));
// 初始化为签名状态
signature.initSign(privateKey);
// 传入签名字节
signature.update(plainText.getBytes());
// 签名
return org.bouncycastle.util.encoders.Hex.toHexString(signature.sign());
}
/**
* 根据 map 的 value、key排序拼接,生成待签名字符串,并签名
*/
public String sign(Map<String, Object> map, String prvKey) throws NoSuchAlgorithmException, InvalidKeySpecException,
InvalidKeyException, SignatureException {
return sign(getSortStr(map), prvKey);
}
public String getSortStr(Map<String, Object> sortedParams) {
StringBuilder signSrc = new StringBuilder();
List<String> keys = new ArrayList<String>(sortedParams.keySet());
Collections.sort(keys);
for (String key : keys) {
Object value = sortedParams.get(key);
if (key != null && !"".equals(key) && value != null && !"sign".equals(key)) {
signSrc.append(key).append("=").append(value);
}
}
return signSrc.toString();
}
/**
* 验证签名
*/
public boolean verify(String plainText, String signatureValue, String pubKey) throws NoSuchAlgorithmException, InvalidKeySpecException,
InvalidKeyException, SignatureException {
Signature signature = Signature.getInstance(GMObjectIdentifiers.sm2sign_with_sm3.toString(), provider);
ECPoint ecPoint = parameters.getCurve().decodePoint(Hex.decode(pubKey));
BCECPublicKey key = (BCECPublicKey) keyFactory.generatePublic(new ECPublicKeySpec(ecPoint, ecParameterSpec));
signature.initVerify(key);
signature.update(plainText.getBytes());
return signature.verify(Hex.decode(signatureValue));
}
public static void main(String[] args) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException {
System.out.println("----------------------jdk版本信息----------------------------------------------------------");
System.out.println(System.clearProperty("java.version"));
System.out.println(System.clearProperty("os.name"));
System.out.println("----------------------测试 开始----------------------------------------------------------");
String str = "test encode";
BC_SM2 sm2 = new BC_SM2();
KeyPair keyPair = sm2.generateSm2KeyPair();
BCECPrivateKey privateKey = (BCECPrivateKey) keyPair.getPrivate();
BCECPublicKey publicKey = (BCECPublicKey) keyPair.getPublic();
// 拿到密钥
String pubKey = "04" + (publicKey.getQ().getXCoord() + "" + publicKey.getQ().getYCoord());
String prvKey = privateKey.getD().toString(16);
System.out.println("Private Key: " + prvKey);
System.out.println("Public Key: " + pubKey);
// 加解密测试
try {
System.out.println("加密前:" + str);
String encode = sm2.encode(str, pubKey);
System.out.println("加密后:" + encode);
String decoder = new String(sm2.decoder(encode, prvKey));
System.out.println("解密后:" + decoder);
} catch (Exception e) {
e.printStackTrace();
System.out.println("加解密错误");
}
// 签名和验签测试
try {
System.out.println("签名源数据:" + str);
String signStr = sm2.sign(str, prvKey);
System.out.println("签名后数据:" + signStr);
boolean verify = sm2.verify(str, signStr, pubKey);
System.out.println("签名验证结果:" + verify);
} catch (Exception e) {
System.out.println("签名和验签错误");
}
}
}
4、请求参数dto
调哪个接口就创建对应的请求参数的dto,我这个是三要素核验的dto。因为在工具类中用反射动态设置参数,属性名称必须与请求参数名称保持一致。
package org.jeecg.modules.cfca.dto;
import lombok.Data;
/**
* 运营商风险识别dto
*
* 验证用户手机号码与姓名、证件号码与运营商实名预留信息是否一致,不一致的情况下返回具体要素不符
*
* @author:gan
* @date: 2023-09-21 10:17
*/
@Data
public class CFCAJINXINCode209ReqDto {
//姓名
private String username;
//手机号码
private String mobile;
//身份证号
private String idcard;
//证件类型,只支持身份证,传 01
private String idtype;
}
5、调用接口工具类(关键是这个)
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jeecg.common.util.VerifyUtil;
import org.jeecg.modules.cfca.dto.CFCAJINXINCode209ReqDto;
import org.jeecg.modules.cfca.dto.CFCAJINXINConfig;
import org.jeecg.modules.cfca.enums.CFCAJINXINCodeEnum;
import javax.net.ssl.*;
import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.security.KeyStore;
import java.util.*;
/**
* CFCA金信反欺诈服务工具类
*
* @author:gan
* @date: 2023-09-21 09:53
*/
public class CFCAJINXINUtils {
private static Logger log = LogManager.getLogger();
private static BC_SM2 bcSm2 = new BC_SM2(); //加密解密需要用到
private final static String GET_STR = "get"; //get方法字符
private final static String GET_CLASS_STR = "getClass"; //getClass方法字符
private static StringBuilder host; // 请求地址前缀部分,格式:http://192.168.1.223:8091
private static String jinXinSM2PublicKey; // 金信平台公钥
private static String merchSM2PrivateKey; // 商户私钥
private static String keystorePath; // 客户通信证书路径(证书需要申请)
private static String keystorePassword; // 客户通信证书密码
private static String truststorePath; // 金信通信证书信任库路径(证书由金信提供)
private static String truststorePassword; // 金信通信证书信任库密码
private static String fullUrl; // 当前接口业务完整地址
/**
* 初始化CFCA金信反欺诈服务需要的配置
* @param config
*/
private static void init(CFCAJINXINConfig config) {
VerifyUtil.checkParam(config, "CFCA金信反欺诈服务请求参数为空!");
List<String> noEmptyFieldList = Arrays.asList("host", "merchNo", "jinXinSM2PublicKey", "merchSM2PrivateKey", "keystorePath", "keystorePassword", "truststorePath", "truststorePassword", "transcode", "version");
VerifyUtil.checkBeanByNonEmptyFiledList(config, noEmptyFieldList, "CFCA金信反欺诈服务请求参数");
host = new StringBuilder();
host.append(config.getHost());
int length = host.length();
if (host.indexOf("/", length - 1) > 0) { //最后一个字符为“/”,则去掉
host = host.delete(length - 1, length);
}
fullUrl = host.append(CFCAJINXINCodeEnum.getUrl(config.getTranscode())).toString();
jinXinSM2PublicKey = config.getJinXinSM2PublicKey();
merchSM2PrivateKey = config.getMerchSM2PrivateKey();
keystorePath = config.getKeystorePath();
keystorePassword = config.getKeystorePassword();
truststorePath = config.getTruststorePath();
truststorePassword = config.getTruststorePassword();
}
/**
* 发送CFCA金信反欺诈服务请求
* @param config
* @param params
*/
public static JSONObject sendRequest(CFCAJINXINConfig config, Object params) throws Exception {
init(config);
// 请求最外层参数,所有接口公共参数
Map<String, Object> reqMap = new HashMap<>();
reqMap.put("transcode", config.getTranscode()); // 接口业务编码,具体业务请参考接口文档
reqMap.put("merchno", config.getMerchNo()); // 商户号
reqMap.put("ordersn", UUID.randomUUID().toString().replaceAll("-", "")); //商户流水号
reqMap.put("dsorderid", UUID.randomUUID().toString().replaceAll("-", "")); //商户订单号
reqMap.put("version", config.getVersion()); //api版本号
reqMap.put("timestamp", System.currentTimeMillis()); // 时间戳
// 请求内层 data 参数,所有接口公共参数
Map<String, Object> data = new HashMap<>();
data.put("sCustomerName", config.getSCustomerName()); // 二级商户名称,例如:xxx商户名-yyy产品名称-zzz使用方法
data.put("sceneCode", config.getSceneCode()); //真实场景 见接口文档枚举
data.put("scUsePurpose", config.getScUsePurpose()); // 预留字段
data.put("protocolVerNm", config.getProtocolVerNm()); // 预留字段
data.put("serialNm", config.getSerialNm()); // 预留字段
data.put("reqIp", config.getReqIp()); // 请求ip
data.put("remark", config.getRemark()); // 备注
//这里设置的是不同接口独有的参数,也是data中的参数
setInterfaceSpecificParams(params, data);
return request(reqMap, data);
}
/**
* 设置不同接口特定参数
* @param params 传入请求参数
* @param data 调用接口请求参数
*/
private static void setInterfaceSpecificParams(Object params, Map<String, Object> data) {
Method[] methods = params.getClass().getMethods();
Method currentMethod = null; //当前方法
String currentMethodName = null; //当前方法名称
StringBuilder currentPropertyName = new StringBuilder(); //当前属性名称
for (int i = 0; i < methods.length; i++) {
currentMethod = methods[i];
currentMethodName = currentMethod.getName();
if (GET_CLASS_STR.equals(currentMethodName)) { //getClass方法不为请求参数
continue;
}
if (currentMethodName.startsWith(GET_STR) && currentMethodName.length() > 3) {
currentPropertyName.append(currentMethodName.substring(3)); //截取currentMethodName字符串的中索引第四到最后的字符,也就是去掉“get”字符
currentPropertyName.replace(0, 1, currentPropertyName.substring(0, 1).toLowerCase()); //将首字符的大写改为小写
try {
data.put(currentPropertyName.toString(), currentMethod.invoke(params));
currentPropertyName.delete(0, currentPropertyName.length()); //设置完参数就清除StringBuilder中的数据,给下一个参数设置时用
} catch (IllegalAccessException e) {
e.printStackTrace();
throw new IllegalArgumentException(e);
} catch (InvocationTargetException e) {
e.printStackTrace();
throw new RuntimeException(e.getMessage());
}
}
}
}
/**
* 处理请求参数并发送请求
* @param reqMap 外层请求参数
* @param data data请求参数
* @throws Exception
*/
private static JSONObject request(Map<String, Object> reqMap, Map<String, Object> data) throws Exception {
//reqMap的值转化为字符
String reqMapToStr = JSONObject.toJSONString(reqMap, SerializerFeature.WriteNonStringValueAsString);
//data的值转化为字符
String dataToStr = JSONObject.toJSONString(data, SerializerFeature.WriteNonStringValueAsString);
//获取【金信平台】SM2 公钥,对 data 使用 SM2 进行加密
reqMap.put("data", bcSm2.encode(dataToStr, jinXinSM2PublicKey));
// 对请求报文生成签名信息
reqMap.put("sign", getSign(reqMap, merchSM2PrivateKey));
log.info("请求地址:{}" , fullUrl);
log.info("请求参数:{}" , dataToStr);
log.info("完整的请求参数:{}" , reqMapToStr);
String result = post(fullUrl, JSONObject.toJSONBytes(reqMap), keystorePath, keystorePassword, truststorePath, truststorePassword); // 发起请求
return response(result);
}
/**
* 处理响应
* @param response 响应结果字符
* @throws Exception
*/
private static JSONObject response(String response) throws Exception {
log.info("响应参数:{}" , response);
JSONObject respJson = JSONObject.parseObject(response);
String sortStr = bcSm2.getSortStr(respJson);
Object respSign = respJson.get("sign");
Object respData = respJson.get("data");
JSONObject decryptDataJsonObj = null; //转化为JSONObject的data数据
if (VerifyUtil.isNotEmpty(respSign) && bcSm2.verify(sortStr, String.valueOf(respSign), jinXinSM2PublicKey)) {
if (VerifyUtil.isNotEmpty(respData)) {
String decryptData = bcSm2.decoder(respData.toString(), merchSM2PrivateKey); // 解密
decryptDataJsonObj = JSONObject.parseObject(decryptData);
respJson.put("data", decryptDataJsonObj); // 后续可根据实际情况替换Object.class
log.info("解密响应参数:{}" , JSONObject.toJSONString(respJson, SerializerFeature.WriteNonStringValueAsString));
log.info("响应参数中的data:{}" , decryptData);
} else {
log.info("【data】接口没有返回或为空");
}
} else {
log.info("验签失败");
}
return respJson;
}
/**
* 判断是否请求成功
* @param respJson
*/
public static boolean isSuccess(JSONObject respJson) {
return "001000000".equals(respJson.getString("platformCode")); //001000000代表接口调用成功,因为只有这里用到,每个接口的返回参数又不一样,不再用枚举类
}
/**
* 接口调用失败时抛出错误信息
* @param respJson
*/
public static void getErrorInfo(JSONObject respJson) {
if (!isSuccess(respJson)) {
throw new RuntimeException(respJson.getString("platformDesc"));
}
}
/**
* 发送HTTP请求分类判断
* @param url 请求路径
* @param request 转化为byte数组后的请求参数
* @param keystorePath 通信证书路径
* @param keystorePassword 通信证书密码
* @param truststorePath 金信通信证书信任库路径
* @param truststorePassword 金信通信证书信任库密码
* @return
* @throws Exception
*/
private static String post(String url, byte[] request, String keystorePath, String keystorePassword, String truststorePath, String truststorePassword) throws Exception {
if (url.startsWith("https://")) {
return doPostHttps(url, request, keystorePath, keystorePassword, truststorePath, truststorePassword);
} else {
return doPostHttp(url, request);
}
}
/**
* 发送HTTP请求,主要就是这个
* @param reqUrl 请求路径
* @param reqData 转化为byte数组后的请求参数
* @param keystorePath 通信证书路径
* @param keystorePassword 通信证书密码
* @param truststorePath 金信通信证书信任库路径
* @param truststorePassword 金信通信证书信任库密码
* @return
* @throws Exception
*/
private static String doPostHttps(String reqUrl, byte[] reqData, String keystorePath, String keystorePassword, String truststorePath, String truststorePassword) throws Exception {
OutputStream outputStream = null;
BufferedReader bufferedReader = null;
HttpsURLConnection httpsURLConnection = null;
try {
// 初始化密钥库
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("SunX509");
KeyStore keyStore = getKeyStore(keystorePath, keystorePassword);
keyManagerFactory.init(keyStore, keystorePassword.toCharArray());
// 初始化信任库
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("SunX509");
KeyStore trustKeyStore = getKeyStore(truststorePath, truststorePassword);
trustManagerFactory.init(trustKeyStore);
// 初始化SSL上下文
SSLContext ctx = SSLContext.getInstance("SSL");
ctx.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null);
SSLSocketFactory sf = ctx.getSocketFactory();
URL url = new URL(reqUrl);
httpsURLConnection = (HttpsURLConnection) url.openConnection();
httpsURLConnection.setSSLSocketFactory(sf);
//设置链接超时时间
httpsURLConnection.setConnectTimeout(60000);
//设置读取超时时间
httpsURLConnection.setReadTimeout(60000);
// 设置是否向httpUrlConnection输出,因为这个是post请求,参数要放在
// http正文内,因此需要设为true, 默认情况下是false;
httpsURLConnection.setDoOutput(true);
// 设置是否从httpUrlConnection读入,默认情况下是true;
httpsURLConnection.setDoInput(true);
// Post 请求不能使用缓存
httpsURLConnection.setUseCaches(false);
// 设定传送的内容类型是json UTF-8
httpsURLConnection.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
// 设定请求的方法为"POST",默认是GET
httpsURLConnection.setRequestMethod("POST");
// 此处getOutputStream会隐含的进行connect(即:如同调用上面的connect()方法,
// 所以在开发中不调用上述的connect()也可以)。
outputStream = httpsURLConnection.getOutputStream();
// 向对象输出流写出数据,这些数据将存到内存缓冲区中
outputStream.write(reqData);
// 刷新对象输出流,将任何字节都写入潜在的流中(些处为ObjectOutputStream)
outputStream.flush();
// 调用HttpURLConnection连接对象的getInputStream()函数,
// 将内存缓冲区中封装好的完整的HTTP请求电文发送到服务端。
// httpsURLConnection.getInputStream() 注意,实际发送请求的代码段就在这里
bufferedReader = new BufferedReader(new InputStreamReader(httpsURLConnection.getInputStream(), "UTF-8"));
StringBuilder jsonString = new StringBuilder();
String line = "";
while ((line = bufferedReader.readLine()) != null) {
jsonString.append(line);
}
return jsonString.toString();
} catch (Exception e) {
e.printStackTrace();
log.error(e.getMessage(), e);
return null;
} finally {
if (outputStream != null) {
outputStream.close();
}
if (bufferedReader != null) {
bufferedReader.close();
}
if(httpsURLConnection != null) {
httpsURLConnection.disconnect();
}
}
}
// 备用方法
private static String doPostHttp(String urlStr, byte[] request) throws Exception {
DataOutputStream objOutputStrm = null;
OutputStream outStrm = null;
InputStream inStrm = null;
try {
URL url = new URL(urlStr);
// 此处的urlConnection对象实际上是根据URL的 // 请求协议(此处是http)生成的URLConnection类 // 的子类HttpURLConnection,故此处最好将其转化 // 为HttpURLConnection类型的对象,以便用到 // HttpURLConnection更多的API.如下:
URLConnection rulConnection = url.openConnection();
HttpURLConnection httpUrlConnection = (HttpURLConnection) rulConnection;
//设置链接超时时间
httpUrlConnection.setConnectTimeout(6000000);
//设置读取超时时间
httpUrlConnection.setReadTimeout(6000000);
// 设置是否向httpUrlConnection输出,因为这个是post请求,参数要放在
// http正文内,因此需要设为true, 默认情况下是false;
httpUrlConnection.setDoOutput(true);
// 设置是否从httpUrlConnection读入,默认情况下是true;
httpUrlConnection.setDoInput(true);
// Post 请求不能使用缓存
httpUrlConnection.setUseCaches(false);
// 设定传送的内容类型是可序列化的java对象
// (如果不设此项,在传送序列化对象时,当WEB服务默认的不是这种类型时可能抛java.io.EOFException)
httpUrlConnection.setRequestProperty("Content-type", "application/json");
// 设定请求的方法为"POST",默认是GET
httpUrlConnection.setRequestMethod("POST");
// 此处getOutputStream会隐含的进行connect(即:如同调用上面的connect()方法,
// 所以在开发中不调用上述的connect()也可以)。
outStrm = httpUrlConnection.getOutputStream();
// 现在通过输出流对象构建对象输出流对象,以实现输出可序列化的对象。
objOutputStrm = new DataOutputStream(outStrm);
// 向对象输出流写出数据,这些数据将存到内存缓冲区中
objOutputStrm.write(request);
// 刷新对象输出流,将任何字节都写入潜在的流中(些处为ObjectOutputStream)
objOutputStrm.flush();
// 调用HttpURLConnection连接对象的getInputStream()函数,
// 将内存缓冲区中封装好的完整的HTTP请求电文发送到服务端。
inStrm = httpUrlConnection.getInputStream(); // <===注意,实际发送请求的代码段就在这里
BufferedReader read = new BufferedReader(new InputStreamReader(inStrm, "UTF-8"));
StringBuffer jsonString = new StringBuffer();
String line = "";
while ((line = read.readLine()) != null) {
jsonString.append(line);
}
String json = jsonString.toString();
return json;
} catch (Exception e) {
e.printStackTrace();
log.error(e.getMessage(), e);
return null;
} finally {
if (objOutputStrm != null)
objOutputStrm.close();
if (outStrm != null) {
outStrm.close();
}
if (inStrm != null)
inStrm.close();
}
}
/**
* 获得KeyStore
*/
private static KeyStore getKeyStore(String keyStorePath, String password) throws Exception {
FileInputStream is = new FileInputStream(keyStorePath);
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
ks.load(is, password.toCharArray());
is.close();
return ks;
}
/**
* 使用私钥加签
* @param reqParams 待加签的map集合,会对此map集合的key做排序,遍历拼接
* @param merchSM2PrivateKey 商户私钥
*/
private static String getSign(Map<String, Object> reqParams, String merchSM2PrivateKey) throws Exception {
StringBuilder signSrc = new StringBuilder();
List<String> keys = new ArrayList<>(reqParams.keySet());
Collections.sort(keys);
for (String key : keys) {
Object value = reqParams.get(key);
if (key != null && !"".equals(key) && value != null && !"sign".equals(key)) {
signSrc.append(key).append("=").append(value);
}
}
String sign = bcSm2.sign(signSrc.toString(), merchSM2PrivateKey);
return sign;
}
public static void main(String[] args) {
CFCAJINXINConfig config = new CFCAJINXINConfig();
config.setHost("请求地址前缀部分");
config.setKeystorePath("客户通信证书路径(证书需要申请)");
config.setKeystorePassword("客户通信证书密码");
config.setTruststorePath("金信通信证书信任库路径(证书由金信提供)");
config.setTruststorePassword("金信通信证书信任库密码");
config.setMerchSM2PrivateKey("商户私钥");
config.setJinXinSM2PublicKey("金信平台公钥");
config.setMerchNo("商户号");
config.setVersion("api版本号");
config.setTranscode(CFCAJINXINCodeEnum.CODE_209.getCode());
CFCAJINXINCode209ReqDto params = new CFCAJINXINCode209ReqDto();
params.setUsername("你的名字");
params.setMobile("你的手机号码");
params.setIdcard("你的身份证号码");
params.setIdtype("01");
try {
sendRequest(config, params);
} catch (Exception e) {
e.printStackTrace();
}
}
}
其中VerifyUtil是之前写的一个判空的工具类,感兴趣可以前往查看,也可以用自己的方式判空。