可嵌入聊天 widget:架构与安全
Simmetric Chat widget 背后的两步 loader 与沙箱 iframe,以及隔离会话、速率限制和分层知识库访问。
你的知识库就是一个产品表面
你团队写下的文档--产品文档、支持文章、内部 wiki--回答的正是你网站访客已经在问的问题。嵌入的聊天 widget 把这份知识库变成一扇对话式入口:访客用自己的话提问,回答带着引用流出,扎根于你选择开放的 workspace。
困难的部分不是聊天本身,而是把近乎不可信的软件嵌入别人的页面,同时不让自己变成攻击面。Simmetric Chat 的 widget 系统--Enterprise 层级--正是围绕这一约束构建的。本文走遍其架构:两步 embed、沙箱运行时、会话隔离、速率限制,以及决定访客提问能触及什么、不能触及什么的分层知识库访问模型。
两步 embed:loader 与沙箱 iframe
widget 分两步交付,这种拆分是刻意的:
- Loader 脚本。 在你的网站上添加一个 script 标签,指向 widget 服务并带上你的 widget ID。loader 是一个小型 JavaScript 文件,在宿主页面中创建一个 iframe。
- iframe 面板。 iframe 加载一个独立的 Preact 应用--以 IIFE bundle 形式编译,CSS 内联--在它自己的文档内渲染浮动按钮、聊天面板和所有交互。
<script src="https://your-simmetric-chat.com/widget/loader.js?widgetId=WIDGET_ID"></script>为什么不把聊天界面直接注入宿主页面?因为把不可信内容的字符串插值进宿主页面正是经典的 XSS 路径--而 widget 的整个内容流都近乎不可信:包含模型输出、文档摘录和访客输入。让界面运行在不透明来源(opaque origin)的沙箱 iframe 中,彻底消除了这一类风险:聊天内容永远不会在你的页面上下文中执行,widget 样式也无法与你的样式冲突(反之亦然)。
旧的字符串插值 embed 路径正是因此从产品中移除--两步模式不是众多选项之一,它就是 embed 本身。widget 的 IIFE 编译出于同样的原因:面板是一个独立的单文件,没有模块系统,运行时也不加载外部资源,这让 iframe 的网络表面保持可预测。
什么在哪里运行
widget 触及三个进程,理解边界地图就能解释后面每一条安全属性:
- 主服务器拥有 widget 的 CRUD 与 branding、workspace 关联、widget 服务调用的内部 API,以及管理端的 lead 和 analytics 端点。整个内部 widget 路由都受该功能约束--widget 功能关闭时,它在每条路由上都返回
402。 - widget 服务是公开的 embed 表面:JS loader、iframe HTML、会话创建、缓存配置的下发、到主服务器的聊天 SSE 代理,以及 lead 端点。它用专用 API key 向主服务器认证,并自带速率限制器。
- iframe是 Preact 客户端:会话引导、SSE 流式输出、引用展示、建议问题、同意门槛和 lead 卡片。
聊天代理转发的内部流式 API 事件格式完全一致--状态消息、计划公告、流式 token、引用和终止事件 done/error--因此访客体验到的就是你的团队内部使用的同一条管线,只是带着 widget 的权限。
匿名访客的隔离会话
访客从不认证;widget 代他们铸造身份。面板打开时,它向 widget 服务请求一个会话,widget 服务再请主服务器创建一个:256 位十六进制 token,服务端生成,24 小时过期。widget 随后的每个请求都针对该会话验证--不是来自你用户应用的 JWT,不是跨源 cookie,不是任何形式的环境凭证。
会话隔离让一个访客的状态永远不会触及另一个:对话是匿名的,计数器和速率限制状态挂在会话 token 上而不是 IP 或指纹上,宿主页面永远看不到 token--它存放在 iframe 自己的 storage 中,通过一个小型版本化 postMessage 协议与宿主交换,双方都会针对各自期望的精确 contentWindow 校验,因此其他脚本伪造的消息会被忽略。
配置下发遵循同样的纪律。widget 配置--branding、欢迎语、建议问题--通过 iframe 自身文档中的 JSON 块在服务端注入下发,而不是通过访客可以篡改的 URL 参数。访客无法影响服务器不会自行解析的任何东西:最近的一项新增进一步确认了这一点,允许每个 widget 的配置选择为其聊天回答服务的 LLM provider 和模型,在每次聊天请求时于服务端解析--永远不由客户端提供。
速率限制作为公开表面的契约
widget 是你基础设施上的公开端点,所以滥用控制是架构的一部分,而不是事后补丁。限制按表面分层:
- 每会话聊天限制: 每会话每小时 20 条消息、每天 5 段对话;超限时返回
429并附带retryAfter提示,widget 会显示速率限制警告而不是静默失败。 - 按 key 的 API 限制器: widget 服务对主服务器的调用按哈希后的 API key 限流,因此一个故障部署无法耗尽共享表面。
- Lead 限制器: lead capture 端点是最严的门槛,每小时 3 次请求,因为公开表单正是自动化滥用集中之处。
- CORS fail-closed: 每个请求的
Origin头都会针对该 widget 自己的允许来源列表校验,该列表可在管理面板编辑。不允许的 preflight 请求会被拒绝--如果来源检查无法访问数据库,访问被拒绝而不是放行。
结果是一个公开聊天表面,在负载或滥用下优雅降级,而不是崩溃或把所有人拒之门外。
分层知识库访问
widget 应当只从你打算公开的知识中回答--不多不少。访问分层由结构强制而非依赖 prompt:
- Workspace 白名单。 每个 widget 关联一个或多个 workspace,白名单在每次更新时原子替换。widget 可汲取的知识恰好是关联 workspace 的向量集合与文档之并集--支持 widget 可以看到你的公开文档 workspace,却永远不触碰内部项目数据。
- 功能约束。 创建 widget 需要 widget 功能 flag,并遵守套餐的 widget 数量上限,因此表面不会无声增长。
- 引用即边界。 回答携带的引用只指向被允许的 workspace--访客可以核实回答来源,且引用轨迹不可能指向白名单之外。
Branding--主色、机器人名称、logo、位置--是渲染在 iframe 内、通过 CSS 自定义属性实现的按 widget 配置,因此咨询公司可以把助手呈现为客户自己的。Community 层级部署在字段为空时会获得合理的默认值;完整定制和 white-labeling 是 enterprise flag。
安全检查清单,汇总
上面每一层存在,都是因为 widget 是你送进别人信任边界的软件:
- 两步 loader + 不透明来源的沙箱 iframe--杜绝字符串插值 XSS
- 服务端注入配置--安全相关的一切都不由客户端提供
- 服务端解析的 model pin--回答所用的模型无法被访客浏览器篡改
- 256 位匿名会话,24 小时过期--无任何环境凭证
- 分层速率限制,
429+retryAfter--滥用下优雅降级 - 动态 fail-closed CORS--来源信任只在唯一一处决定
- 原子 workspace 白名单--知识访问是结构性的,不是寄托于 prompt
从管理面板到可用的代码片段只需几分钟:创建 widget、白名单 workspace、添加来源、粘贴 script 标签。完整的集成指南覆盖 branding、simmetric:* postMessage 协议和端到端 lead capture。


