认证与签名
直接 HTTP OpenAPI 的调用分两步:签名申请 Token,再用 Token 与当前业务请求签名调用接口。Access Secret 是签名密钥;OAuth 客户端的 client_secret 是另一项配置,两者不能互相替代。
1. 生成签名
产品文档的 生成签名 页面提供 JSON 参数输入、Token 示例、JSON 示例及代码导出,可选择 JavaScript、Java、PHP、Python、C#、Go、Android 或 iOS。
通用步骤:
- 取本次实际发送的签名参数,排除
signature自身,不提交无意义的空参数。 - 对象、数组转换成 JSON 字符串;标量转换为字符串。
- 按参数名 ASCII 升序排列,拼接
key1=value1&key2=value2。 - 使用 Access Secret 对拼接字符串计算 HMAC-SHA256。
- 输出小写十六进制,填入本次请求的
signature。
选择服务端使用的语言,点击代码块右上角的复制按钮即可复制当前示例。各示例输入均为参数名 → 最终字符串值:请先按上面的规则序列化对象、数组和标量,签名与实际发送使用同一份字符串,不再交给不同语言重新序列化。
- Node.js
- Python
- Java
- PHP
- Go
import {createHmac} from 'node:crypto';
function sign(params, accessSecret) {
const entries = Object.entries(params)
.filter(([key, value]) => key !== 'signature' && value != null)
.filter(([, value]) => value.trim() !== '')
.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
const message = entries.map(([key, value]) => `${key}=${value}`).join('&');
return createHmac('sha256', accessSecret).update(message, 'utf8').digest('hex');
}
import hashlib
import hmac
def sign(params: dict[str, str], access_secret: str) -> str:
entries = sorted(
(key, value) for key, value in params.items()
if key != "signature" and value is not None and value.strip() != ""
)
message = "&".join(f"{key}={value}" for key, value in entries)
return hmac.new(
access_secret.encode("utf-8"),
message.encode("utf-8"),
hashlib.sha256,
).hexdigest()
import java.nio.charset.StandardCharsets;
import java.util.HexFormat;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public class DuogexSignature {
public static String sign(Map<String, String> params, String accessSecret)
throws Exception {
Map<String, String> sorted = new TreeMap<>(params);
String message = sorted.entrySet().stream()
.filter(e -> !e.getKey().equals("signature"))
.filter(e -> e.getValue() != null && !e.getValue().isBlank())
.map(e -> e.getKey() + "=" + e.getValue())
.collect(Collectors.joining("&"));
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(
accessSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
return HexFormat.of().formatHex(
mac.doFinal(message.getBytes(StandardCharsets.UTF_8)));
}
}
<?php
function sign(array $params, string $accessSecret): string {
$entries = array_filter(
$params,
fn($value, $key) => $key !== 'signature'
&& $value !== null && trim($value) !== '',
ARRAY_FILTER_USE_BOTH
);
ksort($entries, SORT_STRING);
$parts = [];
foreach ($entries as $key => $value) {
$parts[] = $key . '=' . $value;
}
return hash_hmac('sha256', implode('&', $parts), $accessSecret);
}
PHP 的参数值与密钥须使用 UTF-8 编码。
package signature
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"sort"
"strings"
)
func Sign(params map[string]string, accessSecret string) string {
keys := make([]string, 0, len(params))
for key, value := range params {
if key != "signature" && strings.TrimSpace(value) != "" {
keys = append(keys, key)
}
}
sort.Strings(keys)
parts := make([]string, 0, len(keys))
for _, key := range keys {
parts = append(parts, key+"="+params[key])
}
mac := hmac.New(sha256.New, []byte(accessSecret))
mac.Write([]byte(strings.Join(parts, "&")))
return hex.EncodeToString(mac.Sum(nil))
}
参数名使用接口规定的 ASCII 名称;空值应先从最终请求中去掉,值为 "0"、"false" 时应保留。用于过滤空白的判断不会修改非空参数的原值。Java 示例要求 Java 17 或以上;其余示例分别使用 Node.js、Python 3、PHP 和 Go 标准库。上方代码只在服务端运行,不把 Secret 填入网页。
先构建最终 payload,再生成签名。签名后不要修改字段值、对象序列化顺序或时间戳;不能先对 URL 编码后的整段文本签名。Token 参数与业务参数分别生成,不能复用上一次签名。
网关会合并指定 Header、query、body 中的签名参数。重复提供同名参数时必须完全一致,不要在 Header 中保留旧的 app_id 或 timestamp。不要将 Access Secret 放进公开网页、App 包或发送给业务接口。
2. 申请 Token
接口为 POST /auth/oauth2/token,使用 application/x-www-form-urlencoded。普通业务接口的 JSON 要求不能套用到这个请求。
| 位置 | 参数 | 说明 |
|---|---|---|
| Header | TENANT-ID | 数字租户 ID |
| Header | Content-Type | application/x-www-form-urlencoded |
| 表单 | grant_type | 固定 openapi |
| 表单 | username | 获授权的用户名 |
| 表单 | app_id | 应用 ID |
| 表单 | timestamp | 当前毫秒时间戳 |
| 表单 | client_id、client_secret | 当前部署的认证客户端配置 |
| 表单 | access_id | 当前授权的访问 ID,提供时按它匹配密钥 |
| 表单 | signature | 上述实际表单参数的签名 |
示例接续上面的 sign 函数,环境变量由调用服务注入:
const params = {
grant_type: 'openapi',
username: process.env.DUOGEX_USERNAME,
app_id: process.env.DUOGEX_APPLICATION_ID,
timestamp: String(Date.now()),
client_id: process.env.DUOGEX_CLIENT_ID,
client_secret: process.env.DUOGEX_CLIENT_SECRET,
access_id: process.env.DUOGEX_ACCESS_ID,
};
const body = new URLSearchParams({
...params,
signature: sign(params, process.env.DUOGEX_ACCESS_SECRET),
});
const response = await fetch(`${process.env.DUOGEX_API_BASE}/auth/oauth2/token`, {
method: 'POST',
headers: {
'TENANT-ID': process.env.DUOGEX_TENANT_ID,
'Content-Type': 'application/x-www-form-urlencoded',
},
body,
});
const result = await response.json();
if (!response.ok || !result.access_token) {
throw new Error('获取 Token 失败,请核对业务错误与授权配置');
}
// 将令牌保存在服务端,按实际 expires_in 管理有效期。
运行前必须配置示例中的全部环境变量。页面有历史客户端默认值,正式接入应使用管理员确认的当前配置,不将默认值当作已授权凭据。页面响应示例为 43200 秒,有效期仍按真实响应管理。
3. 调用业务接口
读取列表等签名业务接口携带以下请求头:
Authorization: Bearer YOUR_ACCESS_TOKEN
TENANT-ID: YOUR_NUMERIC_TENANT_ID
X-APPLICATION-ID: YOUR_APPLICATION_ID
app_id: YOUR_APPLICATION_ID
timestamp: CURRENT_TIMESTAMP_IN_MILLISECONDS
signature: SIGNATURE_FOR_THIS_REQUEST
Content-Type: application/json
签名参数包含当前业务 payload、app_id、timestamp,以及实际提供的 access_id。signature 不参与自身计算。不要把申请 Token 时的 grant_type、client_secret 等无关参数复制到业务请求。
HTTP 认证、业务签名与表单权限分别校验。签名正确不代表用户有权读取或修改全部记录。下一步见数据查询。