API签名规范
适用范围与角色
- 适用接口:Veloraya 墨西哥地区的代收、代付、查询、余额、银行列表以及所有回调通知。
- 目标:帮助商户快速完成签名接入、排查问题,并满足安全审计要求。
- 关键角色:
- 商户:负责发起 API 请求、生成签名并妥善保管私钥。
- Veloraya:验证商户请求、向商户推送回调并使用平台私钥签名。
流程概览
商户构造请求
- 根据业务参数生成请求体 JSON(body)。
- 生成 timestamp、nonce,并按规范拼接签名原文。
商户生成签名并发起请求
- 使用商户私钥 + SHA256withRSA 对签名原文进行签名并 Base64 编码,得到
Signature。 - 在 HTTP 头中携带
MerchantId / Timestamp / Nonce / Signature和请求体 body 一并发送给 Veloraya。
Veloraya 验证请求并处理业务
- 校验时间戳与 nonce 是否有效、签名是否正确。
- 验证通过后执行路由、风控、记账等业务逻辑,并返回同步响应。
Veloraya 发送回调通知
- 在订单状态变更时,Veloraya 构造回调 body,生成 timestamp、nonce,使用平台私钥签名后发送给商户。
- 商户使用 Veloraya 公钥按回调验签流程校验回调数据合法性,并据此更新本地订单状态。
接入前提
请求方式与网络
- 读写类接口默认使用 POST,余额查询、代付银行查询使用 GET。
- 全量流量必须走 HTTPS,并与 Veloraya 约定出口 IP 以加入白名单。
必填请求头
| 参数名 | 必选 | 类型 | 说明 |
|---|---|---|---|
| Content-Type | Y | string | 固定为 application/json |
| MerchantId | Y | string | Veloraya 分配的商户 ID |
| Timestamp | Y | string | 毫秒级时间戳,需与服务器保持较小误差 |
| Nonce | Y | string | 唯一随机串 |
| Signature | Y | string | 请求签名,Base64 编码的 RSA 结果 |
数据规范
- 请求体 body 必须与签名时完全一致(字段顺序、空格、转义字符都需保持)。
- GET 请求若无 body,签名原文中的 body 置为
""。
请求签名指南(商户侧)
签名原文格式
merchantId={商户ID},timestamp={时间戳},nonce={随机数},body={请求体JSON字符串}操作步骤
- 构造原文:按上述格式拼接字符串。
- UTF-8 编码:将原文转为 UTF-8 字节数组。
- 生成签名:使用 SHA256withRSA 算法与商户私钥签名。
- Base64 编码:将签名字节转成 Base64 字符串,作为
Signature请求头。
POST 请求签名说明
POST 请求(如代收、代付、查询等接口)包含请求体 JSON,签名时将完整的 JSON 字符串作为 body 参数。
重要说明
- 请求方请求体 JSON 中的字段没有顺序要求,可以按任意顺序排列字段。
- 请求方需要保证签名时使用的 JSON 字符串与实际的请求报文体顺序完全一致。
- 服务方会原样获取请求 body 进行签名验证(不会改变请求对象的字段顺序),以此保证和客户端一致的顺序进行签名校验。
POST 请求签名示例
请求示例:
POST /api/payin HTTP/1.1
Content-Type: application/json
MerchantId: 101111
Timestamp: 1703123456789
Nonce: ITqRczTuacKWhOuppvpVIsBXfHhkVXcs
Signature: xxxxxx...
{
"amount": 500,
"currency": "THB",
"merchantOrderId": "ORDER5187977827671717",
"payinMethod": "PromptPay QR",
"payerEmail": "user@example.com",
"payerIdType": "passport",
"payerIdCard": "650100201901155112",
"payerMobile": "05468548186",
"payerName": "Apiratee Rodkaew",
"payToken": "",
"expireSeconds": 3600,
"redirectUrl": "https://www.xxx.com"
}签名原文:
merchantId=101111,timestamp=1703123456789,nonce=ITqRczTuacKWhOuppvpVIsBXfHhkVXcs,
body={"amount": 500,"currency": "THB","merchantOrderId": "ORDER5187977827671717","payinMethod": "PromptPay QR","payerEmail": "user@example.com","payerIdType": "passport","payerIdCard": "650100201901155112","payerMobile": "05468548186","payerName": "Apiratee Rodkaew","payToken": "","expireSeconds": 3600,"redirectUrl": "https://www.xxx.com"}GET 请求签名说明
GET 请求(如余额查询、代付银行查询)没有请求体,但为了保持签名格式的一致性,签名原文中必须包含 body= 部分(值为空字符串)。
设计原则:
- 格式统一:POST 和 GET 请求使用相同的签名原文格式,便于统一验证逻辑,降低实现复杂度。
- 明确性:明确包含
body=避免歧义,防止因格式不一致导致的签名验证失败。
GET 请求签名示例
请求示例:
GET /api/balance?currency=THB HTTP/1.1
Content-Type: application/json
MerchantId: 101111
Timestamp: 1703123456789
Nonce: sVEGRQHPAonwRnDHQhtAJmKoqRhNMRUn
Signature: xxxxxx...签名原文:
merchantId=101111,timestamp=1703123456789,nonce=abc123def456,body=统一代码示例
以下代码示例同时支持 POST 和 GET 请求签名:
java
/**
* 生成 API 请求签名(支持 POST 和 GET)
* @param merchantId 商户 ID
* @param bodyJson 请求体 JSON 字符串,GET 请求传入空字符串 ""
* @param merchantPrivateKeyBase64 Base64 编码的商户私钥
* @return Base64 编码的签名
*/
public String signRequest(String merchantId, String bodyJson,
String merchantPrivateKeyBase64) throws Exception {
// Step 1: 生成时间戳与 nonce
String timestamp = String.valueOf(System.currentTimeMillis());
String nonce = UUID.randomUUID().toString().replace("-", "");
// Step 2: 调用辅助方法生成签名
// 注意:GET 请求时 bodyJson 应为空字符串 ""
String signBase64 = VelorayaSignUtils.generateApiSign(
merchantId, timestamp, nonce, bodyJson, merchantPrivateKeyBase64
);
// Step 3: 将 MerchantId/timestamp/nonce/Signature 写入请求头后发送
return signBase64;
}回调验签指南
- 从回调请求头读取
Signature、Timestamp、Nonce(回调中不包含 merchantId)。 - 构造原文
timestamp={...},nonce={...},body={请求体JSON}。 - 推荐直接复用附录 B 中的
VelorayaSignUtils.verifyCallbackSign(...)方法进行验签,该方法已实现上述步骤和 SHA256withRSA 验签逻辑。
安全与风控要求
- 时间同步:保持服务器与 Veloraya 严格同步,漂移过大请求将被拒绝。
- 防重放:Nonce 必须唯一,建议在业务层做短期去重缓存。
- 通信安全:强制 HTTPS,必要出口 IP 需列入白名单。
常见问题排查
| 场景 | 现象 | 排查建议 |
|---|---|---|
| 签名校验失败 | 返回 code=500 | 检查签名原文、时间戳、私钥/公钥是否匹配 |
| 时间戳过期 | 返回 code=500 | 同步服务器时间后重试 |
| Nonce 重复 | 返回 code=500 | 保证随机串唯一,可在缓存中做短期去重 |
| 回调验签失败 | 返回 code=500 | 确认使用最新 Veloraya 公钥,并检查回调 body 是否被修改 |
附录
附录 A:密钥生成与管理
生成与存储
使用 2048 位 RSA 密钥,公钥使用 X.509 (DER) → Base64,私钥使用 PKCS#8 (DER) → Base64。
工具类:输出 Base64 公私钥
java
import java.security.*;
import java.util.Base64;
public class RsaGenerateKeyPair {
public static void main(String[] args) throws NoSuchAlgorithmException {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
KeyPair keyPair = keyPairGenerator.genKeyPair();
PublicKey publicKey = keyPair.getPublic();
byte[] publicKeyBytes = publicKey.getEncoded();
String publicKeyBase64 = Base64.getEncoder().encodeToString(publicKeyBytes);
System.out.println(publicKeyBase64);
PrivateKey privateKey = keyPair.getPrivate();
byte[] privateKeyBytes = privateKey.getEncoded();
String privateKeyBase64 = Base64.getEncoder().encodeToString(privateKeyBytes);
System.out.println(privateKeyBase64);
}
}分发与轮换
- 仅将公钥(Base64)提供给 Veloraya,私钥自持。
- 建议 6~12 个月轮换一次,确保新旧公钥在 Veloraya 侧并行一段迁移期。
附录 B:签名/验签辅助代码
java
public class VelorayaSignUtils {
/**
* 生成 API 请求签名
*/
public static String generateApiSign(String merchantId, String timestamp,
String nonce, String body,
String merchantPrivateKeyBase64) {
try {
// 1. 按规范拼接签名原文
String content = String.format("merchantId=%s,timestamp=%s,nonce=%s,body=%s",
merchantId, timestamp, nonce, body);
byte[] contentBytes = content.getBytes(StandardCharsets.UTF_8);
// 2. 解析 Base64 编码的商户私钥
byte[] keyBytes = Base64.getDecoder().decode(merchantPrivateKeyBase64);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
PrivateKey privateKey = KeyFactory.getInstance("RSA").generatePrivate(keySpec);
// 3. 使用 SHA256withRSA 算法签名
Signature sig = Signature.getInstance("SHA256withRSA");
sig.initSign(privateKey);
sig.update(contentBytes);
return Base64.getEncoder().encodeToString(sig.sign());
} catch (Exception e) {
throw new RuntimeException("API请求签名生成失败", e);
}
}
/**
* 验证回调签名
*/
public static boolean verifyCallbackSign(String receivedSign, String timestamp,
String nonce, String body,
String velorayaPublicKeyBase64) {
try {
// 1. 按回调规范拼接验签原文
String content = String.format("timestamp=%s,nonce=%s,body=%s",
timestamp, nonce, body);
byte[] contentBytes = content.getBytes(StandardCharsets.UTF_8);
// 2. 解析 Base64 编码的 Veloraya 公钥
byte[] keyBytes = Base64.getDecoder().decode(velorayaPublicKeyBase64);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
PublicKey publicKey = KeyFactory.getInstance("RSA").generatePublic(keySpec);
// 3. 解码签名并使用公钥验签
byte[] signature = Base64.getDecoder().decode(receivedSign);
Signature sig = Signature.getInstance("SHA256withRSA");
sig.initVerify(publicKey);
sig.update(contentBytes);
return sig.verify(signature);
} catch (Exception e) {
throw new RuntimeException("回调通知验签失败", e);
}
}
}