控制台嵌入客户后台

控制台嵌入(K29)让客户在自己的运营后台里以 iframe 只读视图展示当前应用的概览、统计、用户与群组,而不必把管理员密钥交给宿主页面。链路是:管理员在控制台「嵌入代码」页签发一次性 token → 宿主把 token 拼进 /embed?token=… → 嵌入视图用公开端点兑换 token 并渲染作用域内数据 → 通过 postMessage 上报就绪、高度与事件。

概念​

对象说明
嵌入视图 /embed无侧边栏、无写操作的精简页面;只读展示 token 作用域内的数据,不携带 X-Admin-Secret。
一次性 token64 位 hex;首次兑换成功后记录 usedAt,重复兑换 401;过期兑换时移除;按应用最多 200 枚存活。
作用域 scopesoverview(应用概览)、stats(近 7 天趋势)、users(最近 20 个用户)、groups(最近 20 个群);缺省 overview + stats。
有效期 ttlSec30–86400 秒,缺省 900 秒(15 分钟)。
兑换端点公开 GET /api/embed/session?token=…;缺失/非法/已用/过期统一 401(客户端按 1002 语义处理)。
宿主通信im-embed:ready / im-embed:resize / im-embed:event 三类 postMessage。
安全边界(务必阅读)

当前实现由嵌入视图向 window.parent 发送只读展示数据时使用 targetOrigin="*":宿主来源在服务端未知。生产接入必须在宿主侧校验 event.origin,并建议通过固定域名反向代理 + CSP frame-ancestors 限定可嵌入方。

token 是短期一次性的展示凭据,不是管理员凭据:不要写入日志、URL 记录或分享链接;预览一次即消耗。

token 原文保存在服务端状态中(随快照持久化,未做摘要化存储);未过期 token 在重启后仍可兑换。对安全要求更高的部署应改为哈希索引。

前置条件​

  • 服务端已启动且 Admin API 可达:curl -sS http://127.0.0.1:8080/health 返回 {"ok":true}。
  • 已在控制台创建应用,并持有可签发 token 的管理员凭据(X-Admin-Secret,或具备 admin 及以上角色的子账号)。
  • 宿主页面与嵌入视图同源或已配置允许的 iframe 嵌入策略;嵌入视图只读,不需要客户端 SDK。
  • 如需把 iframe 放进 HTTPS 页面,嵌入视图也必须通过 HTTPS 提供,避免混合内容拦截。

步骤​

  1. 签发一次性嵌入 token

    调用 POST /api/admin/apps/{appkey}/embed/token,按需传 scopes 与 ttlSec。响应中的 data.embedUrl 已带 token,data.sessionPath 是对应的兑换路径。

    控制台入口:/console/#embed,勾选作用域后点「生成 token」;页面会给出 iframe 片段与「预览」(预览会消耗 token)。

  2. 把 token 放进 iframe 地址

    把 data.embedUrl 拼到服务端 origin 后作为 src;不要用 srcdoc,也不要经过会记录 URL 的第三方跳转。

  3. 宿主监听 postMessage

    宿主监听 message 事件,按 event.data.type 分发;先校验 event.origin 是否等于嵌入视图的 origin,再读取 payload。

    收到 im-embed:resize 时更新 iframe 高度;收到 im-embed:ready 时隐藏宿主骨架屏;收到 im-embed:event 的 error 时展示可重试提示。

  4. 处理过期与一次性

    token 过期或已被预览消耗后,兑换返回 401;宿主应重新调用签发接口换新 token,而不是复用旧 URL。

    建议每次打开嵌入页都新签一枚 token,TTL 只覆盖本次查看时长。

  5. 收窄权限并上线

    只勾选业务真正需要的 scope;例如客服工作台只需要 users,不要默认签发 overview + stats 之外的敏感面。

    在反向代理层限制 /embed 与 /api/embed/session 的可访问来源,并在宿主侧校验 origin。

参数表​

签发 token​

参数类型必填说明
scopesstring[]否子集:overview / stats / users / groups;缺省 overview+stats;非法值 4xx(1001)。
ttlSecuint32否30–86400,缺省 900;越界 4xx(1001)。

响应字段​

字段说明
token64 位 hex,仅本次返回。
scopes / ttlSec实际生效的作用域与有效期。
createdAt / expiresAt毫秒时间戳。
oneTime / used恒为 true / 签发时为 false。
sessionPath/api/embed/session?token=…。
embedUrl{origin}/embed?token=…。
securityNotes服务端返回的安全提示数组(一次性、短期、作用域、origin 校验)。

