鉴权与签名
Open API 只做应用维度的鉴权:请求头携带 App-Key、Nonce、Timestamp 与 Signature,服务端使用 App-Secret 按固定顺序计算 SHA-1 签名,并校验时间窗口与 Nonce 唯一性。签名不涉及用户 Token;用户身份由请求体中的 userId 等字段传递,应用服务器需保证其可信。
请求头
| 名称 | 必填 | 说明 | 示例 |
|---|---|---|---|
App-Key | 是 | 应用 AppKey | demo |
Nonce | 是 | 随机串,8-64 字符,5 分钟内不可重复 | 9f2a7c1b |
Timestamp | 是 | epoch 毫秒,允许 ±300000ms | 1750000000000 |
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-Secret | demo-secret |
| Nonce | 9f2a7c1b |
| Timestamp | 1750000000000 |
| 待签字符串 | 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 状态 | 说明 | 可重试 | 起始版本 |
|---|---|---|---|---|---|
1018 | INVALID_SIGNATURE | 401 | 签名错误 | 否 | 1.0.0 |
1019 | TIMESTAMP_EXPIRED | 401 | 时间戳超出允许窗口 | 否 | 1.0.0 |
1020 | NONCE_REPLAYED | 401 | Nonce 重复 | 否 | 1.0.0 |
1017 | APP_DISABLED | 403 | 应用已停用 | 否 | 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 传参并由应用服务器保证可信。
相关说明
- 鉴权与签名:请求头、签名算法与鉴权失败错误码
- 请求与响应数据格式:JSON 请求体、响应信封与分页
- 状态码与错误码:全部错误码、HTTP 状态与重试建议
- 限频、配额与容量限制:协议限制与分页上限
- 服务端 SDK:多语言签名 helper 与请求封装