Skip to content

API签名规范 ​

适用范围与角色 ​

  • 适用接口:Veloraya 厄瓜多尔地区的代收、代付、查询、余额、银行列表以及所有回调通知。
  • 目标:帮助商户快速完成签名接入、排查问题,并满足安全审计要求。
  • 关键角色:
    • 商户:负责发起 API 请求、生成签名并妥善保管私钥。
    • Veloraya:验证商户请求、向商户推送回调并使用平台私钥签名。

流程概览 ​

商户构造请求 ​

  1. 根据业务参数生成请求体 JSON(body)。
  2. 生成 timestamp、nonce,并按规范拼接签名原文。

商户生成签名并发起请求 ​

  1. 使用商户私钥 + SHA256withRSA 对签名原文进行签名并 Base64 编码,得到 Signature。
  2. 在 HTTP 头中携带 MerchantId / Timestamp / Nonce / Signature 和请求体 body 一并发送给 Veloraya。

Veloraya 验证请求并处理业务 ​

  1. 校验时间戳与 nonce 是否有效、签名是否正确。
  2. 验证通过后执行路由、风控、记账等业务逻辑,并返回同步响应。

Veloraya 发送回调通知 ​

  1. 在订单状态变更时,Veloraya 构造回调 body,生成 timestamp、nonce,使用平台私钥签名后发送给商户。
  2. 商户使用 Veloraya 公钥按回调验签流程校验回调数据合法性,并据此更新本地订单状态。

接入前提 ​

请求方式与网络 ​

  • 读写类接口默认使用 POST,余额查询、代付银行查询使用 GET。
  • 全量流量必须走 HTTPS,并与 Veloraya 约定出口 IP 以加入白名单。

必填请求头 ​

参数名必选类型说明
Content-TypeYstring固定为 application/json
MerchantIdYstringVeloraya 分配的商户 ID
TimestampYstring毫秒级时间戳,需与服务器保持较小误差
NonceYstring唯一随机串
SignatureYstring请求签名,Base64 编码的 RSA 结果

数据规范 ​

  • 请求体 body 必须与签名时完全一致(字段顺序、空格、转义字符都需保持)。
  • GET 请求若无 body,签名原文中的 body 置为 ""。

请求签名指南(商户侧) ​

签名原文格式 ​

merchantId={商户ID},timestamp={时间戳},nonce={随机数},body={请求体JSON字符串}

操作步骤 ​

  1. 构造原文:按上述格式拼接字符串。
  2. UTF-8 编码:将原文转为 UTF-8 字节数组。
  3. 生成签名:使用 SHA256withRSA 算法与商户私钥签名。
  4. 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;
}

回调验签指南 ​

  1. 从回调请求头读取 Signature、Timestamp、Nonce(回调中不包含 merchantId)。
  2. 构造原文 timestamp={...},nonce={...},body={请求体JSON}。
  3. 推荐直接复用附录 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);
        }
    }
}

Released under the MIT License.