数字签名
翼辉支付与开发者通过相互验证签名来保证接口请求和返回数据的真实性和完整性。开发者在请求接口时需要对数据进行加签,接收到翼辉支付平台返回的信息时需要进行验签。本章主要介绍数字签名的生成和验证方法。
生成数字签名
翼辉支付相关接口使用 SHA-256 with RSA 签名,开发者需要在 爱智开发者平台 申请商户,并生成签名相关的密钥对,商户请求翼辉支付相关接口时使用商户私钥对请求参数进行签名,得到 sign(经过 Base64 编码)值,当商户接收到翼辉支付平台返回数据时,使用翼辉支付平台公钥进行签名校验,如果验签失败,则不能信任接收的数据。
过滤并排序
对所有的请求参数进行空值过滤(值为 null
),增加 mch_no
(开发者在爱智开发者平台申请商户的商户号)、nonce
(开发者生成的随机数,长度不大于 32 位)、sign_type
(计算签名的方法)字段及对应的值。
按照字段名称的字典序进行排序,对于嵌套的数据,嵌套内字段同样按照字典序排序。
序列化
将排好序的数据序列化为 json
格式,此时生成的 json
数据(字段之间不能包含换行符、空格)为待签名字符串。
调用签名方法
开发者使用开发语言对应的 SHA256WithRSA 签名方法以及商户私钥对待签名字符串进行签名,并对签名方法返回的值进行 Base64 编码作为签名值。
添加请求头
发起请求时需增加以下请求头(Headers):
- x-acopay-sign:调用签名方法得到的签名值。
- x-acopay-sign-type:计算签名的方法,
sign_type
参数的值。 - x-acopay-mch-no:计算签名时
mch_no
参数的值。 - x-acopay-nonce:计算签名时
nonce
参数的值。
示例
以查询订单接口为例,请求参数为:mch_trade_no。
组装签名参数
请求:https://api.edgeros.com/pay/v1/transactions/trade?mch_trade_no=mp202105122336582754046116407660
请求参数:mch_trade_no=mp202105122336582754046116407660
增加签名相关参数:
mch_no
(商户号):MCH1000000nonce
(随机数):26457860557876014765076037632383sign_type
(签名类型):SHA256withRSA
排序并序列化
排序并序列化得到待签名字符串:
{
"mch_no": "MCH1000000",
"mch_trade_no": "mp202105122336582754046116407660",
"nonce": "26457860557876014765076037632383",
"sign_type": "SHA256withRSA"
}
签名并发送请求
使用商户私钥对待签名字符串进行 SHA-256 with RSA 算法签名,得到签名值:
H1K7ewydLmJEvBLxS5iOZd3s7uQypuKeIHXe23ZaGcVwsqOtB2owBfZHsL3ZnKf26gS1peR5fwbPtIbJZ//hVKXj/F5IlwL8C0+1Q9v9d9F6h0ZhSJ96mHGK31mgm0lMaJFtD4pMd9FEl8ltpDr/WR8k/bEmoc+1kqWT+iZczunK0eHidk/SSgvCq9uyELE9kOycN2cTjFfRAAMSO92B13jAPoWrml+RJnmvmFvp4HzGrCXQmhssKGJTzVNT4LOzS6XSGQh9z9j5nsf1wiDWBtxgaFa0fUluDFiRftVWP5uaDuyRg5AZeKrmUMa/DHPucVaE83EbODGKNz3p3NBOIg==
最终发送的请求为:
curl -X GET `https://api.edgeros.com/pay/v1/transactions/trade?mch_trade_no=mp202105122336582754046116407660`
-H"x-acopay-nonce:26457860557876014765076037632383"
-H"x-acopay-sign-type:SHA256withRSA"
-H"x-acopay-mch-no:ea887713933c42a0bd7e657b06f279d3"
-H"x-acopay-sign:H1K7ewydLmJEvBLxS5iOZd3s7uQypuKeIHXe23ZaGcVwsqOtB2owBfZHsL3ZnKf26gS1peR5fwbPtIbJZ//hVKXj/F5IlwL8C0+1Q9v9d9F6h0ZhSJ96mHGK31mgm0lMaJFtD4pMd9FEl8ltpDr/WR8k/bEmoc+1kqWT+iZczunK0eHidk/SSgvCq9uyELE9kOycN2cTjFfRAAMSO92B13jAPoWrml+RJnmvmFvp4HzGrCXQmhssKGJTzVNT4LOzS6XSGQh9z9j5nsf1wiDWBtxgaFa0fUluDFiRftVWP5uaDuyRg5AZeKrmUMa/DHPucVaE83EbODGKNz3p3NBOIg=="
Java 代码示例
import com.google.common.collect.Maps;
import com.google.gson.Gson;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.*;
public class Demo {
// 商户私钥字符串
private final String YOUR_PRIVATE_KEY = "your_private_key";
// 查询订单号 URL
private final String QUERY_TRADE_URL = "https://api.edgeros.com/pay/v1/transactions/trade";
// 签名算法
private String sign_type = "SHA256withRSA";
/**
* 查询订单 demo 方法
*/
public void queryTrade(String transaction_no, String mch_trade_no) {
// 商户生成随机串
String nonce = "your nonce";
// 商户号
String mch_no = "your merchant number";
// 收集计算签名所需要的参数
HashMap<String, Object> signParamMap = new HashMap<>();
// 请求参数
signParamMap.put("transaction_no", transaction_no);
signParamMap.put("mch_trade_no", mch_trade_no);
// 增加签名相关参数
signParamMap.put("nonce", nonce);
signParamMap.put("mch_no", mch_no);
signParamMap.put("sign_type", sign_type);
try {
// 计算签名
String sign = getSign(signParamMap);
// 增加请求头
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.add("x-acopay-nonce", nonce);
headers.add("x-acopay-mch-no", mch_no);
headers.add("x-acopay-sign-type", sign_type);
headers.add("x-acopay-sign", sign);
HttpEntity<String> entity = new HttpEntity<>("", headers);
// 发送请求
ResponseEntity<Map> returnMap = restTemplate.exchange(QUERY_TRADE_URL +
"?mch_trade_no=" + mch_trade_no,
HttpMethod.GET, entity,
Map.class);
// 根据响应是否成功 进行验签、业务处理等操作
} catch (Exception e) {
// 进行异常处理
}
}
/**
* 获取签名值 demo 方法
*/
public String getSign(Map<String, Object> signParamMap) throws Exception {
// 过滤空值
Map<String, Object> filterMap = Maps.filterEntries(signParamMap, x -> !Objects.isNull(x.getValue()));
// 进行排序
TreeMap<String, Object> sortedMap = new TreeMap<>();
sortedMap.putAll(filterMap);
// 序列化为 json
String signJson = new Gson().toJson(sortedMap);
// 进行签名
String sign = sign(signJson.getBytes("UTF-8"));
return sign;
}
/**
* 计算签名 demo 方法
*/
private String sign(byte[] message) throws Exception{
// 把私钥字符串转换为 PrivateKey
byte[] buffer = Base64.getDecoder().decode(YOUR_PRIVATE_KEY);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(buffer);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
// 获取 PrivateKey
PrivateKey privateKey = keyFactory.generatePrivate(keySpec);
// 用换好的 PrivateKey 进行签名
Signature signature = Signature.getInstance(sign_type);
signature.initSign(privateKey);
signature.update(message);
return Base64.getEncoder().encodeToString(signature.sign());
}
}
Js 代码示例
const axios = require("axios");
const { KJUR, hextob64, b64tohex } = require("jsrsasign");
// 商户私钥字符串
const YOUR_PRIVATE_KEY = "your_private_key";
// 查询订单号 URL
const QUERY_TRADE_URL = "https://api.edgeros.com/pay/v1/transactions/trade";
// 签名算法
const sign_type = "SHA256withRSA";
const PEM_BEGIN = "-----BEGIN PRIVATE KEY-----\n";
const PEM_END = "\n-----END PRIVATE KEY-----";
const PUK_BEGIN = "-----BEGIN PUBLIC KEY-----\n";
const PUK_END = "\n-----END PUBLIC KEY-----";
class demo {
/**
* 查询订单方法
*/
async queryTrade(transaction_no, mch_trade_no) {
// 商户生成随机串
let nonce = "your nonce";
// 商户号
let mch_no = "your merchant number";
// 收集计算签名所需要的参数
let signParamMap = {
transaction_no: transaction_no, //edgeros订单号'null'
mch_trade_no: mch_trade_no, //商户订单号
nonce: nonce, //商户随机字符串
mch_no: mch_no, //商户号
sign_type: sign_type,
};
try {
// 计算签名
let sign = this.getSign(signParamMap);
let res = await axios({
url: QUERY_TRADE_URL + "?mch_trade_no=" + mch_trade_no,
method: "get",
headers: {
"x-acopay-nonce": nonce,
"x-acopay-mch-no": mch_no,
"x-acopay-sign-type": sign_type,
"x-acopay-sign": sign,
},
});
// 根据响应是否成功 进行验签、业务处理等操作
this.verifySign(res.data);
} catch (error) {
// 进行异常处理
console.log("请求报错", error);
}
}
/**
* 获取签名值 demo 方法
*/
getSign(signParamMap) {
// 过滤空值
Object.keys(signParamMap).forEach((key) => {
if (!signParamMap[key]) delete signParamMap[key];
});
// 进行排序
let newSignParamMap = {};
Object.keys(signParamMap)
.sort()
.map((key) => {
newSignParamMap[key] = signParamMap[key];
});
// 序列化为 json
let signJson = JSON.stringify(newSignParamMap);
// 进行签名
let signStr = this.sign(signJson);
return signStr;
}
/**
* 计算签名 demo 方法
*/
sign(message) {
let privateKey = this._formatKey(YOUR_PRIVATE_KEY, "private");
const signature = new KJUR.crypto.Signature({
alg: sign_type,
prvkeypem: privateKey,
});
signature.updateString(message);
let signData = signature.sign();
return hextob64(signData);
}
/**
* 添加密钥开始,结束符
* @param {*} key
* @returns
*/
_formatKey(key, type) {
if (type === "private") {
if (!key.startsWith(PEM_BEGIN)) {
key = PEM_BEGIN + key;
}
if (!key.endsWith(PEM_END)) {
key = key + PEM_END;
}
return key;
} else {
if (!key.startsWith(PUK_BEGIN)) {
key = PUK_BEGIN + key;
}
if (!key.endsWith(PUK_END)) {
key = key + PUK_END;
}
return key;
}
}
}
验证数字签名
当开发者接收到翼辉支付相关接口返回时,需进行签名校验,确保数据的的真实性和完整性。返回结果 status
为 200 时必须验签,否则判断返回结果是否包含签名值,再进行验签。
验签流程
- 对所有的接收参数进行过滤空值(值为
null
),然后去除data
字段里的sign
字段,字段之间不能包含换行符、空格。 - 对所有的接收参数字段名按照字典序排序。对于嵌套的数据,嵌套内数据同样按字典序进行排序,作为待验签字符串。
- 签名参数 sign 的值经过 base64 解码后即为响应签名值。
- 开发者使用开发语言对应的 SHA256WithRSA 验签方法,通过待验签字符串、响应签名值、及翼辉支付平台公钥验证签名。
示例
以查询订单接口返回为例。
获取验签参数
接收参数:
{
"status": 200,
"message": "SUCCESS",
"fieldErrors": null,
"data": {
"mch_no": "MCH1000000",
"sign_type": "SHA256withRSA",
"sign": "H1K7ewydLmJEvBLxS5iOZd3s7uQypuKeIHXe23ZaGcVwsqOtB2owBfZHsL3ZnKf26gS1peR5fwbPtIbJZ//hVKXj/F5IlwL8C0+1Q9v9d9F6h0ZhSJ96mHGK31mgm0lMaJFtD4pMd9FEl8ltpDr/WR8k/bEmoc+1kqWT+iZczunK0eHidk/SSgvCq9uyELE9kOycN2cTjFfRAAMSO92B13jAPoWrml+RJnmvmFvp4HzGrCXQmhssKGJTzVNT4LOzS6XSGQh9z9j5nsf1wiDWBtxgaFa0fUluDFiRftVWP5uaDuyRg5AZeKrmUMa/DHPucVaE83EbODGKNz3p3NBOIg==",
"nonce": "oGSKKu8GH2oHkGHDSRtDkmKnxhLkc5Yg",
"app_no": "APP1000000",
"mch_trade_no": "mp202105122336582754046116407660",
"transaction_no": "AP202112151150252267829842920145605378",
"code": "SUCCESS",
"description": "成功",
"extra": "返回附加信息",
"success_time": null,
"acoid": "20000",
"amount_total": "608.00"
}
}
获取签名参数 sign,经过 Base64 解码后即为响应签名值。
H1K7ewydLmJEvBLxS5iOZd3s7uQypuKeIHXe23ZaGcVwsqOtB2owBfZHsL3ZnKf26gS1peR5fwbPtIbJZ//hVKXj/F5IlwL8C0+1Q9v9d9F6h0ZhSJ96mHGK31mgm0lMaJFtD4pMd9FEl8ltpDr/WR8k/bEmoc+1kqWT+iZczunK0eHidk/SSgvCq9uyELE9kOycN2cTjFfRAAMSO92B13jAPoWrml+RJnmvmFvp4HzGrCXQmhssKGJTzVNT4LOzS6XSGQh9z9j5nsf1wiDWBtxgaFa0fUluDFiRftVWP5uaDuyRg5AZeKrmUMa/DHPucVaE83EbODGKNz3p3NBOIg==
过滤空值并排序,得到待验签字符串:
{
"data": {
"acoid": "20000",
"amount_total": "608.00",
"app_no": "APP1000000",
"code": "SUCCESS",
"describtion": "成功",
"extra": "返回附加信息",
"mch_no": "MCH1000000",
"mch_trade_no": "mp202105122336582754046116407660",
"nonce": "oGSKKu8GH2oHkGHDSRtDkmKnxhLkc5Yg",
"sign_type": "SHA256withRSA",
"transaction_no": "dc227a8867724c2bb6ac4b438a8c6a47"
},
"message": "SUCCESS",
"status": 200
}
签名验证
使用翼辉支付平台公钥对响应签名值和待验签字符串进行 SHA256WithRSA 签名验证,得到验签结果。
Java 代码示例
import com.google.common.collect.Maps;
import com.google.gson.Gson;
import org.apache.commons.lang3.StringUtils;
import java.io.UnsupportedEncodingException;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.*;
public class Demo {
// 翼辉支付平台公钥字符串
private final String ACOINFO_PAY_PUBLIC_KEY = "acoinfo_pay_public_key";
// 签名算法
private String sign_type = "SHA256withRSA";
/**
* 组装验签参数并验签
*/
public boolean verifySign(Map<String, Object> returnMap) throws Exception {
// 过滤空值
Map<String, Object> filterMap = Maps.filterEntries(returnMap, x -> !Objects.isNull(x.getValue()));
// 利用 TreeMap 进行自然排序
TreeMap<String, Object> sortedMap = new TreeMap<>();
sortedMap.putAll(filterMap);
// 获取业务信息
Map<String, Object> dataMap = (Map) filterMap.get("data");
// 签名值
String sign = "";
if (!Objects.isNull(dataMap)) {
// 取 data 字段中的签名值
sign = (String) dataMap.get("sign");
// 过滤空值和 sign 字段
Map<String, Object> filterDataMap = Maps.filterEntries(dataMap, x ->
(!StringUtils.equals(x.getKey(), "sign") && !Objects.isNull(x.getValue())));
// 业务数据进行排序
TreeMap<String, Object> dataSortedMap = new TreeMap<>();
dataSortedMap.putAll(filterDataMap);
// 排好后放入 sortedMap 替换原来的无序 data
sortedMap.put("data", dataSortedMap);
}
// 序列化为 json
String verifyJson = new Gson().toJson(sortedMap);
return verify(sign, verifyJson);
}
/**
* 验签方法
*/
private boolean verify(String sign, String verifyJson) throws NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException, SignatureException, UnsupportedEncodingException {
// 把翼辉支付平台公钥字符串转换为 PublicKey
byte[] buffer = Base64Helper.decode(ACOINFO_PAY_PUBLIC_KEY);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(buffer);
PublicKey publicKey = keyFactory.generatePublic(keySpec);
// 进行验签操作
Signature signature = Signature.getInstance(sign_type);
signature.initVerify(publicKey);
signature.update(verifyJson.getBytes("UTF-8"));
// 签名参数 sign 的值经过 base64 解码后验签
return signature.verify(Base64.getDecoder().decode(sign));
}
}
Js 代码示例
const { KJUR, hextob64, b64tohex } = require("jsrsasign");
//平台公钥
const ACOINFO_PAY_PUBLIC_KEY = "acoinfo_pay_public_key";
// 签名算法
const sign_type = "SHA256withRSA";
const PEM_BEGIN = "-----BEGIN PRIVATE KEY-----\n";
const PEM_END = "\n-----END PRIVATE KEY-----";
const PUK_BEGIN = "-----BEGIN PUBLIC KEY-----\n";
const PUK_END = "\n-----END PUBLIC KEY-----";
class demo {
/**
* 组装验签参数并验签
*/
verifySign(returnMap) {
// 过滤空值
Object.keys(returnMap).forEach((key) => {
if (!returnMap[key]) delete returnMap[key];
});
// 排序
let sortedMap = {};
Object.keys(returnMap)
.sort()
.map((key) => {
sortedMap[key] = returnMap[key];
});
// 获取业务信息
let dataMap = sortedMap.data;
// 签名值
let sign = "";
if (dataMap) {
// 取 data 字段中的签名值
sign = dataMap.sign;
// 过滤空值和 sign 字段
Object.keys(dataMap).forEach((key) => {
if (!dataMap[key] || key === "sign") delete dataMap[key];
});
// 业务数据进行排序
let dataSortedMap = {};
Object.keys(dataMap)
.sort()
.map((key) => {
dataSortedMap[key] = dataMap[key];
});
// 排好后放入 sortedMap 替换原来的无序 data
sortedMap.data = dataSortedMap;
}
// 序列化为 json
let verifyJson = JSON.stringify(sortedMap);
return this.verify(sign, verifyJson);
}
/**
* 验签方法
*/
verify(sign, verifyJson) {
// // 把翼辉支付平台公钥字符串转换为 PublicKey
let publicKey = this._formatKey(ACOINFO_PAY_PUBLIC_KEY, "public");
let signatureVf = new KJUR.crypto.Signature({
alg: sign_type,
prvkeypem: publicKey,
});
signatureVf.updateString(verifyJson);
let status = signatureVf.verify(b64tohex(sign));
console.log("jsrsasign verify: " + status);
return status;
}
/**
* 添加密钥开始,结束符
* @param {*} key
* @returns
*/
_formatKey(key, type) {
if (type === "private") {
if (!key.startsWith(PEM_BEGIN)) {
key = PEM_BEGIN + key;
}
if (!key.endsWith(PEM_END)) {
key = key + PEM_END;
}
return key;
} else {
if (!key.startsWith(PUK_BEGIN)) {
key = PUK_BEGIN + key;
}
if (!key.endsWith(PUK_END)) {
key = key + PUK_END;
}
return key;
}
}
}