docs
集成指南

认证

概述

FyPay API 使用基于 OAuth 2.0(clientId + clientSecret)并结合 X.509 客户端证书(mTLS) 的认证方式。证书在令牌生成请求的 TLS 握手 中出示;其他所有路由只使用 Bearer 令牌。

为什么使用 mTLS?

与单纯的凭证相比,mTLS(双向 TLS)提供更高的安全性:

  • 双向认证:客户端和服务器在 TLS 握手中相互认证
  • 不可否认性:与账户关联的证书确保可追溯性
  • 防止凭证被盗:即使 clientId/clientSecret 泄露,攻击者仍需要证书及其私钥

前提条件

开始之前,您需要:

PEM 格式、RSA 2048 密钥、有效期 2 年 且 已关联到您账户 的证书,以及对应的私钥。以下示例中,文件名为 client.crt(证书)和 client.key(私钥)。

在 FyPay 控制面板 的 设置 → 凭证 中生成您的 clientId 和 clientSecret。clientSecret 仅显示一次:请复制并保存到密钥库中。

配置您的 HTTP 客户端,在 TLS 握手中出示证书和私钥(使用您所用语言的原生 TLS 机制)。

证书必须 已关联到您的账户。未关联或关联到其他账户的证书即使在技术上有效也会被拒绝。

证书仅以 PEM 格式提供,不提供 .pfx 文件。如果您的技术栈需要 PKCS#12(例如某些 Windows/.NET 场景),请在本地转换:

openssl pkcs12 -export -in client.crt -inkey client.key -out client.pfx

认证端点

POST /api/auth/token

生成 JWT 访问令牌。令牌的有效期(秒)由响应中的 expires_in 字段给出。

这是 唯一 需要客户端证书的路由。证书必须在 TLS 握手中出示(mTLS),而不是通过 HTTP 请求头发送。

请求

curl -X POST https://api.fypay.me/api/auth/token \
  --cert client.crt \
  --key client.key \
  -H "Content-Type: application/json" \
  -d '{
    "clientId": "6967c0a0-3566-48d0-8c13-717038b52279",
    "clientSecret": "your-client-secret"
  }'
字段类型必填描述
clientIdstring (UUID v4)是在控制面板中生成的凭证标识符
clientSecretstring是凭证密钥,在控制面板中仅显示一次

响应 (201 Created)

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 600,
  "scope": "email profile"
}
字段描述
access_token通过 Authorization: Bearer <token> 发送的 JWT 令牌
token_type始终为 Bearer
expires_in令牌有效期(秒)。请据此安排续期
scope令牌作用域

实践示例:Node.js

安装

npm install undici

完整代码

const fs = require('fs');
const { Agent, request } = require('undici');

const mtlsAgent = new Agent({
  connect: {
    cert: fs.readFileSync('./client.crt'),
    key: fs.readFileSync('./client.key'),
  },
});

async function getToken() {
  const { statusCode, body } = await request('https://api.fypay.me/api/auth/token', {
    method: 'POST',
    dispatcher: mtlsAgent,
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      clientId: process.env.FYPAY_CLIENT_ID,
      clientSecret: process.env.FYPAY_CLIENT_SECRET,
    }),
  });

  const data = await body.json();

  if (statusCode !== 201) {
    throw new Error(`获取令牌失败: ${data.code} ${data.message}`);
  }

  console.log('令牌获取成功!');
  console.log('有效期:', data.expires_in, '秒');

  return data;
}

getToken().catch(console.error);

使用原生 https 模块时,等效写法是在请求的 agent 选项中传入 new https.Agent({ cert, key })。

实践示例:Python

安装

pip install requests

完整代码

import os
import requests

url = 'https://api.fypay.me/api/auth/token'
payload = {
    'clientId': os.environ.get('FYPAY_CLIENT_ID'),
    'clientSecret': os.environ.get('FYPAY_CLIENT_SECRET'),
}

try:
    response = requests.post(
        url,
        json=payload,
        cert=('client.crt', 'client.key'),
        timeout=30,
    )
    response.raise_for_status()

    data = response.json()

    print('令牌获取成功!')
    print(f"有效期: {data['expires_in']} 秒")

except requests.exceptions.RequestException as e:
    print(f'获取令牌失败: {e}')
    if e.response is not None:
        print(f'响应: {e.response.text}')

