开放API接口安全设计文档
[toc]
一. 背景介绍
开放平台的API是基于HTTP协议来调用的,系统需要验证商户上送的签名是否正确;商户收到应答,也需要验证签名是否正确,如果商户未正确验证签名,存在潜在的风险,商户自行承担因此而产生的所有损失。以下主要是针对自行封装HTTP请求进行API调用的原理进行详细解说。
根据接口调用协议:填充参数 > 生成签名 > 拼装HTTP请求 > 发起HTTP请求> 验证签名,数据解密 > 得到HTTP响应 > 加签加密返回
二. 调用地址
调用环境 | 服务地址(HTTP) | 服务地址(HTTPS) |
---|---|---|
测试环境 | ||
正式环境 |
三. 接口安全
3.1 公共参数
- 调用任何一个API都必须传入的参数,目前支持的公共参数有:
参数名称 | 参数类型 | 是否必须 | 参数描述 |
---|---|---|---|
encoding | String | 是 | 编码方式,默认UTF-8(大写)。 |
appId | String | 是 | 分配给应用的AppId |
appSecret | String | 是 | 业务接口body或响应数据加密aes秘钥(AES长度为256,工作方式为 CBC) |
timestamp | String | 是 | 时间戳,格式为yyyy-MM-dd HH:mm:ss,时区为GMT+8,例如:2021-12-01 12:00:00。 |
version | String | 是 | API协议版本,当前传参:1.0。 |
signMethod | String | 是 | 签名的摘要算法,当前使用MD5。 |
body | String | 是 | 业务接口请求参数密文(对原始json字符串加密并Base64编码) |
sign | String | 是 | 具体的签名串,针对摘要digest使用私钥签名 |
3.2 报文的签名机制
为了防止API调用过程中被恶意篡改,调用任何一个API都需要携带签名,开放平台服务端会根据请求参数,对签名进行验证,签名不合法的请求将会被拒绝。系统目前支持的签名摘要算法有MD5(signMethod=md5),对于报文的签名加密处理机制如下:
1. 首先,针对请求body使用生成的appSecret进行AES加密,并对加密报文进行Base64编码(Base64.encodeBase64String(
AESUtils.encrypt(appSecret, body).getBytes(StandardCharsets.UTF_8)));
2. 然后,对加密body的AES key使用**对方的公钥**进行加密,并作为接口请求的appSecret;
3. 其次,对使用AES加密后的body使用 MD5 算法做摘要(DigestUtils.md5Hex(concatStr).toLowerCase()),再使用**自己的RSA签名私钥**对摘要做签名操作;
4. 最后,将签名串放在签名(sign)字段和其他字段一起通过HTTP Post的方式传输给开放平台。
3.3 报文的验签机制
对于报文的验签解密处理机制如下:
1. 首先,获取接口请求的appSecret,并使用自己的私钥进行解密得到原始的AES秘钥;
2. 然后,对使用AES加密后的body使用 MD5 算法做摘要(DigestUtils.md5Hex(concatStr).toLowerCase();
3. 其次,使用对方的公钥解密出接口请求的签名(sign),将解密出摘要digest与步骤二计算的摘要比对签名结果;
4. 最后,对body的密文首先进行Base64解码,然后使用解密出的AES解密body报文得到原始的请求Json报文。
四. 代码参考
4.1 AES对称加密
package com.xxx.xxx.xxx;
import java.nio.charset.StandardCharsets;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.RandomStringUtils;
public class AESUtils {
private static final String KEY_ALGORITHM = "AES";
private static final Integer CBC_SECRET_KEY_LENGTH = 128;
private static final Integer ECB_SECRET_KEY_LENGTH = 256;
private static final String AES_CBC = "AES/CBC/PKCS5Padding";
private static final String AES_ECB = "AES/ECB/PKCS5Padding";
public AESUtils() {
}
public static String generateKey(int length) {
return RandomStringUtils.randomAlphanumeric(length);
}
public static String encrypt(String key, String iv, String content) throws Exception {
KeyGenerator ken = KeyGenerator.getInstance("AES");
ken.init(CBC_SECRET_KEY_LENGTH);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
IvParameterSpec ivp = new IvParameterSpec(iv.getBytes());
cipher.init(1, new SecretKeySpec(key.getBytes(), "AES"), ivp);
return Base64.encodeBase64String(cipher.doFinal(content.getBytes(StandardCharsets.UTF_8)));
}
public static String encrypt(String key, String content) throws Exception {
KeyGenerator ken = KeyGenerator.getInstance("AES");
ken.init(ECB_SECRET_KEY_LENGTH);
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(1, new SecretKeySpec(key.getBytes(), "AES"));
return Base64.encodeBase64String(cipher.doFinal(content.getBytes(StandardCharsets.UTF_8)));
}
public static String decrypt(String key, String iv, String content) throws Exception {
KeyGenerator ken = KeyGenerator.getInstance("AES");
ken.init(CBC_SECRET_KEY_LENGTH);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
IvParameterSpec ivp = new IvParameterSpec(iv.getBytes());
cipher.init(2, new SecretKeySpec(key.getBytes(), "AES"), ivp);
byte[] result = cipher.doFinal(Base64.decodeBase64(content));
return new String(result, StandardCharsets.UTF_8);
}
public static String decrypt(String key, String content) throws Exception {
KeyGenerator ken = KeyGenerator.getInstance("AES");
ken.init(ECB_SECRET_KEY_LENGTH);
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(2, new SecretKeySpec(key.getBytes(), "AES"));
byte[] result = cipher.doFinal(Base64.decodeBase64(content));
return new String(result, StandardCharsets.UTF_8);
}
}
4.2 RSA非对称加密
package com.xxx.xxx.xxx;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import javax.crypto.Cipher;
import org.apache.commons.codec.binary.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class RSAUtils {
private static final Logger log = LoggerFactory.getLogger(RSAUtils.class);
public RSAUtils() {
}
public static String encryptByPublicKey(String publicKey, String text) throws Exception {
X509EncodedKeySpec x509EncodedKeySpec2 = new X509EncodedKeySpec(Base64.decodeBase64(publicKey));
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey key = keyFactory.generatePublic(x509EncodedKeySpec2);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(1, key);
byte[] result = cipher.doFinal(text.getBytes());
return Base64.encodeBase64String(result);
}
public static String decryptByPrivateKey(String privateKey, String text) throws Exception {
PKCS8EncodedKeySpec pkcs8EncodedKeySpec5 = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKey));
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PrivateKey key = keyFactory.generatePrivate(pkcs8EncodedKeySpec5);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(2, key);
byte[] result = cipher.doFinal(Base64.decodeBase64(text));
return new String(result);
}
public static String encryptByPrivateKey(String privateKey, String text) throws Exception {
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKey));
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PrivateKey key = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(1, key);
byte[] result = cipher.doFinal(text.getBytes());
return Base64.encodeBase64String(result);
}
public static String decryptByPublicKey(String publicKey, String text) throws Exception {
X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(Base64.decodeBase64(publicKey));
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey key = keyFactory.generatePublic(x509EncodedKeySpec);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(2, key);
byte[] result = cipher.doFinal(Base64.decodeBase64(text));
return new String(result);
}
public static RSAKeyPair generateKeyPair() throws Exception {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
RSAPublicKey rsaPublicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate();
String publicKeyString = Base64.encodeBase64String(rsaPublicKey.getEncoded());
String privateKeyString = Base64.encodeBase64String(rsaPrivateKey.getEncoded());
return new RSAKeyPair(publicKeyString, privateKeyString);
}
public static class RSAKeyPair {
private final String publicKey;
private final String privateKey;
public RSAKeyPair(String publicKey, String privateKey) {
this.publicKey = publicKey;
this.privateKey = privateKey;
}
public String getPublicKey() {
return this.publicKey;
}
public String getPrivateKey() {
return this.privateKey;
}
}
}
五. 商户接口请求示例
5.1 加密加签示例
package com.xxx.xxx.xxx;
import com.google.gson.JsonObject;
import java.nio.charset.StandardCharsets;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.digest.DigestUtils;
import org.junit.Test;
public class EncryptAndDecryptTest {
public static final String appId = "wshoto_supplier";
public static String appSecret;
public static final String encoding = "UTF-8";
public static final String signMethod = "MD5";
public static final String version = "1.0";
// 商户平台RSA秘钥对
public static RSAKeyPair merchantRsaKeyPair;
// 开放平台RSA秘钥对
public static RSAKeyPair platFormRsaKeyPair;
static {
appSecret = AESUtils.generateKey(32);
try {
merchantRsaKeyPair = RSAUtils.generateKeyPair();
platFormRsaKeyPair = RSAUtils.generateKeyPair();
} catch (Exception e) {
e.printStackTrace();
}
}
private JsonObject encryptRequest(String body) throws Exception {
// 1.对body加密(AES 256 CBC)
String encodedBody = Base64.encodeBase64String(
AESUtils.encrypt(appSecret, body).getBytes(StandardCharsets.UTF_8));
System.out.println("encryptRequest01 encoded body: " + encodedBody);
// 2.拼接报文返回
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("appId", appId);
// => 商户侧与平台侧主要体现在公私钥的使用上!(外部商户公钥加密会话AES秘钥)
String aesKey = RSAUtils.encryptByPublicKey(platFormRsaKeyPair.getPublicKey(), appSecret);
jsonObject.addProperty("appSecret", aesKey);
jsonObject.addProperty("body", encodedBody);
jsonObject.addProperty("encoding", encoding);
jsonObject.addProperty("signMethod", signMethod);
jsonObject.addProperty("version", version);
// 3.摘要生成
String digest = DigestUtils.md5Hex(encodedBody).toLowerCase();
System.out.println("encryptRequest01 digest: " + digest);
// 4.签名生成(对摘要私钥加签) // => 商户侧与平台侧主要体现在公私钥的使用上!(外部商户私钥加密会话摘要Digest)
String signStr = RSAUtils.encryptByPrivateKey(merchantRsaKeyPair.getPrivateKey(), digest);
System.out.println("encryptRequest01 signStr: " + signStr);
// 5.签名拼接
jsonObject.addProperty("sign", signStr);
return jsonObject;
}
@Test
public void testEncryptRequest() throws Exception {
String body = "{\n"
+ " \"supplier_id\": \"olGAfxdH\",\n"
+ " \"asset_type\": \"4ru7XS\",\n"
+ " \"risk_level\": \"71\",\n"
+ " \"page_size\": 47,\n"
+ " \"page_num\": 4,\n"
+ " \"fund_type\": \"8kZRTFC\",\n"
+ " \"fuzzy_name\": \"654\"\n"
+ "}";
System.out.println(this.encryptRequest(body));
}
}
5.2 验签解密示例
package com.xxx.xxx.xxx;
import com.google.gson.JsonObject;
import java.nio.charset.StandardCharsets;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.digest.DigestUtils;
import org.junit.Test;
public class EncryptAndDecryptTest {
public static final String appId = "wshoto_supplier";
public static String appSecret;
public static final String encoding = "UTF-8";
public static final String signMethod = "MD5";
public static final String version = "1.0";
// 商户平台RSA秘钥对
public static RSAKeyPair merchantRsaKeyPair;
// 开放平台RSA秘钥对
public static RSAKeyPair platFormRsaKeyPair;
static {
appSecret = AESUtils.generateKey(32);
try {
merchantRsaKeyPair = RSAUtils.generateKeyPair();
platFormRsaKeyPair = RSAUtils.generateKeyPair();
} catch (Exception e) {
e.printStackTrace();
}
}
public String decryptRequest(JsonObject jsonObject) throws Exception {
// 0.平台私钥解密出aes秘钥
String appSecretEncoded = jsonObject.getAsJsonPrimitive("appSecret").getAsString();
// => 商户侧与平台侧主要体现在公私钥的使用上!(外部商户公钥解密会话AES秘钥)
String aesKey = RSAUtils.decryptByPrivateKey(merchantRsaKeyPair.getPrivateKey(), appSecretEncoded);
// 1.获取签名信息
String sign = jsonObject.getAsJsonPrimitive("sign").getAsString();
System.out.println("decryptRequest01 sign: " + sign);
// 2.签名验证 (先公钥解签, 然后再生成摘要比对)
String digest = DigestUtils.md5Hex(jsonObject.getAsJsonPrimitive("body").getAsString()).toLowerCase();
// => 商户侧与平台侧主要体现在公私钥的使用上!(开放平台公钥解密会话摘要Digest)
String reqDigest = RSAUtils.decryptByPublicKey(platFormRsaKeyPair.getPublicKey(), sign);
System.out.println("digest check: " + reqDigest.equals(digest));
// 3.body解密 (先Base64解码, 再AES解密, 与加密方向相反)
String encodedBody = jsonObject.getAsJsonPrimitive("body").getAsString();
String body = new String(Base64.decodeBase64(encodedBody), StandardCharsets.UTF_8);
return AESUtils.decrypt(aesKey, body);
}
@Test
public void testDecryptRequest() throws Exception {
String body = "{\n"
+ " \"supplier_id\": \"olGAfxdH\",\n"
+ " \"asset_type\": \"4ru7XS\",\n"
+ " \"risk_level\": \"71\",\n"
+ " \"page_size\": 47,\n"
+ " \"page_num\": 4,\n"
+ " \"fund_type\": \"8kZRTFC\",\n"
+ " \"fuzzy_name\": \"654\"\n"
+ "}";
JsonObject res = this.encryptRequest(body);
System.out.println("encrypt res: " + res);
System.out.println();
String plainText = this.decryptRequest(res);
System.out.println("plainText: " + plainText);
}
}
六. 开放平台接口请求示例
6.1 加密加签示例
package com.xxx.xxx.xxx;
import com.google.gson.JsonObject;
import java.nio.charset.StandardCharsets;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.digest.DigestUtils;
import org.junit.Test;
public class EncryptAndDecryptTest {
public static final String appId = "wshoto_supplier";
public static String appSecret;
public static final String encoding = "UTF-8";
public static final String signMethod = "MD5";
public static final String version = "1.0";
// 商户平台RSA秘钥对
public static RSAKeyPair merchantRsaKeyPair;
// 开放平台RSA秘钥对
public static RSAKeyPair platFormRsaKeyPair;
static {
appSecret = AESUtils.generateKey(32);
try {
merchantRsaKeyPair = RSAUtils.generateKeyPair();
platFormRsaKeyPair = RSAUtils.generateKeyPair();
} catch (Exception e) {
e.printStackTrace();
}
}
private JsonObject encryptRequest(String body) throws Exception {
// 1.对body加密(AES 256 CBC)
String encodedBody = Base64.encodeBase64String(
AESUtils.encrypt(appSecret, body).getBytes(StandardCharsets.UTF_8));
System.out.println("encryptRequest01 encoded body: " + encodedBody);
// 2.拼接报文返回
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("appId", appId);
// => 商户侧与平台侧主要体现在公私钥的使用上!(外部商户公钥加密会话AES秘钥)
String aesKey = RSAUtils.encryptByPublicKey(merchantRsaKeyPair.getPublicKey(), appSecret);
jsonObject.addProperty("appSecret", aesKey);
jsonObject.addProperty("body", encodedBody);
jsonObject.addProperty("encoding", encoding);
jsonObject.addProperty("signMethod", signMethod);
jsonObject.addProperty("version", version);
// 3.摘要生成
String digest = DigestUtils.md5Hex(encodedBody).toLowerCase();
System.out.println("encryptRequest01 digest: " + digest);
// 4.签名生成(对摘要私钥加签) // => 商户侧与平台侧主要体现在公私钥的使用上!(开放平台私钥加密会话摘要Digest)
String signStr = RSAUtils.encryptByPrivateKey(platFormRsaKeyPair.getPrivateKey(), digest);
System.out.println("encryptRequest01 signStr: " + signStr);
// 5.签名拼接
jsonObject.addProperty("sign", signStr);
return jsonObject;
}
}
6.2 验签解密示例
package com.xxx.xxx.xxx;
import com.google.gson.JsonObject;
import java.nio.charset.StandardCharsets;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.digest.DigestUtils;
import org.junit.Test;
public class EncryptAndDecryptTest {
public static final String appId = "wshoto_supplier";
public static String appSecret;
public static final String encoding = "UTF-8";
public static final String signMethod = "MD5";
public static final String version = "1.0";
// 商户平台RSA秘钥对
public static RSAKeyPair merchantRsaKeyPair;
// 开放平台RSA秘钥对
public static RSAKeyPair platFormRsaKeyPair;
static {
appSecret = AESUtils.generateKey(32);
try {
merchantRsaKeyPair = RSAUtils.generateKeyPair();
platFormRsaKeyPair = RSAUtils.generateKeyPair();
} catch (Exception e) {
e.printStackTrace();
}
}
public String decryptRequest(JsonObject jsonObject) throws Exception {
// 0.平台私钥解密出aes秘钥
String appSecretEncoded = jsonObject.getAsJsonPrimitive("appSecret").getAsString();
// => 商户侧与平台侧主要体现在公私钥的使用上!(开放平台私钥解密会话AES秘钥)
String aesKey = RSAUtils.decryptByPrivateKey(platFormRsaKeyPair.getPrivateKey(), appSecretEncoded);
// 1.获取签名信息
String sign = jsonObject.getAsJsonPrimitive("sign").getAsString();
System.out.println("decryptRequest01 sign: " + sign);
// 2.签名验证 (先公钥解签, 然后再生成摘要比对)
String digest = DigestUtils.md5Hex(jsonObject.getAsJsonPrimitive("body").getAsString()).toLowerCase();
// => 商户侧与平台侧主要体现在公私钥的使用上!(外部商户公钥解密会话摘要Digest)
String reqDigest = RSAUtils.decryptByPublicKey(merchantRsaKeyPair.getPublicKey(), sign);
System.out.println("digest check: " + reqDigest.equals(digest));
// 3.body解密 (先Base64解码, 再AES解密, 与加密方向相反)
String encodedBody = jsonObject.getAsJsonPrimitive("body").getAsString();
String body = new String(Base64.decodeBase64(encodedBody), StandardCharsets.UTF_8);
return AESUtils.decrypt(aesKey, body);
}
}
Q.E.D.