认证与签名
租户 API 的每个请求都以 HMAC-SHA256 签名。服务器依序检查密钥、时间戳、nonce、签名、租户状态、IP 白名单与限流,全部通过才会处理请求。
请求头部
| 头部 | 内容 |
|---|---|
X-Api-Key | 密钥 ID:沙箱 ek_s_…、正式 ek_l_…,例如 ek_s_1a_XXXXXXXXXXXXXXXX |
X-Timestamp | 目前的 Unix 时间,秒(9–11 位数字);与服务器时间差须在 ±300 秒内 |
X-Nonce | 每个请求都不同的随机字符串,16–64 个字母或数字;10 分钟内不可重复 |
X-Signature | 签名,64 个十六进制字符(建议小写;大小写都接受) |
Content-Type | 有正文时为 application/json |
密钥
- 密钥 ID(
X-Api-Key)格式是ek_<s|l>_<租户代号>_<16 码>:s为沙箱、l为正式。密钥 ID 本身就指出是哪个租户,所以请求不需要另外带公司代码。 - Secret 是
es_加 40 个字母或数字。整个字符串(包含es_)以 UTF-8 字节作为 HMAC 密钥。 - 密钥在 Console 上线与对接 → API 密钥 管理:
- 每个环境(沙箱、正式)最多 10 把有效密钥。
- 「显示」「拷贝」Secret 需要先验证双因子(TOTP),每次都会留下审核记录。
- 轮替:产生新密钥,旧密钥转为「轮替中」,24 小时后失效;请在期限内把新 Secret 部署到你的系统。
- 禁用:立即失效,无法复原。
- 沙箱密钥连到你的沙箱租户(测试币、模拟器桌),正式密钥连到正式租户。两者使用同一个主机与同一套 API。
签名算法
text
string_to_sign = METHOD + "\n" +
PATH_AND_QUERY + "\n" +
X-Timestamp + "\n" +
X-Nonce + "\n" +
hex(SHA-256(body))
X-Signature = hex(HMAC-SHA256(key = secret, message = string_to_sign))| 部分 | 规则 |
|---|---|
METHOD | 大写的 HTTP 方法:GET 或 POST |
PATH_AND_QUERY | 从 /api/tenant/v1/… 开始的完整路径,有查询字符串时加上 ? 与查询字符串。不含通信协定、主机与 # 之后的部分。查询字符串必须和实际送出的完全相同(参数顺序、百分比编码都一样) |
X-Timestamp、X-Nonce | 与头部中的字符串完全相同 |
hex(SHA-256(body)) | 实际送出的正文字节(UTF-8)的 SHA-256,小写十六进制。没有正文时是空字符串的哈希:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 |
| 换行 | 只用 \n(LF),最后一行后面没有换行 |
GET 请求
服务器处理 GET 请求时一律忽略正文,正文哈希固定是空字符串的哈希。GET 的参数全部放在查询字符串,并且会被签进 PATH_AND_QUERY。
示例
用下列(仅供示范的)密钥与输入计算:
| 项目 | 值 |
|---|---|
| 密钥 ID | ek_s_1a_XXXXXXXXXXXXXXXX |
| Secret | es_0123456789abcdefghijABCDEFGHIJ0123456789 |
| 方法与路径 | POST /api/tenant/v1/player/launch |
| X-Timestamp | 1790218800(2026-09-24T03:00:00Z) |
| X-Nonce | n0nce8H2kQ9xYz4LmP0aBcDe |
| 正文 | {"username":"alice","lang":"ENG","device":"mobile"} |
正文的 SHA-256:
texta0c1cfba44962853266340b6610d542620041fae49e6bcb10e6446ca0f73ad6a签名字符串(五行):
textPOST /api/tenant/v1/player/launch 1790218800 n0nce8H2kQ9xYz4LmP0aBcDe a0c1cfba44962853266340b6610d542620041fae49e6bcb10e6446ca0f73ad6aX-Signature:text6e4885c5d9f191b2e4ee5f4cb40ae148b13b277e606181bf853e040a06a0ac80
完整的请求:
http
POST /api/tenant/v1/player/launch HTTP/1.1
Host: elite.ewin888.com
X-Api-Key: ek_s_1a_XXXXXXXXXXXXXXXX
X-Timestamp: 1790218800
X-Nonce: n0nce8H2kQ9xYz4LmP0aBcDe
X-Signature: 6e4885c5d9f191b2e4ee5f4cb40ae148b13b277e606181bf853e040a06a0ac80
Content-Type: application/json
{"username":"alice","lang":"ENG","device":"mobile"}GET 的示例:GET /api/tenant/v1/wallet/balance?username=alice,时间戳相同、nonce 为 n0nce8H2kQ9xYz4LmP0aBcDf、没有正文,签名为 273b761bda9ca095a8c7ae22908844bc1ce728212fceda3d0b2129fc9bb03032。
这两组数字可以直接贴到签名调试器验算。
代码示例
每个示例都包含 sign 函数与一个送出请求的小工具,直接可执行文件案会印出上面示例的签名 6e4885c5…0ac80。
js
// elite tenant API: request signing (Node.js 18+, ES module, no dependencies)
import { createHash, createHmac, randomBytes } from 'node:crypto';
import { pathToFileURL } from 'node:url';
/**
* X-Signature = hex(HMAC-SHA256(secret,
* METHOD + "\n" + PATH_AND_QUERY + "\n" + TIMESTAMP + "\n" + NONCE + "\n" + hex(SHA-256(body))))
*/
export function sign(secret, method, pathAndQuery, timestamp, nonce, body = '') {
const bodyHash = createHash('sha256').update(body, 'utf8').digest('hex');
const stringToSign = [method.toUpperCase(), pathAndQuery, timestamp, nonce, bodyHash].join('\n');
return createHmac('sha256', secret).update(stringToSign, 'utf8').digest('hex');
}
/** 32 hex characters; use a new nonce for every request, including retries */
export function newNonce() {
return randomBytes(16).toString('hex');
}
/** Sends a signed request. Returns `data`, or throws { status, code, message }. */
export async function call(method, pathAndQuery, payload, { keyId, secret, baseUrl = 'https://elite.ewin-soft.com' }) {
// Sign exactly the bytes you send: serialize once and reuse the string.
const body = payload === undefined ? '' : JSON.stringify(payload);
const timestamp = String(Math.floor(Date.now() / 1000)); // seconds, not milliseconds
const nonce = newNonce();
const headers = {
'X-Api-Key': keyId,
'X-Timestamp': timestamp,
'X-Nonce': nonce,
'X-Signature': sign(secret, method, pathAndQuery, timestamp, nonce, body),
};
if (body) headers['Content-Type'] = 'application/json';
const res = await fetch(baseUrl + pathAndQuery, { method, headers, body: body || undefined, signal: AbortSignal.timeout(10_000) });
const json = await res.json().catch(() => null);
if (!res.ok || !json?.ok) {
throw { status: res.status, code: json?.error?.code ?? `HTTP_${res.status}`, message: json?.error?.message ?? res.statusText };
}
return json.data;
}
// Example:
// const auth = { keyId: process.env.ELITE_KEY_ID, secret: process.env.ELITE_SECRET };
// const { url } = await call('POST', '/api/tenant/v1/player/launch', { username: 'alice' }, auth);
// const { balance } = await call('GET', '/api/tenant/v1/wallet/balance?username=alice', undefined, auth);
// Worked example from the docs: prints 6e4885c5d9f191b2e4ee5f4cb40ae148b13b277e606181bf853e040a06a0ac80
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
console.log(
sign(
'es_0123456789abcdefghijABCDEFGHIJ0123456789',
'POST',
'/api/tenant/v1/player/launch',
'1790218800',
'n0nce8H2kQ9xYz4LmP0aBcDe',
'{"username":"alice","lang":"ENG","device":"mobile"}',
),
);
}php
<?php
// elite tenant API: request signing (PHP 7.2+; the request helper needs ext-curl)
/**
* hex(HMAC-SHA256(secret, METHOD \n PATH_AND_QUERY \n TIMESTAMP \n NONCE \n hex(SHA-256(body))))
*/
function elite_sign(string $secret, string $method, string $pathAndQuery, string $timestamp, string $nonce, string $body = ''): string
{
$stringToSign = strtoupper($method) . "\n" . $pathAndQuery . "\n" . $timestamp . "\n" . $nonce . "\n" . hash('sha256', $body);
return hash_hmac('sha256', $stringToSign, $secret);
}
/**
* Sends a signed request and returns `data`. Throws RuntimeException with the error code on failure.
*/
function elite_call(string $method, string $pathAndQuery, ?array $payload, string $keyId, string $secret, string $baseUrl = 'https://elite.ewin-soft.com'): array
{
// Sign exactly the bytes you send: encode once and reuse the string.
$body = $payload === null ? '' : json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$timestamp = (string) time(); // seconds
$nonce = bin2hex(random_bytes(16)); // new nonce for every request, including retries
$headers = [
'X-Api-Key: ' . $keyId,
'X-Timestamp: ' . $timestamp,
'X-Nonce: ' . $nonce,
'X-Signature: ' . elite_sign($secret, $method, $pathAndQuery, $timestamp, $nonce, $body),
];
if ($body !== '') {
$headers[] = 'Content-Type: application/json';
}
$ch = curl_init($baseUrl . $pathAndQuery);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
]);
if ($body !== '') {
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
}
$raw = curl_exec($ch);
$status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
$json = json_decode((string) $raw, true);
if ($status < 200 || $status >= 300 || empty($json['ok'])) {
$code = $json['error']['code'] ?? ('HTTP_' . $status);
throw new RuntimeException($code . ': ' . ($json['error']['message'] ?? ''), $status);
}
return $json['data'];
}
// Example:
// $url = elite_call('POST', '/api/tenant/v1/player/launch', ['username' => 'alice'], $keyId, $secret)['url'];
// $balance = elite_call('GET', '/api/tenant/v1/wallet/balance?username=alice', null, $keyId, $secret)['balance'];
// Worked example from the docs: prints 6e4885c5d9f191b2e4ee5f4cb40ae148b13b277e606181bf853e040a06a0ac80
if (PHP_SAPI === 'cli' && isset($argv[0]) && realpath($argv[0]) === __FILE__) {
echo elite_sign('es_0123456789abcdefghijABCDEFGHIJ0123456789', 'POST', '/api/tenant/v1/player/launch',
'1790218800', 'n0nce8H2kQ9xYz4LmP0aBcDe', '{"username":"alice","lang":"ENG","device":"mobile"}'), PHP_EOL;
}cs
// elite tenant API: request signing (.NET 6+)
using System;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
// Worked example from the docs: prints 6e4885c5d9f191b2e4ee5f4cb40ae148b13b277e606181bf853e040a06a0ac80
Console.WriteLine(EliteSigner.Sign("es_0123456789abcdefghijABCDEFGHIJ0123456789", "POST", "/api/tenant/v1/player/launch",
"1790218800", "n0nce8H2kQ9xYz4LmP0aBcDe", "{\"username\":\"alice\",\"lang\":\"ENG\",\"device\":\"mobile\"}"));
public static class EliteSigner
{
/// hex(HMAC-SHA256(secret, METHOD \n PATH_AND_QUERY \n TIMESTAMP \n NONCE \n hex(SHA-256(body))))
public static string Sign(string secret, string method, string pathAndQuery, string timestamp, string nonce, string body)
{
var bodyHash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(body))).ToLowerInvariant();
var stringToSign = $"{method.ToUpperInvariant()}\n{pathAndQuery}\n{timestamp}\n{nonce}\n{bodyHash}";
var mac = HMACSHA256.HashData(Encoding.UTF8.GetBytes(secret), Encoding.UTF8.GetBytes(stringToSign));
return Convert.ToHexString(mac).ToLowerInvariant();
}
/// Sends a signed request and returns `data`. Throws HttpRequestException on failure.
public static async Task<JsonElement> CallAsync(HttpClient http, string method, string pathAndQuery, object? payload,
string keyId, string secret, string baseUrl = "https://elite.ewin-soft.com")
{
// Sign exactly the bytes you send: serialize once and reuse the string.
var body = payload is null ? "" : JsonSerializer.Serialize(payload);
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(); // seconds
var nonce = Convert.ToHexString(RandomNumberGenerator.GetBytes(16)).ToLowerInvariant(); // new for every request
using var request = new HttpRequestMessage(new HttpMethod(method), baseUrl + pathAndQuery);
request.Headers.Add("X-Api-Key", keyId);
request.Headers.Add("X-Timestamp", timestamp);
request.Headers.Add("X-Nonce", nonce);
request.Headers.Add("X-Signature", Sign(secret, method, pathAndQuery, timestamp, nonce, body));
if (body.Length > 0) request.Content = new StringContent(body, Encoding.UTF8, "application/json");
using var response = await http.SendAsync(request);
var text = await response.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(text);
var root = doc.RootElement;
if (!response.IsSuccessStatusCode || !root.GetProperty("ok").GetBoolean())
{
var error = root.GetProperty("error");
throw new HttpRequestException($"{error.GetProperty("code").GetString()}: {error.GetProperty("message").GetString()}");
}
return root.GetProperty("data").Clone();
}
}java
// elite tenant API: request signing (Java 11+, no dependencies)
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.time.Duration;
import java.util.Locale;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public final class EliteSigner {
private static final SecureRandom RANDOM = new SecureRandom();
/** hex(HMAC-SHA256(secret, METHOD \n PATH_AND_QUERY \n TIMESTAMP \n NONCE \n hex(SHA-256(body)))) */
public static String sign(String secret, String method, String pathAndQuery, String timestamp, String nonce, String body) throws Exception {
String bodyHash = hex(MessageDigest.getInstance("SHA-256").digest(body.getBytes(StandardCharsets.UTF_8)));
String stringToSign = method.toUpperCase(Locale.ROOT) + "\n" + pathAndQuery + "\n" + timestamp + "\n" + nonce + "\n" + bodyHash;
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
return hex(mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8)));
}
/** 32 hex characters; use a new nonce for every request, including retries */
public static String newNonce() {
byte[] bytes = new byte[16];
RANDOM.nextBytes(bytes);
return hex(bytes);
}
/** Sends a signed request and returns the JSON response body; throws on a non-2xx status. */
public static String call(HttpClient http, String method, String pathAndQuery, String jsonBody,
String keyId, String secret, String baseUrl) throws Exception {
// Sign exactly the bytes you send: build the JSON string once and reuse it.
String body = jsonBody == null ? "" : jsonBody;
String timestamp = Long.toString(System.currentTimeMillis() / 1000); // seconds
String nonce = newNonce();
HttpRequest.Builder builder = HttpRequest.newBuilder(URI.create(baseUrl + pathAndQuery))
.timeout(Duration.ofSeconds(10))
.header("X-Api-Key", keyId)
.header("X-Timestamp", timestamp)
.header("X-Nonce", nonce)
.header("X-Signature", sign(secret, method, pathAndQuery, timestamp, nonce, body));
if (body.isEmpty()) {
builder.method(method, HttpRequest.BodyPublishers.noBody());
} else {
builder.header("Content-Type", "application/json")
.method(method, HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8));
}
HttpResponse<String> response = http.send(builder.build(), HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
if (response.statusCode() / 100 != 2) {
throw new RuntimeException("HTTP " + response.statusCode() + ": " + response.body());
}
return response.body();
}
private static String hex(byte[] bytes) {
StringBuilder sb = new StringBuilder(bytes.length * 2);
for (byte b : bytes) {
sb.append(Character.forDigit((b >> 4) & 0xf, 16)).append(Character.forDigit(b & 0xf, 16));
}
return sb.toString();
}
// Worked example from the docs: prints 6e4885c5d9f191b2e4ee5f4cb40ae148b13b277e606181bf853e040a06a0ac80
public static void main(String[] args) throws Exception {
System.out.println(sign("es_0123456789abcdefghijABCDEFGHIJ0123456789", "POST", "/api/tenant/v1/player/launch",
"1790218800", "n0nce8H2kQ9xYz4LmP0aBcDe", "{\"username\":\"alice\",\"lang\":\"ENG\",\"device\":\"mobile\"}"));
}
}py
"""elite tenant API: request signing (Python 3.8+, standard library only)."""
import hashlib
import hmac
import json
import secrets
import time
import urllib.request
def sign(secret: str, method: str, path_and_query: str, timestamp: str, nonce: str, body: bytes = b"") -> str:
"""hex(HMAC-SHA256(secret, METHOD \\n PATH_AND_QUERY \\n TIMESTAMP \\n NONCE \\n hex(SHA-256(body))))"""
body_hash = hashlib.sha256(body).hexdigest()
string_to_sign = "\n".join([method.upper(), path_and_query, timestamp, nonce, body_hash])
return hmac.new(secret.encode("utf-8"), string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest()
def call(method, path_and_query, payload, key_id, secret, base_url="https://elite.ewin-soft.com"):
"""Sends a signed request and returns `data`. Raises urllib.error.HTTPError on 4xx/5xx."""
# Sign exactly the bytes you send: serialize once and reuse them.
body = b"" if payload is None else json.dumps(payload, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
timestamp = str(int(time.time())) # seconds
nonce = secrets.token_hex(16) # new nonce for every request, including retries
headers = {
"X-Api-Key": key_id,
"X-Timestamp": timestamp,
"X-Nonce": nonce,
"X-Signature": sign(secret, method, path_and_query, timestamp, nonce, body),
"User-Agent": "my-backend/1.0",
}
if body:
headers["Content-Type"] = "application/json"
request = urllib.request.Request(base_url + path_and_query, data=body or None, method=method, headers=headers)
with urllib.request.urlopen(request, timeout=10) as response:
return json.loads(response.read())["data"]
# Example:
# url = call("POST", "/api/tenant/v1/player/launch", {"username": "alice"}, KEY_ID, SECRET)["url"]
# balance = call("GET", "/api/tenant/v1/wallet/balance?username=alice", None, KEY_ID, SECRET)["balance"]
if __name__ == "__main__":
# Worked example from the docs: prints 6e4885c5d9f191b2e4ee5f4cb40ae148b13b277e606181bf853e040a06a0ac80
print(sign("es_0123456789abcdefghijABCDEFGHIJ0123456789", "POST", "/api/tenant/v1/player/launch",
"1790218800", "n0nce8H2kQ9xYz4LmP0aBcDe", b'{"username":"alice","lang":"ENG","device":"mobile"}'))go
// elite tenant API: request signing (Go 1.20+, standard library only)
package main
import (
"bytes"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"time"
)
// Sign returns hex(HMAC-SHA256(secret, METHOD \n PATH_AND_QUERY \n TIMESTAMP \n NONCE \n hex(SHA-256(body)))).
func Sign(secret, method, pathAndQuery, timestamp, nonce string, body []byte) string {
sum := sha256.Sum256(body)
stringToSign := strings.Join([]string{strings.ToUpper(method), pathAndQuery, timestamp, nonce, hex.EncodeToString(sum[:])}, "\n")
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(stringToSign))
return hex.EncodeToString(mac.Sum(nil))
}
// NewNonce returns 32 hex characters; use a new nonce for every request, including retries.
func NewNonce() string {
b := make([]byte, 16)
if _, err := rand.Read(b); err != nil {
panic(err)
}
return hex.EncodeToString(b)
}
// Call sends a signed request and returns the JSON response body; it returns an error on a non-2xx status.
// Sign exactly the bytes you send: marshal the JSON once and pass the same slice here.
func Call(client *http.Client, method, pathAndQuery string, body []byte, keyID, secret, baseURL string) ([]byte, error) {
timestamp := strconv.FormatInt(time.Now().Unix(), 10) // seconds
nonce := NewNonce()
req, err := http.NewRequest(method, baseURL+pathAndQuery, bytes.NewReader(body))
if err != nil {
return nil, err
}
req.Header.Set("X-Api-Key", keyID)
req.Header.Set("X-Timestamp", timestamp)
req.Header.Set("X-Nonce", nonce)
req.Header.Set("X-Signature", Sign(secret, method, pathAndQuery, timestamp, nonce, body))
if len(body) > 0 {
req.Header.Set("Content-Type", "application/json")
}
res, err := client.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
data, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
if res.StatusCode/100 != 2 {
return data, fmt.Errorf("HTTP %d: %s", res.StatusCode, data)
}
return data, nil
}
// Worked example from the docs: prints 6e4885c5d9f191b2e4ee5f4cb40ae148b13b277e606181bf853e040a06a0ac80
func main() {
fmt.Println(Sign("es_0123456789abcdefghijABCDEFGHIJ0123456789", "POST", "/api/tenant/v1/player/launch",
"1790218800", "n0nce8H2kQ9xYz4LmP0aBcDe", []byte(`{"username":"alice","lang":"ENG","device":"mobile"}`)))
}服务器的检查顺序
| 顺序 | 检查 | 失败时 |
|---|---|---|
| 1 | X-Api-Key 的格式 | 401 UNAUTHORIZED(invalid credentials) |
| 2 | 正文不超过 64 KB | 413 PAYLOAD_TOO_LARGE |
| 3 | 密钥存在、未禁用(轮替中的旧密钥在 24 小时内仍有效) | 401 UNAUTHORIZED(invalid credentials) |
| 4 | X-Timestamp 格式正确、与服务器时间差 ≤ 300 秒 | 401 UNAUTHORIZED(timestamp outside the ±300 s window) |
| 5 | X-Nonce 格式(16–64 字母或数字) | 401 UNAUTHORIZED(invalid credentials) |
| 6 | 签名以固定时间比对 | 401 UNAUTHORIZED(invalid credentials) |
| 7 | 租户未禁用、未停权 | 403 TENANT_SUSPENDED |
| 8 | 来源 IP 在白名单内(有设置时) | 403 IP_NOT_ALLOWED |
| 9 | 租户限流 | 429 RATE_LIMITED |
| 10 | nonce 在 10 分钟内没有用过 | 401 UNAUTHORIZED(nonce already used) |
- 密钥不存在与签名错误回同一个消息,避免被用来探测密钥。
- nonce 只有在前面全部通过时才会被记录,所以因签名错误、限流等被拒绝的请求不会占用 nonce。即使如此,每次重试都请产生新的 nonce 与时间戳。
- 收到
nonce already used表示这个请求没有被处理(例如网络层自动重送了同一个请求)。
IP 白名单
在 Console 上线与对接 → IP 白名单与 Webhook 设置,最多 100 笔 IPv4、IPv6 地址或 CIDR(例如 203.0.113.10、203.0.113.0/24、2001:db8::/32)。空白代表不限制。
- 判断依据是 Cloudflare 看到的来源 IP(
CF-Connecting-IP),也就是你的服务器的对外 IP。 - 服务器同时有 IPv4 与 IPv6 时,请两种都加入,或让 HTTP 客户端固定使用其中一种。
- 白名单设置在正式租户上;沙箱密钥不受限制。
常见错误
| 状况 | 原因与解法 |
|---|---|
一直收到 invalid credentials | 用签名调试器输入同样的值,逐行比对签名字符串 |
| 路径少了前缀 | PATH_AND_QUERY 必须从 /api/tenant/v1 开始,不是相对于 Base URL 的 /player/launch |
| GET 签名错误 | 忘了把查询字符串签进去,或签名用的查询字符串与实际送出的编码、顺序不同 |
| POST 签名错误 | 签名用的正文和送出的正文不是同一个字符串:只串行化一次,签名与送出都用那个字符串;不要让 HTTP 函数库重新串行化 |
| 非英文字符 | 以 UTF-8 字节计算正文哈希(例如中文暱称) |
timestamp outside the ±300 s window | 服务器时钟偏移(请打开 NTP),或送了毫秒而不是秒 |
nonce already used | 重试时沿用了旧的 nonce;每个请求都要产生新的 nonce |
| Secret 不对 | Secret 要包含 es_ 前缀;密钥 ID 与 Secret 必须是同一把、同一个环境 |
IP_NOT_ALLOWED | 对外 IP 不在白名单,或服务器改走 IPv6 |