其他语言

在所有语言中,证书和私钥都通过 HTTP 客户端的原生 TLS 机制加载:

语言机制
PHP (cURL)CURLOPT_SSLCERT = client.crt,CURLOPT_SSLKEY = client.key
C# (.NET)HttpClientHandler.ClientCertificates.Add(X509Certificate2.CreateFromPemFile("client.crt", "client.key"))
Go在 tls.Config.Certificates 中使用 tls.LoadX509KeyPair("client.crt", "client.key")
Java使用 openssl pkcs12 -export 转换为 PKCS#12,并作为 KeyStore 加载到 SSLContext 中

使用令牌

获取令牌后,在其他所有请求的 Authorization 请求头中携带它。这些路由 不 需要客户端证书:

curl -X GET https://api.fypay.me/api/balance \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

令牌续期

不要假设令牌有固定的有效期。请在每次响应中读取 expires_in,并在令牌过期前续期。

推荐策略

const fs = require('fs');
const { Agent, request } = require('undici');

class TokenManager {
  constructor(clientId, clientSecret, certPath, keyPath) {
    this.clientId = clientId;
    this.clientSecret = clientSecret;
    this.agent = new Agent({
      connect: {
        cert: fs.readFileSync(certPath),
        key: fs.readFileSync(keyPath),
      },
    });
    this.token = null;
    this.expiresAt = 0;
  }

  async getValidToken() {
    const safetyMarginMs = 30000;
    if (this.token && Date.now() < this.expiresAt - safetyMarginMs) {
      return this.token;
    }
    return this.refreshToken();
  }

  async refreshToken() {
    const { statusCode, body } = await request('https://api.fypay.me/api/auth/token', {
      method: 'POST',
      dispatcher: this.agent,
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        clientId: this.clientId,
        clientSecret: this.clientSecret,
      }),
    });

    const data = await body.json();
    if (statusCode !== 201) {
      throw new Error(`${data.code}: ${data.message}`);
    }

    this.token = data.access_token;
    this.expiresAt = Date.now() + data.expires_in * 1000;
    return this.token;
  }
}

const tokenManager = new TokenManager(
  process.env.FYPAY_CLIENT_ID,
  process.env.FYPAY_CLIENT_SECRET,
  './client.crt',
  './client.key'
);

const token = await tokenManager.getValidToken();

证书验证

系统会对握手中出示的证书执行以下验证:

  1. mTLS 握手:证书必须由 FyPay 签发,且您必须持有对应的私钥
  2. 有效期:证书必须处于有效期内(自签发起 2 年)
  3. 注册:证书的 SHA-256 指纹必须已注册且处于有效状态
  4. 账户关联:证书必须关联到与凭证(clientId)相同的账户

未关联或关联到其他账户的证书将被拒绝,即使在技术上有效。

错误响应

POST /api/auth/token 返回的所有错误都遵循相同格式:

{
  "statusCode": 400,
  "timestamp": "2026-04-24T16:59:25.577Z",
  "path": "/api/auth/token",
  "method": "POST",
  "code": "PUB_CERT_HEADER_MISSING",
  "message": "Client certificate header is missing",
  "userMessage": "Client certificate is required.",
  "details": {
    "hint": "Present your client certificate during the TLS handshake (e.g. curl --cert client.crt --key client.key)."
  },
  "errorId": "81421e7a5afbd9cf25212f51dac08da1"
}
  • code:稳定的机器可读标识符。请基于此字段进行处理。
  • message:简短的技术描述(英文,用于日志)。
  • userMessage:面向最终用户的描述。
  • details.hint:为集成 API 的开发者提供的实用指导。
  • errorId:请求的唯一标识符。联系技术支持时请提供。

错误码参考

HTTP错误码含义
400PUB_CERT_HEADER_MISSINGTLS 握手中未出示客户端证书
400PUB_CERT_MALFORMED_PEM证书的 PEM 格式无效
400PUB_REQUEST_BODY_INVALIDJSON 请求体无效
401PUB_CERT_NOT_VERIFIED握手中出示的证书被拒绝
401PUB_CERT_NOT_YET_VALID证书的生效日期在未来
401PUB_CERT_EXPIRED证书已过期
401PUB_CERT_NOT_REGISTERED证书未注册或已被吊销
401PUB_INVALID_CREDENTIALSclientId 或 clientSecret 错误
403PUB_CERT_NOT_AUTHORIZED_FOR_ACCOUNT证书未关联到该 clientId 所属账户
403PUB_ACCOUNT_INACTIVE凭证正确,但账户未处于激活状态
502PUB_AUTH_UPSTREAM_ERROR认证服务出现意外错误
503PUB_AUTH_UPSTREAM_UNAVAILABLE认证服务暂时不可用

