CodePay RSA 签名认证机制

Online Payment (Hosted Checkout) 集成所需的认证体系详解

更新: 2026-02-07

一、三把密钥的分工

CodePay 的 RSA 签名认证涉及 3 把密钥,分属两对 RSA 密钥对:

密钥 谁生成 谁持有 用途
app_rsa_private_key 我方生成 我方保留(绝不外泄) 对发出的请求签名,证明"这个请求是我发的"
app_rsa_public_key 我方生成 上传到 CodePay 商户后台 CodePay 用它验证我方请求签名的合法性
gateway_rsa_public_key CodePay 生成 从 CodePay 商户后台下载 我方用它验证 CodePay Webhook/响应的签名
注意:CodePay 自己还有一把 gateway_rsa_private_key,但那是他们内部持有的,不会给我们。我们只拿到他们的公钥用于验签。

密钥对关系图

graph LR subgraph "我方 (Celoria)" A["app_rsa_private_key
(我方私钥 - 保密)"] B["app_rsa_public_key
(我方公钥)"] C["gateway_rsa_public_key
(CodePay 公钥)"] end subgraph "CodePay" D["app_rsa_public_key
(我方公钥 - 已上传)"] E["gateway_rsa_private_key
(CodePay 私钥 - 保密)"] F["gateway_rsa_public_key
(CodePay 公钥)"] end B -->|"上传"| D F -->|"下载"| C style A fill:#c62828,stroke:#b71c1c,color:#fff style E fill:#c62828,stroke:#b71c1c,color:#fff style B fill:#2e7d32,stroke:#1b5e20,color:#fff style D fill:#2e7d32,stroke:#1b5e20,color:#fff style C fill:#1565c0,stroke:#0d47a1,color:#fff style F fill:#1565c0,stroke:#0d47a1,color:#fff

二、我方发请求 → CodePay(创建支付订单)

sequenceDiagram participant B as Celoria 后端 participant C as CodePay Gateway B->>B: 1. 组装请求数据 B->>B: 2. 参数按 key 字母排序,拼成字符串 B->>B: 3. 用 app_rsa_private_key 签名 → sign B->>C: 4. POST /api/entry/checkout
{data + sign} C->>C: 5. 用 app_rsa_public_key 验证 sign alt 签名合法 C-->>B: 200 OK + checkout_url else 签名不合法 C-->>B: 401 Invalid Signature end

签名流程详解

1组装请求数据

{
  "merchant_no": "M123456",
  "amount": 5000,
  "currency": "USD",
  "order_id": "DEP_20260207_001",
  "notify_url": "https://api.celoria.ai/api/webhooks/codepay",
  "return_url": "https://celoria.ai/booking/success"
}

2参数排序拼接

把所有参数按 key 的字母顺序排列,用 & 拼接成字符串:

amount=5000¤cy=USD&merchant_no=M123456¬ify_url=https://api.celoria.ai/api/webhooks/codepay&order_id=DEP_20260207_001&return_url=https://celoria.ai/booking/success

3RSA 签名

app_rsa_private_key 对上面的字符串做 SHA256WithRSA 签名:

const crypto = require('crypto');

function signRequest(params, privateKey) {
  // 1. 过滤空值,按 key 排序
  const sorted = Object.keys(params)
    .filter(k => params[k] !== '' && params[k] !== null && params[k] !== undefined)
    .sort()
    .map(k => `${k}=${params[k]}`)
    .join('&');

  // 2. 用私钥签名 (SHA256WithRSA)
  const sign = crypto.createSign('RSA-SHA256');
  sign.update(sorted);
  return sign.sign(privateKey, 'base64');
}

4发送请求

把签名值放入 sign 字段,连同 sign_type: "RSA2" 一起发送:

{
  "merchant_no": "M123456",
  "amount": 5000,
  "currency": "USD",
  "order_id": "DEP_20260207_001",
  "notify_url": "https://api.celoria.ai/api/webhooks/codepay",
  "return_url": "https://celoria.ai/booking/success",
  "sign_type": "RSA2",
  "sign": "a3f8d2e1b9c7..."
}

三、CodePay → 我方(Webhook 回调通知)

sequenceDiagram participant G as Guest (App) participant CP as CodePay 支付页 participant CG as CodePay Gateway participant B as Celoria 后端 G->>CP: 在支付页输入卡号,完成支付 CP->>CG: 支付处理 CG->>CG: 用 gateway_rsa_private_key 签名结果 CG->>B: POST /api/webhooks/codepay
{payment_result + sign} B->>B: 用 gateway_rsa_public_key 验证 sign alt 签名合法 B->>B: 更新订单状态 → 已支付 B-->>CG: 200 OK {"received": true} else 签名不合法 B-->>CG: 401 拒绝(可能伪造) end CG->>G: 重定向到 return_url

验签代码

function verifyCodePaySignature(params, sign, gatewayPublicKey) {
  // 1. 取出 sign 和 sign_type,其余参数排序拼接
  const filtered = Object.keys(params)
    .filter(k => k !== 'sign' && k !== 'sign_type')
    .filter(k => params[k] !== '' && params[k] !== null)
    .sort()
    .map(k => `${k}=${params[k]}`)
    .join('&');

  // 2. 用 CodePay 公钥验证签名
  const verify = crypto.createVerify('RSA-SHA256');
  verify.update(filtered);
  return verify.verify(gatewayPublicKey, sign, 'base64');
}
安全要求:Webhook 验签失败时必须返回 401 并拒绝处理。绝不能跳过验签直接信任 Webhook 数据,否则攻击者可以伪造支付成功通知。

四、完整定金支付流程

sequenceDiagram participant G as Guest App participant B as Celoria 后端 participant CP as CodePay Note over G,CP: Checkout 完成,门店有定金政策 G->>B: POST /api/booking/checkout
{cart_items, guest_token} B->>B: 检查门店定金政策
计算定金金额 B->>CP: POST /api/entry/checkout
{amount, notify_url, return_url, sign} CP-->>B: {checkout_url} B-->>G: {checkout_url, deposit_amount, policy_summary} Note over G: 显示定金金额 + 退订规则 G->>G: WebView 打开 checkout_url G->>CP: 用户在 CodePay 页面输入卡号 CP->>CP: 扣款处理 CP->>B: Webhook: payment.completed
{order_id, trans_no, sign} B->>B: 验签 → 更新预约状态为已确认
记录定金交易 CP->>G: 重定向到 return_url G->>B: 查询预约状态 B-->>G: {status: confirmed, deposit_paid: true} Note over G: 显示预约成功页

五、环境变量配置

Online Payment 需要在 .env 中新增以下配置(与现有 Cloud Mode 配置并存):

# ===== CodePay Online Payment (Hosted Checkout) =====
CODEPAY_APPID=xxx                          # CodePay 分配的应用 ID
CODEPAY_APP_RSA_PRIVATE_KEY=xxx            # 我方 RSA 私钥 (PEM 格式,单行 base64)
CODEPAY_GATEWAY_RSA_PUBLIC_KEY=xxx         # CodePay 网关公钥 (PEM 格式)
CODEPAY_SIGN_TYPE=RSA2                     # 签名算法 (SHA256WithRSA)

# ===== 以下为已有配置(Cloud Mode 复用) =====
# CODEPAY_MERCHANT_NO=M123456             # 商户号(两种模式共用)
# CODEPAY_WEBHOOK_SECRET=xxx              # 仅 Cloud Mode HMAC 验签用
复用关系CODEPAY_MERCHANT_NO 两种模式共用同一个商户号。Webhook 端点 /api/webhooks/codepay 也可以复用,只需在处理逻辑中根据请求内容区分 Cloud Mode (HMAC) 和 Online Payment (RSA) 的验签方式。

六、密钥生成步骤

生成 RSA 2048 密钥对

# 1. 生成私钥
openssl genrsa -out app_rsa_private_key.pem 2048

# 2. 从私钥导出公钥
openssl rsa -in app_rsa_private_key.pem -pubout -out app_rsa_public_key.pem

# 3. 查看公钥内容(用于上传到 CodePay 商户后台)
cat app_rsa_public_key.pem

上传与下载

操作 去哪里 做什么
上传我方公钥 CodePay 商户后台 app_rsa_public_key.pem 的内容粘贴上去
下载 CodePay 公钥 CodePay 商户后台 复制 gateway_rsa_public_key 保存到本地
保存私钥 服务器 .env 文件 将私钥内容(去掉换行)存入 CODEPAY_APP_RSA_PRIVATE_KEY
私钥安全app_rsa_private_key.pem 绝不能提交到 Git、不能放在前端代码中、不能通过聊天/邮件传输。仅存放在服务器环境变量中。

七、与现有 Cloud Mode 的对比

对比项 Cloud Mode(店内刷卡) Online Payment(远程定金)
场景 员工在 Web 端发起,客人在 P5 终端刷卡 客人在 Guest App 里远程在线支付
认证方式 OAuth2 (Client ID + Secret) RSA 签名 (appid + RSA 密钥对)
支付界面 P5 终端上的 CodePay Register App CodePay 托管的 Web 支付页面 (WebView)
API 端点 POST /v1/orders (Cloud API) POST /api/entry/checkout (Hosted Checkout)
Webhook 验签 HMAC-SHA256 RSA-SHA256
商户号 共用 CODEPAY_MERCHANT_NO
Webhook 端点 共用 /api/webhooks/codepay(内部区分验签方式)

八、参考文档