postMessage 消息​

type载荷触发时机
im-embed:ready{ appkey, scopes }兑换成功且首屏渲染完成。
im-embed:resize{ height }初始化、内容渲染、窗口 resize 后。
im-embed:event{ name, detail }ready(附 appkey/scopes)或 error(附 message)。

示例​

签发 token(管理员)​

Shell
curl -sS -X POST "http://127.0.0.1:8080/api/admin/apps/demo/embed/token" \
  -H 'X-Admin-Secret: dev-admin' \
  -H 'content-type: application/json' \
  -d '{"scopes":["overview","stats","users"],"ttlSec":900}'

# 响应(节选)
# {"code":0,"data":{"token":"<64hex>","scopes":["overview","stats","users"],
#   "sessionPath":"/api/embed/session?token=<64hex>",
#   "embedUrl":"http://127.0.0.1:8080/embed?token=<64hex>",
#   "expiresAt":1750000000000,"oneTime":true,"used":false}}

兑换 token(公开端点,不需要管理密钥)​

Shell
curl -sS "http://127.0.0.1:8080/api/embed/session?token=<64hex>"

# 首次兑换:200 + data.view
# 再次兑换 / 过期 / 非法:401(code=1002)
curl -sS -i "http://127.0.0.1:8080/api/embed/session?token=<64hex>" | head -n 1

宿主页面:iframe + postMessage​

Text
<!-- 1) 服务端签发 token 后,把 embedUrl 填进 src -->
<iframe
  id="im-embed"
  src="http://127.0.0.1:8080/embed?token=<64hex>"
  width="100%"
  height="640"
  style="border:1px solid #e5e7eb;border-radius:12px"
  title="IM 控制台嵌入视图"
  referrerpolicy="no-referrer"
></iframe>

<script>
  const frame = document.getElementById('im-embed');
  // 只接受嵌入视图 origin 的消息(生产必须校验,不要用 '*' 判断)
  const EMBED_ORIGIN = 'http://127.0.0.1:8080';

  window.addEventListener('message', (event) => {
    if (event.origin !== EMBED_ORIGIN) return;
    const data = event.data || {};
    switch (data.type) {
      case 'im-embed:ready':
        console.log('embed ready', data.appkey, data.scopes);
        break;
      case 'im-embed:resize':
        frame.style.height = Math.max(320, Number(data.height) || 0) + 'px';
        break;
      case 'im-embed:event':
        if (data.name === 'error') console.warn('embed error', data.detail);
        break;
      default:
        break;
    }
  });
</script>

主动刷新:重新签发并替换 src​

JavaScript
async function refreshEmbed(appkey, adminSecret) {
  const res = await fetch('/api/admin/apps/' + encodeURIComponent(appkey) + '/embed/token', {
    method: 'POST',
    headers: {
      'content-type': 'application/json',
      'X-Admin-Secret': adminSecret,
    },
    body: JSON.stringify({ scopes: ['overview', 'stats'], ttlSec: 600 }),
  });
  const json = await res.json();
  if (json.code !== 0) throw new Error(json.msg || 'token 签发失败');
  document.getElementById('im-embed').src = json.data.embedUrl;
}
说明

上例的 fetch 演示的是「由应用服务器代签发」的形态。浏览器端不应持有 X-Admin-Secret;生产环境应把签发请求放在应用服务器,只把短 TTL、窄 scope 的 token 下发给宿主页面。

常见问题​

预览一次后再打开就 401?​

token 是一次性的,预览已经兑换并消耗。请重新签发;不要把同一 URL 同时用于预览和正式嵌入。

iframe 空白但接口 200?​

检查宿主是否因 X-Frame-Options / CSP frame-ancestors 拒绝嵌入;再检查 HTTPS 页面是否嵌入了 HTTP 视图(混合内容)。

能不能嵌到任意第三方站点?​

技术上 iframe 不受限,但必须由宿主校验 event.origin,并在网关侧限制可访问来源;当前视图使用 targetOrigin="*" 发送只读数据,生产化收窄已登记为后续项。

token 能当长期凭据用吗?​

不能。最长 86400 秒、兑换即失效、只读且受 scope 限制;它不是管理员密钥,也不能调用任何写接口。

为什么 users/groups 只有 20 条?​

嵌入视图定位为概览展示,按字典序取前 20 条;完整列表请在控制台「用户管理 / 群组管理」中查看。

应用被删除/停用后嵌入页还能用吗?​

不能。兑换时校验应用存在且未停用;已签发的 token 会返回 401。

相关链接​