认证与签名

用商户 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、POST
  • PATH:请求路径(不含域名与 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),
  ];
}

校验顺序(平台侧)

  1. Access Key 是否有效
  2. 是否开通开发者权限
  3. 时间戳是否在 ±5 分钟内
  4. Nonce 是否未使用
  5. 来源 IP 是否在白名单
  6. 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)。安装方式为直接拷贝源码,无强制包管理器发布版本;按语言依赖见各文件头注释。


Did this page help you?