Avistadocs
集成指南

认证

概述

Avista API 使用基于 OAuth 2.0X.509 证书 (mTLS) 的认证方式。这种分层安全模型确保只有拥有有效证书的授权客户端才能访问 API。

为什么使用 mTLS?

mTLS(双向 TLS)与简单令牌相比提供更高级别的安全性:

  • 双向认证:客户端和服务端互相验证身份
  • 不可否认性:与账户关联的证书确保可追溯性
  • 防止凭证被盗:即使 clientId/clientSecret 泄露,攻击者仍需要证书

前提条件

在开始之前,您需要:

通过 Avista 门户获取您的客户端证书。证书必须为 PEM 格式,并将关联到您的账户

在管理面板中申请您的凭证(clientIdclientSecret)。

配置您的环境,以在 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();

证书验证

系统对证书执行以下验证:

  1. 有效的 PEM 格式:证书必须为 PEM 格式并经过 URL 编码
  2. 账户关联:证书的 SHA256 指纹必须已注册并关联到您的账户
  3. 凭证匹配:证书必须属于与 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 不匹配。平台特意不区分这两种情况(防止枚举攻击)。

如何修复: 验证 clientIdclientSecret。如果问题持续存在,请携带响应中的 errorId 联系技术支持。

PUB_AUTH_UPSTREAM_UNAVAILABLE (503)

上游认证服务暂时不可访问。

如何修复: 使用指数退避重试(例如 1s、2s、4s)。如果问题持续超过一分钟,请联系技术支持。

PUB_AUTH_UPSTREAM_ERROR (502)

上游返回了无法识别的错误。

如何修复: 携带 errorId 联系技术支持 — 此错误码表示服务端问题,我们将进行排查。

迁移说明(针对已有集成)

如果您的集成硬编码检查了旧的 PUB_INVALID_CERTIFICATEPUB_TOKEN_GENERATION_FAILED 错误码,它们已被重命名:

旧错误码新错误码
PUB_INVALID_CERTIFICATEPUB_CERT_MALFORMED_PEM(仅限 PEM 解析失败 — 其他证书问题现有专用错误码)
PUB_TOKEN_GENERATION_FAILEDPUB_AUTH_UPSTREAM_UNAVAILABLE (503) 用于瞬时不可用,PUB_AUTH_UPSTREAM_ERROR (502) 用于无法识别的上游错误

PUB_INVALID_CREDENTIALS 保留,但含义更为精确 — 现在仅在 clientIdclientSecret 错误时触发,不再作为认证服务任何 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。如果指纹与我们记录的不符,请在切换前联系技术支持进行注册。

最佳实践

后续步骤

本页目录