OpenAPI 加密鉴权接入指南
本文档面向调用 AnyMetrics OpenAPI 的接入方(客户端),说明如何对请求进行加密鉴权。核心动作只有一个:把你要代表的用户账号 account 加密成 token,随请求头发送。
总览
AnyMetrics OpenAPI 的加密鉴权流程:
- 管理员在 AnyMetrics 管理台「企业安全 → OPEN API 鉴权」开启功能并生成密钥,复制出公钥(RSA)或对称密钥(AES)交给接入方。
- 接入方用拿到的密钥,把目标用户的
account(明文)加密成token。 - 调用 OpenAPI 时带上两个请求头:
tenant-id和token。 - 服务端用对应私钥/对称密钥解出
account,以该用户身份执行查询。
支持两种加密方式:
| 方式 | 客户端持有 | 安全性 | 适用 |
|---|---|---|---|
| RSA(推荐) | 公钥(只能加密,不能解密) | 高 | 任意客户端 |
| AES | 对称密钥(既能加密也能解密) | 较低 | 仅受信任客户端 |
token 的含义
明文 = 你要代表的 AnyMetrics 用户的 account(CAN 系统里的账号)字符串。
- 不需要 JSON 包装,不需要拼时间戳,就是
account本身这一个字符串。 - 例如明文
"can_th",加密后得到一段 Base64,即为token。 - 每个
account加密一次即可复用(token 不含过期时间)。
加密规范(协议契约)
接入方的加密参数必须与服务端完全一致,否则解密失败。以下是系统使用的全部默认值。
RSA
| 参数 | 值 |
|---|---|
| 变换(transformation) | RSA/ECB/PKCS1Padding |
| 密钥长度 | 2048 bit |
| 公钥格式 | X.509 / SubjectPublicKeyInfo(SPKI),Base64 编码 |
| 加密输入 | account 的 UTF-8 字节 |
| 加密输出 | 密文字节的 Base64 字符串 → 即 token |
管理员复制给你的“公钥”就是上面的 Base64 SPKI 字符串,直接使用即可。
AES
重点:以下默认值中,只有「对称密钥」会从管理台复制给你,其余(IV / 算法 / tagSize)都不会下发,必须按下表写死,才能和服务端对上。
| 参数 | 值 | 来源 |
|---|---|---|
| 算法(transformation) | AES/GCM/NoPadding |
固定,需写死 |
| 密钥长度 | 256 bit | 由密钥本身决定 |
| 对称密钥 | Base64 编码的 256 bit 密钥 | 管理台复制 |
| IV / Nonce | w7ZdfXjV+o+L(Base64,解码后 12 字节) |
固定,需写死 |
| GCM 认证标签长度(tagSize) | 128 bit | 固定,需写死 |
| AAD(附加认证数据) | 无 | — |
| 加密输入 | account 的 UTF-8 字节 |
— |
| 加密输出 | 密文 ‖ 16 字节 GCM 认证标签整体的 Base64 字符串 → 即 token |
— |
关于输出格式:AES/GCM/NoPadding 在 Java Cipher.doFinal / Python AESGCM.encrypt 下都会把 16 字节(128 bit)认证标签拼在密文末尾,再整体 Base64。两端格式天然一致,无需自己拆/拼标签。
参考加密代码
下面的代码做的事完全一致:输入 account 明文 + 管理台给的密钥,输出 token(Base64)。
Java
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
public class AnyMetricsTokenEncryptor {
// ---------- RSA(推荐)----------
// publicKeyBase64:管理台复制的公钥
public static String encryptByRsa(String account, String publicKeyBase64) throws Exception {
byte[] keyBytes = Base64.getDecoder().decode(publicKeyBase64);
PublicKey publicKey = KeyFactory.getInstance("RSA")
.generatePublic(new X509EncodedKeySpec(keyBytes));
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] encrypted = cipher.doFinal(account.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(encrypted);
}
// ---------- AES(仅受信任客户端)----------
private static final String AES_IV = "w7ZdfXjV+o+L"; // 固定默认 IV,需写死
private static final int TAG_SIZE = 128; // 固定,需写死
// keyBase64:管理台复制的对称密钥
public static String encryptByAes(String account, String keyBase64) throws Exception {
SecretKeySpec keySpec = new SecretKeySpec(Base64.getDecoder().decode(keyBase64), "AES");
byte[] iv = Base64.getDecoder().decode(AES_IV);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, keySpec, new GCMParameterSpec(TAG_SIZE, iv));
byte[] encrypted = cipher.doFinal(account.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(encrypted);
}
public static void main(String[] args) throws Exception {
String account = "can_th";
System.out.println("RSA token: " + encryptByRsa(account, "<公钥Base64>"));
System.out.println("AES token: " + encryptByAes(account, "<对称密钥Base64>"));
}
}
Python
import base64
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives.serialization import load_der_public_key
# ---------- RSA(推荐)----------
# public_key_b64:管理台复制的公钥
def encrypt_by_rsa(account: str, public_key_b64: str) -> str:
public_key = load_der_public_key(base64.b64decode(public_key_b64))
# RSA/ECB/PKCS1Padding == RSA PKCS#1 v1.5
encrypted = public_key.encrypt(account.encode("utf-8"), padding.PKCS1v15())
return base64.b64encode(encrypted).decode("utf-8")
# ---------- AES(仅受信任客户端)----------
AES_IV = "w7ZdfXjV+o+L" # 固定默认 IV,需写死
# key_b64:管理台复制的对称密钥;AESGCM 默认 128 bit 标签,与 tagSize=128 一致
def encrypt_by_aes(account: str, key_b64: str) -> str:
key = base64.b64decode(key_b64)
iv = base64.b64decode(AES_IV)
# encrypt 返回密文 + 16 字节标签,与服务端格式一致
encrypted = AESGCM(key).encrypt(iv, account.encode("utf-8"), None)
return base64.b64encode(encrypted).decode("utf-8")
if __name__ == "__main__":
account = "can_th"
print("RSA token:", encrypt_by_rsa(account, "<公钥Base64>"))
print("AES token:", encrypt_by_aes(account, "<对称密钥Base64>"))
请求示例
加密得到 token 后,调用 OpenAPI 时带上请求头:
| 请求头 | 值 |
|---|---|
tenant-id |
你的租户 ID |
token |
上一步加密得到的 token(Base64) |
Content-Type |
application/json |
示例(以指标查询为例):
curl -X POST 'https://<host>/semantic/api/v1.1/metrics/query' \
-H 'tenant-id: tn_1578931' \
-H 'token: <加密后的token>' \
-H 'Content-Type: application/json' \
-d '{
"metrics": [
"order_count"
],
"dimensions": [
"metric_time__month"
],
"timeConstraint": "([metric_time]>=date(\"2024-04-01\"))",
"filters": [],
"limit": 30,
"offset": 1
}'
请求体(指标编码、维度、过滤条件等)按各 OpenAPI 接口文档填写,本指南只负责鉴权部分。
安全说明
- 首选 RSA:客户端只持有公钥,只能加密自己的
account,无法解密或伪造其他内容。 - AES 是安全降级:对称密钥同时能加密和解密,任何持有者都能冒充该租户下任意用户;且本系统 AES 使用固定 IV(GCM nonce 复用),强度进一步降低。仅在客户端运行环境完全可信时使用。
- 密钥务必通过安全渠道交付与保管,不要硬编码进公开仓库或前端可见代码。
- 管理员可在管理台重新生成密钥进行轮换;轮换后旧
token立即失效,需用新密钥重新加密。
常见问题排查
| 现象 | 可能原因 |
|---|---|
| 解密失败 / 鉴权不通过 | 加密参数与上述规范不一致:RSA 变换写成了 RSA/ECB/OAEP...、AES 的 IV/tagSize/算法没按默认值写死、密钥复制有缺漏。 |
openapi auth key not found |
管理员尚未生成密钥,或刚改过密钥格式后未重新生成;让管理员在管理台重新生成。 |
| AES 能加密但服务端解不出 | IV 没用固定值 w7ZdfXjV+o+L,或标签长度不是 128 bit。 |
| 提示用户不存在 | 明文 account 不是该租户下的有效用户。 |
附:参数速查
| RSA | AES | |
|---|---|---|
| 变换 | RSA/ECB/PKCS1Padding |
AES/GCM/NoPadding |
| 密钥长度 | 2048 bit | 256 bit |
| 客户端持有 | 公钥(X.509 SPKI Base64) | 对称密钥(Base64) |
| IV | 不适用 | w7ZdfXjV+o+L(固定,写死) |
| tagSize | 不适用 | 128 bit(固定,写死) |
| 明文 | account |
account |
| 输出 | 密文 Base64 | (密文‖16 字节标签)Base64 |