文档首页 / 嵌入签名与令牌

嵌入签名与令牌

第三方系统免登录接入的签名规则、载荷字段、HMAC-SHA256 计算方法,含 curl、Java、Python、Node.js 示例,以及签名换取会话令牌的过程。

概述

平台没有 OAuth、CAS、SAML 一类的标准单点登录协议。第三方系统接入的免登录方式是签名免登:第三方后端用 appSecret 对「应用 ID、账号、姓名、报表 ID、过期时间」做 HMAC-SHA256,把载荷和签名一起做 Base64Url 编码,得到一个签名串(参数名 _s)。平台收到签名串后验签,自动查找或创建对应的外部用户,再放行访问。

签名是在第三方服务端本地计算的,不需要调用平台接口去申请。使用前先在 第三方应用管理 中注册应用获取 appId 与 appSecret。

签名规则

项 规则
待签名内容 appId|account|userName|reportId|expireAt,五段用竖线 | 连接;reportId 为空时该段为空字符串
签名算法 HMAC-SHA256(待签名内容, appSecret),字符串按 UTF-8,输出小写十六进制
载荷 JSON 字段见下表,序列化后按 UTF-8 做 Base64Url 编码,不带填充 =
使用位置 查询参数 _s,或接口请求体中的 signature 字段

载荷字段:

字段 类型 说明
appId 字符串 应用 ID
account 字符串 第三方系统中的用户账号,平台据此生成内部账号 ext__{appId}__{account}
userName 字符串 用户姓名,用于显示;应始终提供
reportId 字符串 限定的报表 ID,可为空
expireAt 整数 过期时间,Unix 时间戳,单位秒
signature 字符串 上述十六进制签名值

验签失败或不满足条件时,平台返回的提示:

提示 原因
签名格式无效 不是合法的 Base64Url 或 JSON
签名已过期 expireAt 早于当前时间
应用不存在: xxx appId 未注册
应用已禁用 应用状态为「禁用」
签名验证失败 签名值不匹配,通常是密钥、字段顺序或字符编码不一致
无权访问该报表 应用配置了报表白名单,且不含该 reportId
用户已被停用 该外部用户在用户管理中被停用

reportId 的绑定方式

场景 reportId 取值
嵌入查看报表 推荐绑定具体报表 ID
获取报表目录、标签、参数、上传外部数据集 通常留空;配置了报表白名单时结果自动限定在白名单内
后端直接导出 必须与请求参数 fileId 完全一致,不可为空

操作步骤

用 curl 生成签名并换取会话令牌

以下脚本依赖 openssl,可在 Linux 与 macOS 终端运行。

BASE=https://your-domain          # 平台访问地址
APP_ID=erp-system
SECRET='在三方集成页面保存的 appSecret'
ACCOUNT=zhangsan
NAME='张三'
REPORT=rpt_sales
EXPIRE=$(( $(date +%s) + 600 ))   # 10 分钟后过期,单位秒

SIGN=$(printf '%s' "$APP_ID|$ACCOUNT|$NAME|$REPORT|$EXPIRE" \
  | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')

PAYLOAD=$(printf '{"appId":"%s","account":"%s","userName":"%s","reportId":"%s","expireAt":%s,"signature":"%s"}' \
  "$APP_ID" "$ACCOUNT" "$NAME" "$REPORT" "$EXPIRE" "$SIGN")

# Base64Url,无填充
S=$(printf '%s' "$PAYLOAD" | base64 | tr -d '\n=' | tr '+/' '-_')

# 用签名换取嵌入会话
curl -s -X POST "$BASE/api/embed/auth" \
  -H 'Content-Type: application/json' \
  -d "{\"signature\":\"$S\",\"reportId\":\"$REPORT\"}"

成功响应(code 为 200):

{
  "code": 200,
  "success": true,
  "message": "...",
  "data": {
    "sessionToken": "embed_3f2c...",
    "userId": "b1c9...",
    "userName": "张三",
    "externalAccount": "ext__erp-system__zhangsan",
    "appId": "erp-system",
    "reportId": "rpt_sales",
    "expireAt": 1790658966000
  }
}

expireAt 是会话过期时间,毫秒时间戳,取「签名过期时间」与「当前时间加应用最大有效期」中较早者。之后调用需要嵌入会话的接口时,带上请求头 X-Embed-Session: <sessionToken>:

curl -s "$BASE/api/embed/session/validate" -H "X-Embed-Session: embed_3f2c..."

Java

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

public static String buildSignature(String appId, String secret, String account,
                                    String userName, String reportId, int expireMinutes) throws Exception {
    long expireAt = System.currentTimeMillis() / 1000 + expireMinutes * 60L;
    String content = String.join("|", appId, account, userName,
            reportId != null ? reportId : "", String.valueOf(expireAt));
    Mac mac = Mac.getInstance("HmacSHA256");
    mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
    StringBuilder hex = new StringBuilder();
    for (byte b : mac.doFinal(content.getBytes(StandardCharsets.UTF_8))) hex.append(String.format("%02x", b));
    String payload = String.format(
        "{\"appId\":\"%s\",\"account\":\"%s\",\"userName\":\"%s\",\"reportId\":\"%s\",\"expireAt\":%d,\"signature\":\"%s\"}",
        appId, account, userName, reportId != null ? reportId : "", expireAt, hex);
    return Base64.getUrlEncoder().withoutPadding().encodeToString(payload.getBytes(StandardCharsets.UTF_8));
}

Python

import base64, hashlib, hmac, json, time

def build_signature(app_id, secret, account, user_name, report_id="", expire_minutes=10):
    expire_at = int(time.time()) + expire_minutes * 60
    content = "|".join([app_id, account, user_name, report_id, str(expire_at)])
    sig = hmac.new(secret.encode(), content.encode(), hashlib.sha256).hexdigest()
    payload = json.dumps({
        "appId": app_id, "account": account, "userName": user_name,
        "reportId": report_id, "expireAt": expire_at, "signature": sig,
    }, ensure_ascii=False)
    return base64.urlsafe_b64encode(payload.encode()).rstrip(b"=").decode()

Node.js

const crypto = require('crypto');

function buildSignature(appId, secret, account, userName, reportId = '', expireMinutes = 10) {
  const expireAt = Math.floor(Date.now() / 1000) + expireMinutes * 60;
  const content = [appId, account, userName, reportId, expireAt].join('|');
  const signature = crypto.createHmac('sha256', secret).update(content).digest('hex');
  const payload = JSON.stringify({ appId, account, userName, reportId, expireAt, signature });
  return Buffer.from(payload).toString('base64url');
}

注意事项

注意:appSecret 只能在第三方服务端使用;每次打开报表都应实时生成新的签名,不要长期缓存或写进静态页面。

注意:平台对 /api/embed/** 按客户端 IP 限流,每个 IP 每分钟最多 120 次请求,超过返回 429(请求频率超限,请稍后再试)。

提示:签名有效期只需覆盖「打开页面」这一步,有效期建议 5 到 15 分钟。会话建立后,嵌入页遇到会话过期会自动用地址栏中的签名重新认证,此时签名本身也必须还在有效期内。

提示:expireAt 是秒,会话的 expireAt 是毫秒,两者单位不同。

相关文档

联系我们

请填写您的信息,我们将在 1 个工作日内与您联系。