调用安心签相关接口,很详细
一、获取CFCA安心签对接资料
先找CFCA安心签对接人员要相关资料,包括接口文档
案例代码如下:
以及测试需要的相关文件,一般会一起给。
二、调用接口
1、找安心签的对接人加上ip白名单
这里提一嘴,安心签加白名单的速度比较慢,起码要一天,并且不能加ip网段,如果ip不是固定的,测试起来会比较难受,如果要调安心签的请记得一定要先让他们加好白名单。
2、查询接口文档
比如我要调用txCode为3001(个人开户接口)
查看如何传参
3、查看对应的测试案例
在安心签对接人员给的Demo中找到对应的测试案例
具体如下:
package cfca.trustsign.demo.test;
import cfca.sadk.algorithm.common.PKIException;
import cfca.trustsign.common.vo.cs.HeadVO;
import cfca.trustsign.common.vo.cs.PersonVO;
import cfca.trustsign.common.vo.request.tx3.Tx3001ReqVO;
import cfca.trustsign.demo.connector.HttpConnector;
import cfca.trustsign.demo.constant.Request;
import cfca.trustsign.demo.converter.JsonObjectMapper;
import cfca.trustsign.demo.util.SecurityUtil;
import cfca.trustsign.demo.util.TimeUtil;
public class Test3001 {
public static void main(String[] args) throws PKIException {
HttpConnector httpConnector = new HttpConnector();
httpConnector.init();
PersonVO person = PersonVO.builder().personName("刘三十jiu").identTypeCode("1").identNo("110102200001071156").mobilePhone("95010117774")
.email("[email protected]").authenticationMode("公安部").build();
// 监管平台注意:用户首次开户时会签发五年有效期证书,五年后如果证书过期会导致用户签名时交易失败,所以平台要注意证书的有效期。
// 若五年到期后交易失败,平台可再次对用户进行实名认证并调用此接口完成证书更新操作。
// 平台亦可自行记录开户时间,五年到期前90天内(安心签默认到期前90天内可更新证书),平台可再次对用户进行实名认证并调用此接口提前完成证书的展期操作,避免用户签章操作的不连续。
person.setAuthenticationTime(TimeUtil.getCurrentTime(TimeUtil.FORMAT_14));
Tx3001ReqVO tx3001ReqVO = new Tx3001ReqVO();
tx3001ReqVO.setHead(HeadVO.builder().txTime(TimeUtil.getCurrentTime(TimeUtil.FORMAT_14)).build());
tx3001ReqVO.setPerson(person);
String req = new JsonObjectMapper().writeValueAsString(tx3001ReqVO);
System.out.println("req:" + req);
String txCode = "3001", signature = SecurityUtil.p7SignMessageDetach(HttpConnector.JKS_PATH, HttpConnector.JKS_PWD, HttpConnector.ALIAS, req);
// String txCode = "3001", signature = SecurityUtil.p7SignMessageDetachSm2(HttpConnector.SCER_PATH, HttpConnector.SCER_PASS, HttpConnector.ALIAS, req);
String res = httpConnector.post("platId/" + Request.PLAT_ID + "/txCode/" + txCode + "/transaction", req, signature);
System.out.println("res:" + res);
}
}
4、导入安心签相关jar包
安心签没有对应的maven依赖,需要将示例中的Demo中的jar包中添加到项目中,如果找对接人员要了多次Demo,则导入最新的jar包,因为旧的jar包可能缺少一些东西。
这是Demo中的jar包,不需要全部都导入,只需要导入调用接口用到的就可以了
主要是用到这个
5、测试调用接口
public void testTxCode3001() throws Exception {
AnxinqianConfig anxinqianConfig = new AnxinqianConfig();
anxinqianConfig.setJKS_PATH("你的JKS通讯证书文件绝对路径");
anxinqianConfig.setJKS_PWD("你的通讯证书密码");
anxinqianConfig.setALIAS("你制作通讯证书时的设置");
anxinqianConfig.setPLAT_ID("你的安心签平台id");
anxinqianConfig.setUrl("https://安心签ip:443/FEP/");
anxinqianConfig.setTxCode(InterfCodeEnum.CODE_3001.getCode());
Tx3001ReqVO tx3001ReqVO = new Tx3001ReqVO();
HeadVO headVO = new HeadVO();
headVO.setTxTime(DateUtil.format(DateUtil.date(), DatePattern.PURE_DATETIME_PATTERN)); //格式为yyyyMMddHHmmss
PersonVO personVO = new PersonVO();
personVO.setPersonName("你的名字"); //个人名称
personVO.setIdentNo("你的身份证号码"); //证件号码
personVO.setMobilePhone("你的手机号码"); //手机号码
personVO.setAuthenticationMode("居民身份证"); //认证方式
personVO.setIsOpenSM2(0); //是否开通国密证书,0:不开通;1:开通;默认为0
tx3001ReqVO.setHead(headVO);
tx3001ReqVO.setPerson(personVO);
JSONObject responseJSONObject = AnxinqianUtil.sendRequest(anxinqianConfig, tx3001ReqVO);
log.info("测试3001接口,返回结果:{}", responseJSONObject);
}
解释:
Tx3001ReqVO 类的全限类名cfca.trustsign.common.vo.request.tx3.Tx3001ReqVO
HeadVO类的全限类名cfca.trustsign.common.vo.cs.HeadVO
PersonVO类的全限类名cfca.trustsign.common.vo.cs.PersonVO
时间相关的类来自hutool工具包
调用成功后控制台打印是这样的:
返回结果格式化后是这样的:
如果没有白名单,调用接口会报超时的错误:
调用任何应该接口都会报这个错。
6、调用下载合同接口(这个特殊)
下载合同接口与其他txCode开头的接口不太一样,这个接口只返回一个byte数组,这里单独拿出来说,也写了专门下载合同的方法,下面是测试代码:
public void testDownload() throws Exception {
AnxinqianConfig anxinqianConfig = new AnxinqianConfig();
anxinqianConfig.setJKS_PATH("你的JKS通讯证书文件绝对路径");
anxinqianConfig.setJKS_PWD("你的通讯证书密码");
anxinqianConfig.setALIAS("你制作通讯证书时的设置");
anxinqianConfig.setPLAT_ID("你的安心签平台id");
anxinqianConfig.setUrl("https://安心签ip:443/FEP/");
anxinqianConfig.setDownloadUrl("/opt/jeecg-boot/upload/hetong"); //安心签保存合同公共地址
anxinqianConfig.setHetongUrl("你配置访问下载安心签合同的公共域名"); //访问安心签下载合同公共域名,就是文件访问域名,配置Nginx解析到安心签保存合同公共地址
String contractNo = "CL20230406000010002"; //合同号
//获取安心签下载合同接口的部分路径,用于拼接到公共路径中
String uri = AnxinqianUtil.getDownloadContractUri(anxinqianConfig, contractNo);
byte[] fileByte = AnxinqianUtil.getFile(anxinqianConfig, uri);
//===============================下面这些是服务器创建合同文件的逻辑,可以自定义===============================
//合同不存在,直接结束方法
if (CommonUtil.isEmpty(fileByte))
throw new RuntimeException("合同不存在!");
String commonUploadUrl = anxinqianConfig.getDownloadUrl().endsWith("/") ? anxinqianConfig.getDownloadUrl() : anxinqianConfig.getDownloadUrl() + "/";
String uploadUrl = commonUploadUrl + contractNo + "/";
Path uploadPath = Paths.get(uploadUrl);
//创建文件夹及其父文件夹如果被创建文件夹的父文件夹不存在,就创建它如果被创建的文件夹已经存在,就是用已经存在的文件夹,不会重复创建,没有异常抛出
Files.createDirectories(uploadPath);
Files.write(Paths.get(uploadUrl + contractNo + ".pdf"), fileByte);
String contractUrl = (anxinqianConfig.getHetongUrl().endsWith("/") ? anxinqianConfig.getHetongUrl() : anxinqianConfig.getHetongUrl() + "/") + contractNo + "/" + contractNo + ".pdf";
log.info("合同路径:{}", contractUrl);
}
返回结果:
查看对应保存合同的路径:
可以看到合同已经下载下来了。使用域名访问
三、工具类
1、CFCA安心签接口枚举类
/**
* 安心签接口码枚举类
*
* @author:user
* @date: 2022-08-26 11:01
*/
public enum InterfCodeEnum {
CODE_3911("3911", "签署合同"), //废弃
CODE_3913("3913", "单一用户多合同H5签署"),
CODE_3001("3001", "个人信息验证"),
CODE_3101("3101", "发送验证码"),
CODE_3102("3102", "确认验证码"),
CODE_3201("3201", "创建合同"),
CODE_3210("3210", "合同查询");
private String code;
private String desc;
InterfCodeEnum(String code, String desc) {
this.code = code;
this.desc = desc;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public static String getDesc(String code) {
for (InterfCodeEnum value : values()) {
if (value.code.equals(code))
return value.desc;
}
throw new RuntimeException("未知接口码:" + code);
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
}
2、CFCA安心签配置类
import lombok.Data;
/**
* 安心签配置dto
*
* @author:user
* @date: 2022-08-22 16:53
*/
@Data
public class AnxinqianConfig {
//JKS通讯证书文件绝对路径
private String JKS_PATH;
//通讯证书密码
private String JKS_PWD;
//制作通讯证书时设置,若直接用AnXinSign_CertTool转换,则为anxinsign
private String ALIAS;
//账号注册成功时生成的,可登陆安心签测试页面查看或联系技术人员获取
private String PLAT_ID;
//公共接口地址
private String url;
//===============================上面的是公共的,不能为空的===============================
//具体哪个接口
private String uri;
//合同下载到本地的公共地址
private String downloadUrl;
//设置到合同表的公共域名
private String hetongUrl;
//授权地点,客户平台将采集到的用户IP或所在地发送给安心签
private String location;
//签名域的标签值,创建合同接口的createContract参数下的signLocation
private String signLocation;
//安心签项目编号,创建合同接口(3201)的createContract参数下的signInfos中的projectCode
private String projectCode3201;
//签名域的标签值,创建合同接口的createContract参数下的signInfos中的signLocation
private String signLocationBelow;
//安心签项目编号,发送验证码接口(3101)的proxySign参数下的projectCode
private String projectCode3101;
//安心签项目编号,发送验证码接口(3102)的proxySign参数下的projectCode
private String projectCode3102;
//===============================3913接口相关 start===============================
//签名域的标签值,单一用户多合同H5签署接口的sendShortUrl参数下的contractInfos中的signLocation
private String signLocationBelow3913;
//关键字,单一用户多合同H5签署接口的sendShortUrl参数下的contractInfos中的keywordInfo的keyword
private String keyword3913;
//关键字所在页,单一用户多合同H5签署接口的sendShortUrl参数下的contractInfos中的keywordInfo的pageNo
private String pageNo3913;
//关键字所在page页的索引,单一用户多合同H5签署接口的sendShortUrl参数下的contractInfos中的keywordInfo的indexInPage
private String indexInPage3913;
//X轴偏移坐标,单一用户多合同H5签署接口的sendShortUrl参数下的contractInfos中的keywordInfo的offsetX
//单位为:磅,最大偏移量为200
private String offsetX3913;
//Y轴偏移坐标,单一用户多合同H5签署接口的sendShortUrl参数下的contractInfos中的keywordInfo的offsetY
//单位为:磅,最大偏移量为200
private String offsetY3913;
//签章图片宽度,单一用户多合同H5签署接口的sendShortUrl参数下的contractInfos中的keywordInfo的width
//最大为400px
private String width3913;
//签章图片高度,单一用户多合同H5签署接口的sendShortUrl参数下的contractInfos中的keywordInfo的height
//最大为400px
private String height3913;
//===============================3913接口相关 end===============================
//接口码
private String txCode;
}
3、CFCA安心签返回结果码枚举类
这个主要用来判断调用接口有没有成功,没有成功我都是直接抛出错误的。
/**
* 安心签接口返回结果码
*
* @author:gan
* @date: 2023-04-03 17:49
*/
public enum RetCodeEnum {
CODE_60000000("60000000", "成功"),
CODE_60030401("60030401", "已经开通代签权限");
private String code;
private String desc;
RetCodeEnum(String code, String desc) {
this.code = code;
this.desc = desc;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public static String getDesc(String code) {
for (RetCodeEnum value : values()) {
if (value.getCode().equals(code)) {
return value.desc;
}
}
throw new RuntimeException("未知安心签返回结果码:" + code);
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
}
4、调用接口工具类(关键是这个)
package org.jeecg.modules.anxinqian.utils;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.util.VerifyUtil;
import org.jeecg.modules.anxinqian.constant.MIMEType;
import org.jeecg.modules.anxinqian.constant.Request;
import org.jeecg.modules.anxinqian.constant.SystemConst;
import org.jeecg.modules.anxinqian.dto.AnxinqianConfig;
import org.jeecg.modules.anxinqian.enums.RetCodeEnum;
import org.jeecg.modules.anxinqian.mapper.JsonObjectMapper;
import org.springframework.web.bind.annotation.RequestMethod;
import java.io.File;
import java.net.HttpURLConnection;
import java.net.URLEncoder;
import java.util.Map;
/**
* 安心签工具类
*
* @author:user
* @date: 2022-08-22 17:35
*/
@Slf4j
public class AnxinqianUtil {
public static String JKS_PATH = "./jks/admin.jks";
public static String JKS_PWD = "11111111";
public static String url = "https://localhost:8443/FEP/";
public static String channel = "Test";
public static String keyStorePath = JKS_PATH;
public static String keyStorePassword = JKS_PWD;
private final static HttpClient httpClient = new HttpClient();
// public void init() {
// httpClient = new HttpClient();
// httpClient.config.connectTimeout = connectTimeout;
// httpClient.config.readTimeout = readTimeout;
// httpClient.httpConfig.userAgent = "TrustSign FEP";
// httpClient.httpConfig.contentType = MIMEType.FORM;
// httpClient.httpConfig.accept = MIMEType.JSON;
// try {
// if (isSSL) {
// httpClient.initSSL(keyStorePath, keyStorePassword.toCharArray(), trustStorePath, trustStorePassword.toCharArray());
// }
// } catch (Exception e) {
// e.printStackTrace();
// }
// if (!url.endsWith("/")) {
// url += "/";
// }
// }
/**
* 初始化方法
* @param config
*/
public static void init(AnxinqianConfig config) {
httpClient.config.connectTimeout = 30000;
httpClient.config.readTimeout = 10000;
httpClient.httpConfig.userAgent = "TrustSign FEP";
httpClient.httpConfig.contentType = MIMEType.FORM;
httpClient.httpConfig.accept = MIMEType.JSON;
try {
getInterfUri(config);
keyStorePath = config.getJKS_PATH();
keyStorePassword = config.getJKS_PWD();
httpClient.initSSL(keyStorePath, keyStorePassword.toCharArray(), keyStorePath, keyStorePassword.toCharArray());
} catch (Exception e) {
e.printStackTrace();
}
url = config.getUrl();
if (!url.endsWith("/")) {
url += "/";
}
}
/**
* 发送安心签请求
* @param config 安心签配置
* @param requestParam 请求参数
* @param map 其他参数
*/
public static JSONObject sendRequest(AnxinqianConfig config, Object requestParam, Map<String, String> map) throws Exception {
init(config);
//处理请求参数
JsonObjectMapper requestJsonObjectMapper = new JsonObjectMapper();
String requestParamStr = requestJsonObjectMapper.writeValueAsString(requestParam);
log.info("处理后请求参数:{}", JSON.toJSONString(requestParam, SerializerFeature.WriteNullStringAsEmpty));
//这个应该是类似加密参数的操作
String signature = SecurityUtil.p7SignMessageDetach(config.getJKS_PATH(), config.getJKS_PWD(), config.getALIAS(), requestParamStr);
log.info("加密请求参数:{}", signature);
return deal(config.getUri(), RequestMethod.POST.toString(), prepare(requestParamStr, signature, map));
}
/**
* 发送安心签请求
* @param config 安心签配置
* @param requestParam 请求参数
*/
public static JSONObject sendRequest(AnxinqianConfig config, Object requestParam) throws Exception {
return sendRequest(config, requestParam, null);
}
/**
* 发送安心签请求
* @param config 安心签配置
* @param data 请求参数
* @param signature 环境参数加密
* @param file 文件
*/
public static void sendRequest(AnxinqianConfig config, String data, String signature, File file) throws Exception {
init(config);
deal(config.getUri(), RequestMethod.POST.toString(), data, file, signature);
}
/**
* 获取请求路径uri
* @param config 安心签配置
*/
public static void getInterfUri(AnxinqianConfig config) {
config.setUri("platId/" + config.getPLAT_ID() + "/txCode/" + config.getTxCode() + "/transaction");
}
/**
* 获取下载合同uri
* @param config 安心签配置
* @param contractNo 合同号
* @return
*/
public static String getDownloadContractUri(AnxinqianConfig config, String contractNo) {
return "platId/" + config.getPLAT_ID() + "/contractNo/" + contractNo.trim() + "/downloading";
}
public JSONObject post(String uri, String data, String signature) throws Exception {
return deal(uri, "POST", prepare(data, signature, null));
}
public JSONObject post(String uri, String data, String signature, Map<String, String> map) throws Exception {
return deal(uri, "POST", prepare(data, signature, map));
}
public String post(String uri, String data, String signature, File file) throws Exception {
return deal(uri, "POST", data, file, signature);
}
//这是原来的getFile
// public byte[] getFile(String uri) {
// HttpURLConnection connection = null;
// try {
// connection = httpClient.connect(url + uri, "GET");
// int responseCode = httpClient.send(connection, null);
// System.out.println("responseCode:" + responseCode);
// if (responseCode != 200) {
// System.out.println(CommonUtil.getString(httpClient.receive(connection)));
// return null;
// }
// return httpClient.receive(connection);
// } catch (Exception e) {
// e.printStackTrace();
// return null;
// } finally {
// httpClient.disconnect(connection);
// }
// }
//这是改造过的,下载合同用到
public static byte[] getFile(AnxinqianConfig config, String uri) throws Exception{
init(config);
HttpURLConnection connection = httpClient.connect(url + uri, RequestMethod.GET.toString());
int responseCode = httpClient.send(connection, null);
log.info("响应码:{}", responseCode);
return httpClient.receive(connection);
}
//这是原来的prepare方法
// private String prepare(String data, String signature, Map<String, String> map) {
// try {
// StringBuilder request = new StringBuilder();
// request.append(Request.CHANNEL).append("=").append(URLEncoder.encode(channel, SystemConst.DEFAULT_CHARSET));
// if (CommonUtil.isNotEmpty(data)) {
// request.append("&").append(Request.DATA).append("=").append(URLEncoder.encode(data, SystemConst.DEFAULT_CHARSET));
// }
// if (CommonUtil.isNotEmpty(signature)) {
// request.append("&").append(Request.SIGNATURE).append("=").append(URLEncoder.encode(signature, SystemConst.DEFAULT_CHARSET));
// }
// if (CommonUtil.isNotEmpty(map)) {
// for (Map.Entry<String, String> pair : map.entrySet()) {
// request.append("&").append(pair.getKey()).append("=")
// .append(pair.getValue() == null ? "" : URLEncoder.encode(pair.getValue(), SystemConst.DEFAULT_CHARSET));
// }
// }
// return request.toString();
// } catch (UnsupportedEncodingException e) {
// return null;
// }
// }
//这是改造过的prepare
private static String prepare(String data, String signature, Map<String, String> map) throws Exception {
StringBuilder request = new StringBuilder();
request.append(Request.CHANNEL).append("=").append(URLEncoder.encode(channel, SystemConst.DEFAULT_CHARSET));
if (CommonUtil.isNotEmpty(data)) {
request.append("&").append(Request.DATA).append("=").append(URLEncoder.encode(data, SystemConst.DEFAULT_CHARSET));
}
if (CommonUtil.isNotEmpty(signature)) {
request.append("&").append(Request.SIGNATURE).append("=").append(URLEncoder.encode(signature, SystemConst.DEFAULT_CHARSET));
}
if (CommonUtil.isNotEmpty(map)) {
for (Map.Entry<String, String> pair : map.entrySet()) {
request.append("&").append(pair.getKey()).append("=")
.append(pair.getValue() == null ? "" : URLEncoder.encode(pair.getValue(), SystemConst.DEFAULT_CHARSET));
}
}
return request.toString();
}
//这是原来的deal方法
// private String deal(String uri, String method, String request) {
// HttpURLConnection connection = null;
// try {
// connection = httpClient.connect(url + uri, method);
// System.out.println(url + uri);
// System.out.println(method);
// System.out.println(request);
// int responseCode = httpClient.send(connection, request == null ? null : CommonUtil.getBytes(request));
// System.out.println("responseCode:" + responseCode);
// System.out.println(connection.getHeaderFields());
// return CommonUtil.getString(httpClient.receive(connection));
// } catch (Exception e) {
// e.printStackTrace();
// return e.getMessage();
// } finally {
// httpClient.disconnect(connection);
// }
// }
/**
* 处理http请求
* @param uri
* @param method
* @param request
* @return
*/
private static JSONObject deal(String uri, String method, String request) throws Exception {
HttpURLConnection connection = httpClient.connect(url + uri, method);
log.info("请求地址:{}", url + uri);
log.info("请求方法:{}", method);
log.info("请求参数:{}", request);
int responseCode = httpClient.send(connection, request == null ? null : CommonUtil.getBytes(request));
log.info("响应码:{}", responseCode);
// log.info("连接头部信息:{}", connection.getHeaderFields());
JSONObject resultJson = JSONObject.parseObject(new String(httpClient.receive(connection), SystemConst.DEFAULT_CHARSET));
log.info("返回结果:{}", resultJson);
handleResult(resultJson);
return resultJson;
}
//这是原来的deal方法
// private String deal(String uri, String method, String request, File file, String signature) {
// HttpURLConnection connection = null;
// try {
// connection = httpClient.connect(url + uri, method);
// System.out.println(url + uri);
// System.out.println(method);
// System.out.println(request);
// int responseCode = httpClient.send(connection, request == null ? null : request, file, signature);
// System.out.println("responseCode:" + responseCode);
// return CommonUtil.getString(httpClient.receive(connection));
// } catch (Exception e) {
// e.printStackTrace();
// return e.getMessage();
// } finally {
// httpClient.disconnect(connection);
// }
// }
//这是改造的deal方法
private static String deal(String uri, String method, String request, File file, String signature) throws Exception {
HttpURLConnection connection = httpClient.connect(url + uri, method);
log.info("请求地址:{}", url + uri);
log.info("请求方法:{}", method);
log.info("请求参数:{}", request);
int responseCode = httpClient.send(connection, request == null ? null : request, file, signature);
log.info("响应码:{}", responseCode);
return CommonUtil.getString(httpClient.receive(connection));
}
/**
* 处理安心签返回结果(如果有错误就抛出)
* @param resultJson
* @return
*/
private static void handleResult(JSONObject resultJson) {
VerifyUtil.checkParam(resultJson, "安心签返回结果为空!"); //VerifyUtil是一个判空的类,之前的文章中有给出(https://blog.csdn.net/studio_1/article/details/123845497),也可以用自己的方式判空
String errorCode = resultJson.getString("errorCode");
if (VerifyUtil.isNotEmpty(errorCode)) {
String errorMessage = resultJson.getString("errorMessage");
if (RetCodeEnum.CODE_60030401.getCode().equals(errorCode)) {
log.info("errorMessage:{}", errorMessage);
return; //报这个错可以结束方法,返回错误码给前端
}
throw new RuntimeException(errorMessage);
}
JSONObject head = resultJson.getJSONObject("head");
if (!RetCodeEnum.CODE_60000000.getCode().equals(head.getString("retCode"))) //60000000表示成功
throw new RuntimeException(head.getString("retMessage"));
}
}