API对接实战
目录
一 背景
二 了解B公司接口的基础约定
三 基础域名
四 请求及相应格式说明
五 确定要对接哪些API
六 根据API文档,编写一些基础工具类。
七 根据API文档,编写必要的DTO
八 针对每个API方法,进行对接
九 对接代码结构
十 一些对接技巧
一 背景
在平时工作中,经常会遇到的一种场景是:A公司要对接B公司的API方法,这时,A公司就要阅读B公司的接口文档,从接口文档中找到自己需要对接的API,并根据接口文档的要求,完成编码工作,最终完成对接工作。
本篇是站在A公司的角度,去对接B公司API接口的实战。
二 了解B公司接口的基础约定
一般情况下,B公司都会给出以下类似约定来满足基础对接,并且会提供测试环境和正式环境的两套信息。
appkey:A公司商户平台 id
appsecret:A公司商户平台 secret
三 基础域名
一般情况下,B公司会提供测试环境和生产环境两个基础域名。
例如
测试环境 : https://api-b-dev.com.cn
生产环境 : https://api-b-prod.com.cn
四 请求及相应格式说明
一般情况下,B公司会提供请求及相应的基础格式说明。
例如:
1 请求方式
post
2 请求消息格式
application/json
3 响应消息格式
application/json
4 请求公共参数
例如,B公司有以下要求
所有接口均需要以 Http Header 方式传递以下参数;
参数名 |
描述 |
必填 |
appkey |
商户平台 id |
是 |
request_id |
请求标识 ,每次请求唯一 |
是 |
sign_type |
签名方法,固定为 sha256 |
是 |
signature |
签名,算法为 HMACSHA256(appkey+timestamp+appsecret,appsecret) |
是 |
version |
版本, 固定为 2.0 |
是 |
callback_url |
回调地址, 以 https://或者 http://开头并进行 base64 编码 |
是(同步响应的接口可不必填写) |
timestamp |
时间戳(秒), 30 分钟过期 |
是 |
当然,不同的公司提供的参数各不相同,因公司而异。
5 响应/回调参数说明
例如:B公司所有API都有响应,并且有的API还有回调响应,不论是响应还是回调响应,它们的参数格式都一样。
参数 |
类型 |
描述 |
code |
int |
状态码 |
msg |
String |
消息 |
request_id |
String |
请求时的 request_id |
data |
Object |
数据 |
appkey |
String |
商户平台id |
6 针对异步回调的说明
例如:B公司对异步回调说明如下:
异步回调:
某些特定的接口需要异步返回结果,因此需商户A提供一个回调地址,将其进行base64 编码后,配置在 Http 请求 Header 中的 callback_url 里。
应答机制:
应答机制是指当商户A收到B公司数据通知时,必须回写 success 字符串,不区分大小写,B公司收到该“ success”,便认为商户A已收到通知; 否则会继续重复请求回调接口 3 次, 时间间隔为 1s, 5s, 30s。如果 4 次都访问不通,则会间隔 3h 继续轮询回调。
回调解密:
回调使用 aes 加密,需解密后使用。为避免由于网络波动造成回调失败,长时间未收到回调,请主动查询。
7 请求体加密说明及示例
数据采用AES加密,加密后作为data的值。
示例:
加密前:
{'settlement_code':['JS19BUB14F5D8D4C'],'random_code':['19BUB14F5D8D4C','19BUAD0E89D780']}
加密后:
{'data':'236agZcupcSsMZghtlmzhb7lEWzGZc3FO5GWQyrSB5kP/y1ESvd+CuBgQiWU/fwAICY/s0mideku/rXSKEb8In41F4SkUVLyLzYoYGed4QTjsqohTM0T6wmbkOiT1TH3'}
对 {'settlement_code':['JS19BUB14F5D8D4C'],'random_code':['19BUB14F5D8D4C','19BUAD0E89D780']} 进行AES加密,结果为:
236agZcupcSsMZghtlmzhb7lEWzGZc3FO5GWQyrSB5kP/y1ESvd+CuBgQiWU/fwAICY/s0mideku/rXSKEb8In41F4SkUVLyLzYoYGed4QTjsqohTM0T6wmbkOiT1TH3
8 回调解密说明以及示例
数据采用AES解密,解密data值部分,解密后是json字符串
解密前:
{
'code':0,'
'msg':'处理成功',
'request_id':'47fbb4ce-ae8e-4276-9d4a-4d279c9dfa78',
'data':'TI6H4Zx7YeWM0dSiial6L+nCvrEv8Oqk1ZFhWXqYZcRzzZyy/xECQW0nf
DszpNDmRMlSmsWkBJMmu4a/PmBivUBoNJwFBzAnOfn8gtYKdxDU16lDFwN5d/I
W1UJijJ2lU5YkDs/rMTyRN1NTR+0vJ1So0lmeZQiGQWEwE5t4wZykSC3cMQZyvJ95
2J7KU6aBXv1ZUGncZbWHQQaLw4UxFaBWIO8bVlkBIAqzolswI4dhtqBzFwmdEx+7
hzHSeidOVbIja5adgKMAjvIUTdtUEb/cO0ipO6QbK8wglk6dQ8+7rFTchBYIoaaqM9Sf
hcdvAYuSGk6yHIyN4GEtLBA5Zw==',
'appkey':'47fbb4ce-ae8e-4276-9d4a-4d279c9dfa78'
}
解密后:
{
'code':0,'
'msg':'处理成功',
'request_id':'47fbb4ce-ae8e-4276-9d4a-4d279c9dfa78',
'data':
{
'settlement_code':'JS19BR19A690E9F9',
'order_random_code':'09708757-7ea1-4fda',
'refund_merchant_amount':54736.84,
'refund_service_amount':263.16,
'change_code':'FW19BRAA9A200255',
'change_merchant_amount':4263.16,
'change_service_amount':236.84
},
'appkey':'47fbb4ce-ae8e-4276-9d4a-4d279c9dfa78'
}
9 一般公司B还会对code值进行说明。
五 确定要对接哪些API
一般情况下,公司B会针对某个项目提供必要的API,我们往往只需要对接少部分API接口,因此,首先确认要对接哪些API方法。我们只需要按照API的要求进行对接即可。
六 根据API文档,编写一些基础工具类。
工具类分两类。一类工具类,公司B会提供DEMO,我们拿来用即可。另外一类就需要自己根据API要求自己编写了。
1 公司B提供的基础工具类
例如,公司B提供了AES加解密以及签名的工具类
import org.apache.commons.codec.binary.Base64;
import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.Security;
/**
* AES加解密工具
*/
public class EncryptUtil {
private static final String CipherMode='AES/CBC/PKCS7Padding';
private static final String EncryptAlg ='AES';
private static final String Encode='UTF-8';
private static final String APPSECRET = '7da8046aa2da46bfb08429058e910081';
private static final String AESIV = 'ff465fdecc764337';
/**
* 加密:有向量16位,结果转base64
* @param context
* @return
*/
public static String encrypt(String context) {
try { //下面这行在进行PKCS7Padding加密时必须加上,否则报错
Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
byte[] content=context.getBytes(Encode);
Cipher cipher = Cipher.getInstance(CipherMode);
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(APPSECRET.getBytes(Encode), EncryptAlg), new IvParameterSpec(AESIV.getBytes(Encode)));
byte[] data = cipher.doFinal(content);
String result= Base64.encodeBase64String(data);
return result;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 解密
* @param context
* @return
*/
public static String decrypt(String context) {
try {
byte[] data=Base64.decodeBase64(context);
Cipher cipher = Cipher.getInstance(CipherMode);
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(APPSECRET.getBytes(Encode), EncryptAlg), new IvParameterSpec(AESIV.getBytes(Encode)));
byte[] content = cipher.doFinal(data);
String result=new String(content,Encode);
return result;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 生成 HMACSHA256
* @param data 待处理数据
* @param key 密钥
* @return 加密结果
* @throws Exception
*/
public static String HMACSHA256(String data, String key) throws Exception {
Mac sha256_HMAC = Mac.getInstance('HmacSHA256');
SecretKeySpec secret_key = new SecretKeySpec(key.getBytes('UTF-8'), 'HmacSHA256');
sha256_HMAC.init(secret_key);
byte[] array = sha256_HMAC.doFinal(data.getBytes('UTF-8'));
StringBuilder sb = new StringBuilder();
for (byte item : array) {
sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
}
return sb.toString();
}
public static void main(String[] args) {
//test256();
testEncrypt();
//testdecrypt();
}
private static void testdecrypt() {
String s1 = 'TI6H4Zx7YeWM0dSiial6L+nCvrEv8Oqk1ZFhWXqYZcRzzZyy/xECQW0nf' +
'DszpNDmRMlSmsWkBJMmu4a/PmBivUBoNJwFBzAnOfn8gtYKdxDU16lDFwN5d/I' +
'W1UJijJ2lU5YkDs/rMTyRN1NTR+0vJ1So0lmeZQiGQWEwE5t4wZykSC3cMQZyvJ95' +
'2J7KU6aBXv1ZUGncZbWHQQaLw4UxFaBWIO8bVlkBIAqzolswI4dhtqBzFwmdEx+7hzHSeid' +
'OVbIja5adgKMAjvIUTdtUEb/cO0ipO6QbK8wglk6dQ8+7rFTchBYIoaaqM9Sf' +
'hcdvAYuSGk6yHIyN4GEtLBA5Zw==';
System.out.println(decrypt(s1));
}
private static void testEncrypt() {
String s = '{\'name\':\'小明\',\'certificate_num\':\'451121196209260032\',\'certificate_type\':1,\'phone_num\':\'1388888888\',\'merchant_id\':\'c7c114d5da444df2b5d47a66c9c11111\'}';
String afterEncrypt = encrypt(s);
System.out.println(afterEncrypt);
}
private static void test256() {
// 签名算法
String s2 = 'c7c114d5da444df2b5d47a66c9cbd3fc16010271967da8046aa2da46bfb08429058e910081';
String key = '7da8046aa2da46bfb08429058e911111';
try {
String s1 = HMACSHA256(s2,key);
System.out.println(s1);
} catch (Exception e) {
e.printStackTrace();
}
}
}
2 根据API要求自己编写的工具类
例如:根据公司B的要求,编写特定的post方法
// 满足公司B的post方法
public static String sendPostByJsonWithHeader(String url, String body, Map<String, String> headers) throws Exception {
CloseableHttpClient httpclient = HttpClients.custom().build();
HttpPost post = null;
String resData = null;
CloseableHttpResponse result = null;
try {
// 封装 url,并且是 post 请求。
post = new HttpPost(url);
HttpEntity entity = new StringEntity(body, Consts.UTF_8);
// 基本配置
post.setConfig(RequestConfig.custom().setConnectTimeout(30000).setSocketTimeout(30000).build());
// 封装消息头
if (null != headers && !headers.isEmpty()) {
for (Entry<String, String> entry : headers.entrySet()) {
post.setHeader(entry.getKey(), entry.getValue());
}
}
// 消息头支持 json
post.setHeader('Content-Type', 'application/json');
// 封装数据
post.setEntity(entity);
// 发送请求
result = httpclient.execute(post);
if (HttpStatus.SC_OK == result.getStatusLine().getStatusCode()) {
// 返回结果
resData = EntityUtils.toString(result.getEntity());
}
} finally {
if (result != null) {
result.close();
}
if (post != null) {
post.releaseConnection();
}
httpclient.close();
}
return resData;
}
// 生成header的方法
private static Map<String, String> generateHeader(String callbackUrlParam) {
Map<String, String> headers = new HashMap<>();
// 商户结算平台 id,固定
headers.put('appkey', Constant.appkey);
// 请求标识 ,每次请求唯一,动态数据
String requestID = UUID.randomUUID().toString();
headers.put('request_id', requestID);
// 时间戳(秒),30 分钟过期,动态数据
Date date = new Date();
String timestamp = String.valueOf(date.getTime() / 1000);
headers.put('timestamp', timestamp);
// 签名方法 ,暂支持 sha256
headers.put('sign_type', 'sha256');
// 签名,算法为 HMACSHA256(appkey+timestamp+appsecret),动态生成
String signatureStr = Constant.appkey + timestamp + Constant.appsecret;
String signature = null;
try {
signature = EncryptUtil.HMACSHA256(signatureStr, Constant.appsecret);
} catch (Exception e) {
e.printStackTrace();
}
headers.put('signature', signature);
// 版本, 本文档为 2.0
headers.put('version', '2.0');
// 回调地址, 以 https://或者 http://开头并进行 base64 编码
String callbackUrl = callbackUrlParam;
if (callbackUrl != null) {
// 需要对callbackUrlParam进行base64 编码,然后赋值给 callbackUrl
Base64.Encoder encoder = Base64.getEncoder();
byte[] textByte;
try {
textByte = callbackUrlParam.getBytes('UTF-8');
callbackUrl = encoder.encodeToString(textByte);
headers.put('callback_url', callbackUrl);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
// 项目编号,暂不支持
headers.put('project_code', null);
return headers;
}
七 根据API文档,编写必要的DTO
针对每个API,主要包含请求DTO,响应DTO,回调响应DTO,这个就要跟踪API要求,编写满足要求的DTO。
当然有些DTO是可以抽象成一个类,例如一般响应DTO和回调响应DTO都是一样的,这个时候就可以抽象为一个DTO了。
例如:
package GDDto;
/**
* @className: GDCommonRes
* @description: 共同响应结果
* @date: 2020/9/24
* @author: cakin
*/
public class GDCommonRes {
/**
* 状态码
*/
private int code;
/**
* 消息
*/
private String msg;
/**
* 请求时的 request_id
*/
private String request_id;
/**
* 数据
*/
private String data;
/**
* appkey
*/
private String appkey;
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public String getRequest_id() {
return request_id;
}
public void setRequest_id(String request_id) {
this.request_id = request_id;
}
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
public String getAppkey() {
return appkey;
}
public void setAppkey(String appkey) {
this.appkey = appkey;
}
}
八 针对每个API方法,进行对接
做好以上准备工作,就可以一个个接口进行对接了。可采用下面方式一个一个接口进行对接。
public static void main(String[] args) {
// 1. 批量创建结算单 回调接口。
// batchSettlement();
// 2. 查询结算单 非回调接口。
//querySettlement();
// 3. 结算单退款 回调接口。
refundSettlement();
// 4. 授权签约 非回调接口。
// authorizSigned();
// 5. 获取签约结果 非回调接口。
// querySigned();
}
九 对接代码结构
十 一些对接技巧
1 遇到问题,如果需要公司B的帮助,需要主动和公司B的对接人员交流,尽快找到问题所在。
2 有些基础代码,如果公司B能提供,主动要一下,如果确实因为信息安全问题,公司B不方面提供,那就得自己写了,写完后,如果不确定代码是否符合B的要求,可以发给公司B的对接人员看看,以确定代码的正确性。
3 学习公司B的接口文档中好的地方,应用到自己的工作中。
4 委婉指出接口文档中的错误和不足,帮助公司B文档质量改进,这样在对接时,公司B的对接人员也会更热心的帮助你。
5 公司A的对接代码,放到正式代码的test目录中,一来可以方便调用正式代码中的工具类,二来方便将对接代码移植到正式代码中。