跳转至

OpenAPI 加密鉴权接入指南

本文档面向调用 AnyMetrics OpenAPI 的接入方(客户端),说明如何对请求进行加密鉴权。核心动作只有一个:把你要代表的用户账号 account 加密成 token,随请求头发送

总览

AnyMetrics OpenAPI 的加密鉴权流程:

  1. 管理员在 AnyMetrics 管理台「企业安全 → OPEN API 鉴权」开启功能并生成密钥,复制出公钥(RSA)或对称密钥(AES)交给接入方。
  2. 接入方用拿到的密钥,把目标用户的 account(明文)加密成 token
  3. 调用 OpenAPI 时带上两个请求头:tenant-idtoken
  4. 服务端用对应私钥/对称密钥解出 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