鉴权与签名

Open API 只做应用维度的鉴权:请求头携带 App-Key、Nonce、Timestamp 与 Signature,服务端使用 App-Secret 按固定顺序计算 SHA-1 签名,并校验时间窗口与 Nonce 唯一性。签名不涉及用户 Token;用户身份由请求体中的 userId 等字段传递,应用服务器需保证其可信。

请求头​

名称必填说明示例
App-Key是应用 AppKeydemo
Nonce是随机串,8-64 字符,5 分钟内不可重复9f2a7c1b
Timestamp是epoch 毫秒,允许 ±300000ms1750000000000
Signature是hex(sha1(AppSecret + Nonce + Timestamp)) 小写372e885320aabd997aafc5491f9d90fdca94432e
X-Request-Id否可选请求头;缺省或超过 128 字符时由服务端生成,响应原值回声req_7f3a9c

签名算法​

签名公式
Signature = hex_lower(sha1(AppSecret + Nonce + Timestamp))

把 App-Secret、Nonce、Timestamp 按上述顺序直接拼接(无分隔符)后计算 SHA-1,输出 40 位十六进制字符串。IDL 声明的算法为 sha1(appSecret + nonce + timestamp),编码为 hex-lowercase;输出统一使用小写 hex,服务端比较签名时忽略大小写。

签名计算示例​

输入值
App-Secretdemo-secret
Nonce9f2a7c1b
Timestamp1750000000000
待签字符串demo-secret9f2a7c1b1750000000000
Signature(SHA-1 hex)372e885320aabd997aafc5491f9d90fdca94432e
可直接复核

以上为一个真实可复算的签名样例,可用于核对你的实现:sha1("demo-secret" + "9f2a7c1b" + "1750000000000") = 372e885320aabd997aafc5491f9d90fdca94432e。时间戳为固定示例值,实际请求请使用当前毫秒时间戳。

时间窗口与 Nonce​

项目规则
时间戳单位epoch 毫秒(13 位)
时间窗口±300,000 毫秒(5 分钟)
Nonce 长度8-64 字符
Nonce 有效期300,000 毫秒内不可重复使用
签名编码小写十六进制(40 字符)
  • 每次请求重新生成 Nonce(例如 16 字节随机数的 hex),不要复用;同一 Nonce 在有效期内重复使用会被拒绝。
  • Timestamp 与服务器时间偏差超过窗口会被拒绝;客户端/服务器请开启时钟同步。
  • App-Secret 只保存在服务端,禁止下发到客户端或写入前端代码。
  • 签名只代表“应用身份”,不代表用户身份;用户维度权限由应用服务器自行校验。

鉴权失败错误码​

错误码名称HTTP 状态说明可重试起始版本
1018INVALID_SIGNATURE401签名错误否1.0.0
1019TIMESTAMP_EXPIRED401时间戳超出允许窗口否1.0.0
1020NONCE_REPLAYED401Nonce 重复否1.0.0
1017APP_DISABLED403应用已停用否1.0.0

代码示例​

APP_KEY="demo"
APP_SECRET="YOUR_APP_SECRET"
BASE_URL="https://im.example.com"
NONCE="$(openssl rand -hex 16)"
TIMESTAMP="$(date +%s%3N)"
SIGNATURE="$(printf '%s' "$APP_SECRET$NONCE$TIMESTAMP" | openssl sha1 | awk '{print $2}')"

curl -sS -X POST "$BASE_URL/im/v2/user/register" \
  -H "Content-Type: application/json" \
  -H "App-Key: $APP_KEY" \
  -H "Nonce: $NONCE" \
  -H "Timestamp: $TIMESTAMP" \
  -H "Signature: $SIGNATURE" \
  -d '{"userId":"user1","name":"Alice","avatarUrl":"https://cdn.example.com/avatar/user1.png"}'
import { createHash, randomBytes } from 'node:crypto';

const APP_KEY = 'demo';
const APP_SECRET = 'YOUR_APP_SECRET';

const nonce = randomBytes(16).toString('hex');   // 32 位 hex,满足 8-64 字符
const timestamp = String(Date.now());            // epoch 毫秒
const signature = createHash('sha1')
  .update(APP_SECRET + nonce + timestamp)
  .digest('hex');

const headers = {
  'Content-Type': 'application/json',
  'App-Key': APP_KEY,
  Nonce: nonce,
  Timestamp: timestamp,
  Signature: signature,
};
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.util.HexFormat;

String appKey = "demo";
String appSecret = "YOUR_APP_SECRET";
String baseUrl = "https://im.example.com";
String path = "/im/v2/user/register";

byte[] nonceBytes = new byte[16];
new SecureRandom().nextBytes(nonceBytes);
String nonce = HexFormat.of().formatHex(nonceBytes);
String timestamp = String.valueOf(System.currentTimeMillis());
String signature = HexFormat.of().formatHex(
    MessageDigest.getInstance("SHA-1")
        .digest((appSecret + nonce + timestamp).getBytes(StandardCharsets.UTF_8)));

String body = "{\"userId\":\"user1\",\"name\":\"Alice\"}";
HttpRequest request = HttpRequest.newBuilder(URI.create(baseUrl + path))
    .header("Content-Type", "application/json")
    .header("App-Key", appKey)
    .header("Nonce", nonce)
    .header("Timestamp", timestamp)
    .header("Signature", signature)
    .POST(HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8))
    .build();
HttpResponse<String> response = HttpClient.newHttpClient()
    .send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
import hashlib
import json
import secrets
import time
import urllib.request

app_key = "demo"
app_secret = "YOUR_APP_SECRET"
base_url = "https://im.example.com"
path = "/im/v2/user/register"

nonce = secrets.token_hex(16)
timestamp = str(int(time.time() * 1000))
signature = hashlib.sha1((app_secret + nonce + timestamp).encode("utf-8")).hexdigest()

body = json.dumps({"userId": "user1", "name": "Alice"}).encode("utf-8")
request = urllib.request.Request(
    base_url + path,
    data=body,
    method="POST",
    headers={
        "Content-Type": "application/json",
        "App-Key": app_key,
        "Nonce": nonce,
        "Timestamp": timestamp,
        "Signature": signature,
    },
)
with urllib.request.urlopen(request) as response:
    print(json.loads(response.read().decode("utf-8")))

安全建议​

  • 为 App-Secret 配置轮换与最小权限:不同环境(测试/生产)使用不同应用与密钥。
  • 服务端日志不要打印完整 App-Secret 与 Signature 之外的敏感信息;如需排查,记录 requestId。
  • 生产环境强制 HTTPS,避免签名头与业务数据在链路上被窃听或篡改。
说明

签名只对 app 维度;不涉及用户 token。用户身份由 body.userId/fromUserId 传参并由应用服务器保证可信。

相关说明​