认证与签名
用商户 RSA 私钥对每个请求加签,平台验签放行
认证与签名
CAFINX 开放接口对每个请求做 RSA 验签。你用商户私钥加签,平台用你上传的商户公钥验签。
Base URL
| 环境 | Base URL |
|---|---|
| 沙箱 | https://uat-openapi.cafinx.com/v1 |
| 生产 | https://openapi.cafinx.com/v1 |
请求完整 URL = Base URL + 资源路径(如 /ping、/cards)。签名 PATH 为网关视角路径(通常 /v1/...,不含 query)。
必带请求头
| 请求头 | 说明 |
|---|---|
X-Access-Key | 凭证 Access Key,以 ak_ 开头 |
X-Timestamp | 毫秒级时间戳,与服务器时间相差不超过 ±5 分钟 |
X-Nonce | 每次请求全局唯一的随机串(建议 UUID) |
X-Signature | 对签名串做 RSA-SHA256 后 Base64 编码的结果 |
写接口(POST)额外:
| 请求头 | 说明 |
|---|---|
X-Idempotency-Key | 幂等键,≤64 字符;必填。同键 24h 内重复请求回放首次响应;同键不同 body 返回 409(幂等冲突) |
签名串构造
按顺序用 \n(换行符)拼接 5 段:
签名串 = METHOD + "\n" + PATH + "\n" + timestamp + "\n" + nonce + "\n" + sha256Hex(body)
METHOD:HTTP 方法大写,如GET、POSTPATH:请求路径(不含域名与 query),与网关实际收到的 URI 一致(例如/v1/ping)timestamp/nonce:与同名请求头完全一致sha256Hex(body):请求体原文的 SHA-256 十六进制小写;GET 无 body 时对空串""取摘要
然后:
X-Signature = Base64( RSA-SHA256( 签名串, 商户私钥 ) )
关键:
sha256Hex(body)必须对你实际发送的那串字节求摘要。序列化后不要再改动空格或字段顺序。
空 body 的 SHA-256 固定为:
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
代码示例
Node.js
import crypto from 'crypto';
import fs from 'fs';
const privateKey = fs.readFileSync('client_private_pkcs8.pem', 'utf8');
function signRequest(method, path, body = '') {
const ts = String(Date.now());
const nonce = crypto.randomUUID();
const bodyHash = crypto.createHash('sha256').update(body, 'utf8').digest('hex');
const raw = [method.toUpperCase(), path, ts, nonce, bodyHash].join('\n');
const signature = crypto.sign('RSA-SHA256', Buffer.from(raw), privateKey).toString('base64');
return {
'X-Access-Key': process.env.CAFINX_AK,
'X-Timestamp': ts,
'X-Nonce': nonce,
'X-Signature': signature,
};
}Python
import base64, hashlib, time, uuid
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
with open("client_private_pkcs8.pem", "rb") as f:
private_key = serialization.load_pem_private_key(f.read(), password=None)
def sign_request(method: str, path: str, body: bytes = b""):
ts = str(int(time.time() * 1000))
nonce = str(uuid.uuid4())
body_hash = hashlib.sha256(body).hexdigest()
raw = f"{method.upper()}\n{path}\n{ts}\n{nonce}\n{body_hash}".encode()
sig = private_key.sign(raw, padding.PKCS1v15(), hashes.SHA256())
return {
"X-Access-Key": "ak_xxx",
"X-Timestamp": ts,
"X-Nonce": nonce,
"X-Signature": base64.b64encode(sig).decode(),
}Java
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
import java.util.UUID;
public class Sign {
static PrivateKey loadPkcs8(String pemPath) throws Exception {
String pem = Files.readString(Path.of(pemPath))
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s", "");
byte[] der = Base64.getDecoder().decode(pem);
return KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(der));
}
static String sha256Hex(byte[] body) throws Exception {
byte[] d = MessageDigest.getInstance("SHA-256").digest(body);
StringBuilder sb = new StringBuilder();
for (byte b : d) sb.append(String.format("%02x", b));
return sb.toString();
}
public static String[] headers(PrivateKey key, String ak, String method, String path, byte[] body)
throws Exception {
String ts = String.valueOf(System.currentTimeMillis());
String nonce = UUID.randomUUID().toString();
String raw = method.toUpperCase() + "\n" + path + "\n" + ts + "\n" + nonce + "\n" + sha256Hex(body);
Signature s = Signature.getInstance("SHA256withRSA");
s.initSign(key);
s.update(raw.getBytes(StandardCharsets.UTF_8));
String sig = Base64.getEncoder().encodeToString(s.sign());
return new String[]{ ak, ts, nonce, sig };
}
}C# (.NET 6+)
using System.Security.Cryptography;
using System.Text;
// 完整客户端:docs/developer-sdk/csharp/CafinxClient.cs
static (string ts, string nonce, string sig) Sign(
string privateKeyPem, string method, string path, string body = "")
{
using var rsa = RSA.Create();
rsa.ImportFromPem(privateKeyPem);
var signPath = path.Contains('?') ? path[..path.IndexOf('?')] : path;
var ts = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString();
var nonce = Guid.NewGuid().ToString("D");
var bodyHash = string.IsNullOrEmpty(body)
? "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
: Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(body))).ToLowerInvariant();
var raw = string.Join("\n", method.ToUpperInvariant(), signPath, ts, nonce, bodyHash);
var sig = Convert.ToBase64String(
rsa.SignData(Encoding.UTF8.GetBytes(raw), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1));
return (ts, nonce, sig);
}Go
package main
import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"encoding/pem"
"os"
"strconv"
"strings"
"time"
"github.com/google/uuid"
)
func loadPKCS8(path string) (*rsa.PrivateKey, error) {
b, err := os.ReadFile(path)
if err != nil { return nil, err }
block, _ := pem.Decode(b)
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil { return nil, err }
return key.(*rsa.PrivateKey), nil
}
func sign(priv *rsa.PrivateKey, method, path string, body []byte) (ts, nonce, sig string, err error) {
ts = strconv.FormatInt(time.Now().UnixMilli(), 10)
nonce = uuid.NewString()
sum := sha256.Sum256(body)
raw := strings.ToUpper(method) + "\n" + path + "\n" + ts + "\n" + nonce + "\n" + hex.EncodeToString(sum[:])
h := sha256.Sum256([]byte(raw))
signed, err := rsa.SignPKCS1v15(rand.Reader, priv, crypto.SHA256, h[:])
if err != nil { return }
sig = base64.StdEncoding.EncodeToString(signed)
return
}PHP
<?php
function cafinx_sign(string $pemPath, string $method, string $path, string $body = ''): array {
$key = openssl_pkey_get_private(file_get_contents($pemPath));
$ts = (string) round(microtime(true) * 1000);
$nonce = bin2hex(random_bytes(16));
$bodyHash = hash('sha256', $body);
$raw = strtoupper($method) . "\n{$path}\n{$ts}\n{$nonce}\n{$bodyHash}";
openssl_sign($raw, $signature, $key, OPENSSL_ALGO_SHA256);
return [
'X-Timestamp' => $ts,
'X-Nonce' => $nonce,
'X-Signature' => base64_encode($signature),
];
}校验顺序(平台侧)
- Access Key 是否有效
- 是否开通开发者权限
- 时间戳是否在 ±5 分钟内
- Nonce 是否未使用
- 来源 IP 是否在白名单
- RSA 验签是否通过
任一步失败返回对应错误码(见 错误码)。假密钥高频请求会触发 429(请求过于频繁)。
密钥生成(OpenSSL)
openssl genrsa -out client_private.pem 2048
openssl pkcs8 -topk8 -inform PEM -in client_private.pem -out client_private_pkcs8.pem -nocrypt
openssl rsa -in client_private.pem -pubout -out client_public.pem # 上传此公钥仓库内参考实现目录:docs/developer-sdk/(Java / Node / Python / PHP)。安装方式为直接拷贝源码,无强制包管理器发布版本;按语言依赖见各文件头注释。
Updated about 2 months ago
Did this page help you?
