Skip to content

认证与签名

所有接口均需通过 API Key + 请求签名 进行认证。

请求头说明

请求头必填说明
Authorization格式:Bearer <apiKey>
X-TimestampUnix 时间戳(秒),与服务器时差不超过 5 分钟
X-Signature请求签名,见下方签名算法
X-Request-ID请求唯一标识,建议使用 32 位随机字符串
X-User-ID当前用户标识
Content-Typeapplication/json(文件上传接口使用 multipart/form-data,无需手动设置)

签名算法

签名用于防止请求篡改和重放攻击,算法为 HMAC-SHA256

步骤:

  1. 构建规范化查询字符串canonicalQuery):取 URL 查询参数中所有字段(排除 null、空字符串),按字段名**字典序(ASCII 升序)**排序,拼接为 key1=value1&key2=value2&... 格式;无查询参数时为空字符串
  2. 构建规范化请求体字符串canonicalBody):取 JSON 请求体中所有字段(排除 null、空字符串、纯空白值),按字段名**字典序(ASCII 升序)**排序,拼接为 key1=value1&key2=value2&... 格式;无请求体或 multipart/form-data 接口时为空字符串
  3. 拼接签名基串signatureBase),各部分以换行符 \n 分隔:
    METHOD\nPATH\nX-Timestamp\nX-User-ID\ncanonicalQuery\ncanonicalBody
  4. apiSecret 为密钥,对签名基串做 HMAC-SHA256,取小写十六进制结果作为 X-Signature

示例(POST /v1/chat/stream):

METHOD          = POST
PATH            = /v1/chat/stream
X-Timestamp     = 1742000000
X-User-ID       = user-123
canonicalQuery  = (空,无 URL 查询参数)
canonicalBody   = agentId=agent-uuid&conversationId=conv-uuid&text=你好

signatureBase   = "POST\n/v1/chat/stream\n1742000000\nuser-123\n\nagentId=agent-uuid&conversationId=conv-uuid&text=你好"
X-Signature     = HMAC-SHA256(signatureBase, apiSecret)

注意事项

  • 字符串值去掉首尾空白后再参与签名
  • 非字符串类型(如对象、数组)取其 JSON 字符串参与签名
  • 对象/数组即使为空(如 {} / [])也会参与签名;若不希望参与,请省略该字段或传 null
  • multipart/form-data 上传接口的 canonicalBody 固定为空字符串

JavaScript 签名工具函数

javascript
/**
 * 构建认证请求头
 * @param {string} apiKey - API Key
 * @param {string} apiSecret - API Secret
 * @param {string} userId - 用户 ID
 * @param {Object} body - 请求体对象(multipart 接口传 {})
 * @param {Object} options - 配置项
 * @param {string} options.url - 请求 URL(必填,用于提取 path 和 query)
 * @param {string} [options.method='POST'] - HTTP 方法
 * @param {boolean} [options.isStreamRequest=false] - 是否 SSE 流式请求
 * @param {boolean} [options.isMultipart=false] - 是否 multipart/form-data
 */
async function buildAuthHeaders(apiKey, apiSecret, userId, body = {}, options = {}) {
  const method = (options.method || 'POST').toUpperCase()
  const timestamp = Math.floor(Date.now() / 1000).toString()
  const requestId = generateSecureRandomString(32)

  // 解析 URL,提取 path 和 query 参数
  const resolvedUrl = new URL(options.url, window.location.origin)
  const path = resolvedUrl.pathname
  const queryParams = {}
  resolvedUrl.searchParams.forEach((value, key) => {
    queryParams[key] = value
  })

  // 规范化参数(排除 null 和空字符串,字典序排序)
  function canonicalize(params) {
    return Object.keys(params)
      .filter(k => {
        const v = params[k]
        if (v == null) return false
        if (typeof v === 'string') return v.trim() !== ''
        return true
      })
      .sort()
      .map(k => {
        const v = params[k]
        const val = typeof v === 'string' ? v.trim() : JSON.stringify(v)
        return `${k}=${val}`
      })
      .join('&')
  }

  const canonicalQuery = canonicalize(queryParams)
  const canonicalBody = options.isMultipart ? '' : canonicalize(body)

  // 构建签名基串
  const signatureBase = [method, path, timestamp, userId, canonicalQuery, canonicalBody].join('\n')

  // HMAC-SHA256 签名
  const encoder = new TextEncoder()
  const cryptoKey = await crypto.subtle.importKey(
    'raw',
    encoder.encode(apiSecret),
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['sign']
  )
  const sigBuffer = await crypto.subtle.sign('HMAC', cryptoKey, encoder.encode(signatureBase))
  const signature = Array.from(new Uint8Array(sigBuffer))
    .map(b => b.toString(16).padStart(2, '0'))
    .join('')

  const headers = {
    'Authorization': `Bearer ${apiKey}`,
    'X-User-ID': userId,
    'X-Timestamp': timestamp,
    'X-Signature': signature,
    'X-Request-ID': requestId,
    'Accept': options.isStreamRequest ? 'text/event-stream' : 'application/json',
  }
  if (!options.isMultipart) {
    headers['Content-Type'] = 'application/json'
  }
  return headers
}

// 生成安全随机字符串
function generateSecureRandomString(length) {
  const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
  const randomValues = new Uint8Array(length)
  crypto.getRandomValues(randomValues)
  return Array.from(randomValues, v => chars[v % chars.length]).join('')
}

调用示例

javascript
// 普通 JSON 接口
const headers = await buildAuthHeaders(apiKey, apiSecret, userId, { agentId }, {
  url: '/v1/chat/conversation',
})

// SSE 流式接口
const headers = await buildAuthHeaders(apiKey, apiSecret, userId, { agentId, conversationId, text }, {
  url: '/v1/chat/stream',
  isStreamRequest: true,
})

// multipart/form-data 文件上传接口
const headers = await buildAuthHeaders(apiKey, apiSecret, userId, {}, {
  url: '/v1/agent/face-detect',
  isMultipart: true,
})

限流规则

每个 API Key 独立计算:

  • 频率限制(Rate Limit):单位时间内最大请求次数,超出返回 Too many requests 错误
  • 总量限制(Max Usage):累计调用总次数上限,超出返回 API key usage limit reached 错误

具体限制值由平台管理员在创建 API Key 时配置。

常见认证错误

HTTP 状态码原因
401缺少 Authorization / X-Timestamp / X-Signature 等必要请求头
401时间戳与服务器时差超过 5 分钟(防重放)
401API Key 无效或已禁用
401签名验证失败(参数顺序、路径提取或 secret 错误)

Released under the MIT License.