Safira Paydocs
集成指南

认证

概述

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.safirapay.com/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.safirapay.com/api/auth/token',
  headers: {
    'Content-Type': 'application/json',
    'X-SSL-Client-Cert': encodedCert
  },
  data: {
    clientId: process.env.SAFIRAPAY_CLIENT_ID,
    clientSecret: process.env.SAFIRAPAY_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.safirapay.com/api/auth/token'
headers = {
    'Content-Type': 'application/json',
    'X-SSL-Client-Cert': encoded_cert
}
payload = {
    'clientId': os.environ.get('SAFIRAPAY_CLIENT_ID'),
    'clientSecret': os.environ.get('SAFIRAPAY_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.safirapay.com/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.safirapay.com/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.SAFIRAPAY_CLIENT_ID,
  process.env.SAFIRAPAY_CLIENT_SECRET,
  './client-cert.pem'
);

// In any request
const token = await tokenManager.getValidToken();

证书验证

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

  1. 有效的 PEM 格式:证书必须为 PEM 格式并经过 URL 编码
  2. 账户关联:证书的 SHA256 指纹必须已注册并关联到您的账户
  3. 凭证匹配:证书必须属于与 OAuth 凭证相同的账户

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

常见错误

400 Bad Request

原因: 证书缺失或格式错误

{
  "statusCode": 400,
  "message": "Certificado ausente no header X-SSL-Client-Cert"
}

解决方案: 请检查:

  • 证书是否为 PEM 格式
  • 证书是否经过 URL 编码(使用 encodeURIComponent()
  • 请求中是否包含 X-SSL-Client-Cert 请求头

401 Unauthorized

原因: 凭证无效或证书未授权

{
  "statusCode": 401,
  "message": "Credenciais inválidas ou certificado inválido"
}

解决方案: 请检查:

  • clientIdclientSecret 是否正确
  • 证书是否已在 Avista 门户中关联到您的账户
  • 证书是否与正在使用的 OAuth 凭证匹配

403 Forbidden

原因: 证书未关联到账户

{
  "statusCode": 403,
  "message": "Certificado não vinculado à conta"
}

解决方案: 联系 Avista 技术支持将证书关联到您的账户。

最佳实践

后续步骤

本页目录