嵌入签名与令牌
第三方系统免登录接入的签名规则、载荷字段、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是毫秒,两者单位不同。