认证
概述
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"
}'| 字段 | 类型 | 必填 | 描述 |
|---|---|---|---|
clientId | string (UUID v4) | 是 | 在控制面板中生成的凭证标识符 |
clientSecret | string | 是 | 凭证密钥,在控制面板中仅显示一次 |
响应 (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();证书验证
系统会对握手中出示的证书执行以下验证:
- mTLS 握手:证书必须由 FyPay 签发,且您必须持有对应的私钥
- 有效期:证书必须处于有效期内(自签发起 2 年)
- 注册:证书的 SHA-256 指纹必须已注册且处于有效状态
- 账户关联:证书必须关联到与凭证(
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 | 错误码 | 含义 |
|---|---|---|
| 400 | PUB_CERT_HEADER_MISSING | TLS 握手中未出示客户端证书 |
| 400 | PUB_CERT_MALFORMED_PEM | 证书的 PEM 格式无效 |
| 400 | PUB_REQUEST_BODY_INVALID | JSON 请求体无效 |
| 401 | PUB_CERT_NOT_VERIFIED | 握手中出示的证书被拒绝 |
| 401 | PUB_CERT_NOT_YET_VALID | 证书的生效日期在未来 |
| 401 | PUB_CERT_EXPIRED | 证书已过期 |
| 401 | PUB_CERT_NOT_REGISTERED | 证书未注册或已被吊销 |
| 401 | PUB_INVALID_CREDENTIALS | clientId 或 clientSecret 错误 |
| 403 | PUB_CERT_NOT_AUTHORIZED_FOR_ACCOUNT | 证书未关联到该 clientId 所属账户 |
| 403 | PUB_ACCOUNT_INACTIVE | 凭证正确,但账户未处于激活状态 |
| 502 | PUB_AUTH_UPSTREAM_ERROR | 认证服务出现意外错误 |
| 503 | PUB_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),并在切换前与技术支持确认该证书已注册并关联到您的账户。