PUB_CERT_HEADER_MISSING (400)

TLS 握手中未出示客户端证书。

解决方法: 配置您的 HTTP 客户端,在 TLS 连接中发送证书和私钥(例如 curl --cert client.crt --key client.key)。如果您的应用与 FyPay 之间存在终止 TLS 的代理或网关,则到 api.fypay.me 的连接必须由出示证书的客户端发起。

PUB_CERT_MALFORMED_PEM (400)

证书的 PEM 格式无效。

PUB_REQUEST_BODY_INVALID (400)

JSON 请求体不符合规范(缺少字段、类型错误,或 clientId 不是 UUID v4)。请查看 details.violations 以定位有问题的字段,并发送 Content-Type: application/json。

PUB_CERT_NOT_VERIFIED (401)

TLS 握手中出示的证书被拒绝。

解决方法: 使用 FyPay 为您的账户签发的证书及其对应的私钥。不接受其他证书颁发机构签发的证书。

PUB_CERT_NOT_YET_VALID (401)

证书的 notBefore 日期在未来。

解决方法: 通过 NTP 同步系统时钟。如果日期确实在未来,请等待或申请新证书。

PUB_CERT_EXPIRED (401)

证书的 notAfter 日期已过。证书自签发起有效期为 2 年。

解决方法: 向技术支持申请新证书并在集成中替换。请在到期前规划更换。

PUB_CERT_NOT_REGISTERED (401)

证书的 SHA-256 指纹不在注册表中:该证书从未注册或已被吊销。

解决方法: 使用 openssl x509 -noout -fingerprint -sha256 -in client.crt 计算本地指纹,并连同响应中的 errorId 一起提供给技术支持。

PUB_INVALID_CREDENTIALS (401)

clientId 无法识别,或 clientSecret 不匹配。平台有意不区分这两种情况(防止枚举)。

解决方法: 检查 clientId 和 clientSecret。由于 clientSecret 仅显示一次,如果丢失,请在 设置 → 凭证 中生成新的凭证。

PUB_CERT_NOT_AUTHORIZED_FOR_ACCOUNT (403)

证书有效且已注册,但未关联到所提供 clientId 所属的账户。

解决方法: 确认您使用的是同一账户的证书/clientId 组合。

PUB_ACCOUNT_INACTIVE (403)

凭证正确,但账户未处于激活状态。重试不会成功。

解决方法: 携带响应中的 errorId 联系技术支持。

PUB_AUTH_UPSTREAM_UNAVAILABLE (503)

认证服务暂时无法访问。

解决方法: 使用指数退避重试(例如 1 秒、2 秒、4 秒)。如果问题持续超过一分钟,请联系技术支持。

PUB_AUTH_UPSTREAM_ERROR (502)

认证服务返回了意外错误。

解决方法: 携带 errorId 联系技术支持。

故障排查

"我已经发送了证书,但仍然收到 PUB_CERT_HEADER_MISSING"

证书必须在 TLS 握手中出示,而不是放在 HTTP 请求头中。请确认 HTTP 客户端已配置证书和私钥(例如 curl 中的 --cert/--key,Python requests 中的 cert=(crt, key)),并确认没有中间代理在到达 FyPay 之前终止 TLS。

"偶尔收到 PUB_CERT_NOT_YET_VALID"

系统时钟落后于证书的 notBefore 日期。请配置 NTP,确保主机时钟与 UTC 的偏差在几秒以内。容器可能继承主机的时钟漂移,可在容器内使用 date -u 检查。

"更换证书后收到 PUB_CERT_NOT_REGISTERED"

计算新证书的指纹(openssl x509 -noout -fingerprint -sha256 -in client.crt),并在切换前与技术支持确认该证书已注册并关联到您的账户。

最佳实践

后续步骤

本页目录