调用CFCA金信反欺诈服务相关接口,很详细

调用CFCA金信反欺诈服务相关接口,很详细

  • 一、准备
  • 二、调用接口
    • 1、查询接口文档
    • 2、查看代码示例
    • 3、测试调用接口
  • 三、工具类
    • 1、CFCA金信反欺诈服务接口码枚举类
    • 2、CFCA金信反欺诈服务的公共参数配置
    • 3、加密解密工具类
    • 4、请求参数dto
    • 5、调用接口工具类(关键是这个)

一、准备

之前对接过CFCA安心签相关的接口,以为这次对接也会很麻烦,现实是这次比想象中的要简单一点,起码加白名单就很快。

老规矩,先找CFCA对接的技术人员要相关资料,并让他们帮你的服务器加上白名单。

这是金信反欺诈服务的产品,放在Excel表中,看需要使用哪一个产品

调用CFCA金信反欺诈服务相关接口,很详细_第1张图片

找到目标产品后,找对接的技术人员要相关的文档和demo。

二、调用接口

我这次对接的是运营商风险识别 (三要素详版),也就是三要素核验接口,以下称为三要素核验

调用CFCA金信反欺诈服务相关接口,很详细_第2张图片

1、查询接口文档

这是三要素核验对应的技术文档,是一个PDF文件

调用CFCA金信反欺诈服务相关接口,很详细_第3张图片

先看请求参数

调用CFCA金信反欺诈服务相关接口,很详细_第4张图片

调用CFCA金信反欺诈服务相关接口,很详细_第5张图片

对应请求体示例:

{
	"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": ""
	}
}

然后是响应参数

调用CFCA金信反欺诈服务相关接口,很详细_第6张图片

每个接口成功的响应码相同,失败的响应可能不一样,这是三要素核验的响应码

调用CFCA金信反欺诈服务相关接口,很详细_第7张图片
调用CFCA金信反欺诈服务相关接口,很详细_第8张图片

2、查看代码示例

三要素核验的代码示例在一个压缩包里,解压之后就可以找到对应的代码示例,所有的代码示例都在这里

调用CFCA金信反欺诈服务相关接口,很详细_第9张图片

解压之后可以看到所有的示例

调用CFCA金信反欺诈服务相关接口,很详细_第10张图片

不知道代码示例在那可以先看这里

调用CFCA金信反欺诈服务相关接口,很详细_第11张图片

基本上列举了所有产品接口

调用CFCA金信反欺诈服务相关接口,很详细_第12张图片

这是三要素核验的接口码

调用CFCA金信反欺诈服务相关接口,很详细_第13张图片

我要找的三要素核验示例代码在这

调用CFCA金信反欺诈服务相关接口,很详细_第14张图片

调用CFCA金信反欺诈服务相关接口,很详细_第15张图片

对应三要素核验的示例代码

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); // 调用此方法设置拼接请求地址、生成签名信息、对数据进行加密,后发起请求、解密响应数据。
    }

}

示例里面只有请求参数,没有响应参数,需要查看接口文档

调用CFCA金信反欺诈服务相关接口,很详细_第16张图片

调用接口还需要对应的证书文件,在这里

调用CFCA金信反欺诈服务相关接口,很详细_第17张图片

“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();
    }
}

这里只是测试调用的代码,详情可以查看工具类那里,所有代码都放在那了。

调用成功后控制台打印是这样的:

调用CFCA金信反欺诈服务相关接口,很详细_第18张图片

返回结果格式化后是这样的:

调用CFCA金信反欺诈服务相关接口,很详细_第19张图片

三、工具类

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是之前写的一个判空的工具类,感兴趣可以前往查看,也可以用自己的方式判空。

你可能感兴趣的:(学习,开发语言,java)