认证
概述
Avista API 使用基于 OAuth 2.0 和 X.509 证书 (mTLS) 的认证方式。这种分层安全模型确保只有拥有有效证书的授权客户端才能访问 API。
为什么使用 mTLS?
mTLS(双向 TLS)与简单令牌相比提供更高级别的安全性:
- 双向认证:客户端和服务端互相验证身份
- 不可否认性:与账户关联的证书确保可追溯性
- 防止凭证被盗:即使 clientId/clientSecret 泄露,攻击者仍需要证书
前提条件
在开始之前,您需要:
通过 Avista 门户获取您的客户端证书。证书必须为 PEM 格式,并将关联到您的账户。
在管理面板中申请您的凭证(clientId 和 clientSecret)。
配置您的环境,以在 X-SSL-Client-Cert 请求头中发送证书。
X.509 证书必须在使用前关联到您的账户。未关联的证书即使技术上有效也会被拒绝。
认证端点
POST /api/auth/token
生成一个有效期为 30 分钟(1800 秒)的 JWT 访问令牌。
X.509 证书必须以 URL 编码 格式在 X-SSL-Client-Cert 请求头中发送。系统将验证证书的 SHA256 指纹与关联到账户的记录是否匹配。
请求
curl -X POST https://api.avista.global/api/auth/token \
-H "Content-Type: application/json" \
-H "X-SSL-Client-Cert: -----BEGIN%20CERTIFICATE-----%0AMIIB..." \
-d '{
"clientId": "account-93-550e8400",
"clientSecret": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"
}'响应 (201 Created)
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 1800
}实践示例:Node.js
安装
npm install axios完整代码
const axios = require('axios');
const fs = require('fs');
// Load X.509 certificate
const certificate = fs.readFileSync('./client-cert.pem', 'utf8');
const encodedCert = encodeURIComponent(certificate);
// Request configuration
const config = {
method: 'post',
url: 'https://api.avista.global/api/auth/token',
headers: {
'Content-Type': 'application/json',
'X-SSL-Client-Cert': encodedCert
},
data: {
clientId: process.env.AVISTA_CLIENT_ID,
clientSecret: process.env.AVISTA_CLIENT_SECRET
}
};
// Make request
async function getToken() {
try {
const response = await axios(config);
console.log('Token obtained successfully!');
console.log('Expires in:', response.data.expires_in, 'seconds');
return response.data.access_token;
} catch (error) {
console.error('Error obtaining token:', error.response?.data || error.message);
throw error;
}
}
getToken();实践示例:Python
安装
pip install requests完整代码
import os
import requests
import urllib.parse
# Load and encode certificate
with open('client-cert.pem', 'r') as f:
certificate = f.read()
encoded_cert = urllib.parse.quote(certificate)
# Request configuration
url = 'https://api.avista.global/api/auth/token'
headers = {
'Content-Type': 'application/json',
'X-SSL-Client-Cert': encoded_cert
}
payload = {
'clientId': os.environ.get('AVISTA_CLIENT_ID'),
'clientSecret': os.environ.get('AVISTA_CLIENT_SECRET')
}
# Make request
try:
response = requests.post(url, json=payload, headers=headers)
response.raise_for_status()
data = response.json()
print('Token obtained successfully!')
print(f"Expires in: {data['expires_in']} seconds")
except requests.exceptions.RequestException as e:
print(f'Error obtaining token: {e}')
if hasattr(e.response, 'text'):
print(f'Response: {e.response.text}')使用令牌
获取令牌后,在所有请求的 Authorization 请求头中包含它:
curl -X GET https://api.avista.global/api/balance \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."令牌续期
令牌在 30 分钟 后过期。请在您的应用程序中实现自动续期逻辑以避免中断。
推荐策略
class TokenManager {
constructor(clientId, clientSecret, certificatePath) {
this.clientId = clientId;
this.clientSecret = clientSecret;
this.certificatePath = certificatePath;
this.token = null;
this.expiresAt = null;
}
async getValidToken() {
// Check if the token is still valid (with a 30-second margin)
if (this.token && this.expiresAt && Date.now() < this.expiresAt - 30000) {
return this.token;
}
// Renew the token
return await this.refreshToken();
}
async refreshToken() {
const response = await this.requestNewToken();
this.token = response.access_token;
this.expiresAt = Date.now() + (response.expires_in * 1000);
return this.token;
}
async requestNewToken() {
const fs = require('fs');
const axios = require('axios');
const certificate = fs.readFileSync(this.certificatePath, 'utf8');
const encodedCert = encodeURIComponent(certificate);
const response = await axios.post('https://api.avista.global/api/auth/token', {
clientId: this.clientId,
clientSecret: this.clientSecret
}, {
headers: {
'Content-Type': 'application/json',
'X-SSL-Client-Cert': encodedCert
}
});
return response.data;
}
}
// Usage
const tokenManager = new TokenManager(
process.env.AVISTA_CLIENT_ID,
process.env.AVISTA_CLIENT_SECRET,
'./client-cert.pem'
);
// In any request
const token = await tokenManager.getValidToken();证书验证
系统对证书执行以下验证:
- 有效的 PEM 格式:证书必须为 PEM 格式并经过 URL 编码
- 账户关联:证书的 SHA256 指纹必须已注册并关联到您的账户
- 凭证匹配:证书必须属于与 OAuth 凭证相同的账户
未关联或关联到其他账户的证书将被拒绝,即使技术上有效。
错误响应
POST /api/auth/token 返回的所有错误均遵循相同的结构:
{
"statusCode": 400,
"timestamp": "2026-04-24T16:59:25.577Z",
"path": "/api/auth/token",
"method": "POST",
"code": "PUB_CERT_MALFORMED_PEM",
"message": "Certificate could not be parsed",
"userMessage": "The provided certificate is malformed.",
"details": {
"hint": "The PEM could not be parsed. Common cause: '+' characters in the base64 body must be URL-encoded as '%2B', not '%20'."
},
"errorId": "81421e7a5afbd9cf25212f51dac08da1"
}code:稳定的机器可读标识符 — 根据此字段进行逻辑分支。message:简短的技术描述(英文,面向日志)。userMessage:面向最终用户的描述(英文)。details.hint:面向集成 API 的开发者的可操作指引。errorId:唯一请求标识符 — 联系技术支持时请提供此值。
错误码参考
PUB_CERT_HEADER_MISSING (400)
请求中未包含 X-SSL-Client-Cert 请求头。
如何修复: 在请求头中附上经过 URL 编码的 PEM 证书。如果您位于执行 TLS 终止的网关(NGINX/ALB)之后,请确认网关已配置为将 $ssl_client_escaped_cert 作为 X-SSL-Client-Cert 转发。
PUB_CERT_MALFORMED_PEM (400)
证书在网关或上游服务中无法被解析。
如何修复: 最常见的原因是 URL 编码不正确 — base64 正文中自然出现的 + 字符必须编码为 %2B,而非 %20。某些编码器(例如 Go 的 url.QueryEscape 后跟简单的 + → %20 替换)会以这种方式破坏 base64。请在 Go 中使用 url.PathEscape,或验证您的编码器对字面量 + 生成 %2B。
PUB_REQUEST_BODY_INVALID (400)
JSON 请求体不符合规范(字段缺失、类型错误,或 clientId 不是 UUID v4)。请查看 details.violations 以确定有问题的字段。
PUB_CERT_NOT_YET_VALID (401)
证书的 notBefore 日期在未来。
如何修复: 通过 NTP 同步系统时钟。如果该日期确实在未来,请等待或申请新证书。
PUB_CERT_EXPIRED (401)
证书的 notAfter 日期已过期。
如何修复: 向技术支持申请新证书并在您的集成中进行替换。
PUB_CERT_NOT_REGISTERED (401)
证书的 SHA-256 指纹不在注册表中 — 该证书从未注册或已被吊销。
如何修复: 使用 openssl x509 -noout -fingerprint -sha256 -in cert.pem 计算本地指纹,并与入驻时收到的指纹进行比对。如果匹配,请联系技术支持 — 证书可能已被吊销。如果不匹配,请联系技术支持以注册新证书。
注意: 已吊销的证书当前会以
PUB_CERT_NOT_REGISTERED上报。未来版本可能引入专用的PUB_CERT_REVOKED错误码;在此之前,本错误码同时覆盖两种情况,由支持团队进一步区分。
PUB_CERT_NOT_AUTHORIZED_FOR_ACCOUNT (403)
证书有效且已注册,但未关联到拥有所提供 clientId 的账户。
如何修复: 请确认您在此环境中使用的是正确的证书/clientId 组合。
PUB_INVALID_CREDENTIALS (401)
clientId 未被识别,或 clientSecret 不匹配。平台特意不区分这两种情况(防止枚举攻击)。
如何修复: 验证 clientId 和 clientSecret。如果问题持续存在,请携带响应中的 errorId 联系技术支持。
PUB_AUTH_UPSTREAM_UNAVAILABLE (503)
上游认证服务暂时不可访问。
如何修复: 使用指数退避重试(例如 1s、2s、4s)。如果问题持续超过一分钟,请联系技术支持。
PUB_AUTH_UPSTREAM_ERROR (502)
上游返回了无法识别的错误。
如何修复: 携带 errorId 联系技术支持 — 此错误码表示服务端问题,我们将进行排查。
迁移说明(针对已有集成)
如果您的集成硬编码检查了旧的 PUB_INVALID_CERTIFICATE 或 PUB_TOKEN_GENERATION_FAILED 错误码,它们已被重命名:
| 旧错误码 | 新错误码 |
|---|---|
PUB_INVALID_CERTIFICATE | PUB_CERT_MALFORMED_PEM(仅限 PEM 解析失败 — 其他证书问题现有专用错误码) |
PUB_TOKEN_GENERATION_FAILED | PUB_AUTH_UPSTREAM_UNAVAILABLE (503) 用于瞬时不可用,PUB_AUTH_UPSTREAM_ERROR (502) 用于无法识别的上游错误 |
PUB_INVALID_CREDENTIALS 保留,但含义更为精确 — 现在仅在 clientId 或 clientSecret 错误时触发,不再作为认证服务任何 401 响应的兜底错误码。
故障排查
"我的证书在 openssl 下正常但 API 返回 PUB_CERT_MALFORMED_PEM"
磁盘上的 PEM 文件没有问题 — 问题出在传输过程中。最常见的原因是 + → %20 URL 编码错误。请打印客户端发送的请求头的确切值,对其进行解码,并与原始 PEM 逐字节比对。base64 中的每个字面量 + 都应保留为 %2B。
"我偶尔会收到 PUB_CERT_NOT_YET_VALID"
系统时钟已偏移超过证书的 notBefore 时间。请配置 NTP 并确保主机时钟与 UTC 相差在秒级以内。容器可能会继承宿主机的时钟偏移 — 请在容器内使用 date -u 进行验证。
"轮换证书后我收到 PUB_CERT_NOT_REGISTERED"
请计算新证书的指纹并确认其已完成注册:openssl x509 -noout -fingerprint -sha256 -in new-cert.pem。如果指纹与我们记录的不符,请在切换前联系技术支持进行注册。