Appearance
安全最佳实践
你的应用运行在别人的客户端里、调用的是组织的资源——安全不是可选项。这一页 是接入前后的对照清单,按重要性排序。
1. app_secret 只放服务器
app_key/app_secret 等价于应用后端的密码:
- ✅ 存在服务器环境变量 / 密钥管理服务里
- ❌ 不进前端代码、不进页面 JS、不进 git 仓库、不打进日志
- 页面只需要
jlt.js与免登码,永远不需要接触 secret
怀疑泄露?立即在应用设置里轮换密钥——旧 secret 与它签发的所有 token 即时失效(token 有效期最长 2 小时,轮换不等待)。
2. 配置服务器 IP 白名单
白名单是 secret 泄露的兜底防线:非空时 所有 /api/open/*(含换 token)只接受你后端出口 IP(精确 IP 或 CIDR)。 来源判定以反向代理记录的真实地址为准,伪造 X-Forwarded-For 无效。
3. 事件与工具回调必须验签
事件推送(X-Jlt-Event: org.member.* 等)和 AI 工具调用 (X-Jlt-Event: tool_call)走同一套签名协议,同一份验签代码两处复用。 不要"反正内网就跳过验签"——验签同时防伪造与防重放:
js
import crypto from "node:crypto";
const ts = req.header("X-Jlt-Timestamp") || "";
const sig = req.header("X-Jlt-Signature") || "";
// 1) 用原始请求体字节(不要 re-serialize 过的 JSON)
const expected = crypto
.createHmac("sha256", APP_SECRET)
.update(ts + "." + rawBody.toString("utf8"))
.digest("hex");
// 2) 常量时间比较 + 时间窗
if (
!sig ||
!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)) ||
Math.abs(Date.now() / 1000 - Number(ts)) > 3600
) {
return res.status(401).end();
}再按 X-Jlt-Event-Id 做幂等(重试会重发同一 id)。
4. authCode 的纪律
- 一次性 + 5 分钟:用后即弃,绝不缓存复用
- 换到的身份建你自己的会话(cookie/JWT),不要每次请求都现换
org_role只来自平台返回,不要让页面上报"我是管理员"
5. 页面侧
- 传输必须是 https(仅 localhost 调试放行 http)
- 只通过
jlt.js与容器通信;不自己仿写 postMessage 协议 - 免登参数(
jlt_auth_code等)只从 URL fragment(旧版壳为 query)读取一次并 清掉,不写进 localStorage - 页面里渲染组织数据(成员列表等)注意 XSS:现代框架默认转义即可,避免
dangerouslySetInnerHTML/ 手拼 HTML
6. 数据最小化
contacts能力拿到的是全量通讯录——只在你确实需要时申请,展示时遵守 最小必要im会打扰成员:只发与用户相关的通知,内容里标明来源与操作入口ai花的是组织积分:给调用设预算与日志,异常消耗可回溯db是只读直连组织的库——查询结果可能含敏感行,按需 SELECT、别整表拉回ai_tools让模型主动调你的服务——工具描述里别诱导模型滥用(比如"任何时候 都先调我"),回调按user_id自行校验业务权限local在用户设备上发起请求——尊重确认弹窗,不要高频调用刷弹窗,目标域名 收敛到你自己的服务ai+local同时授予会解锁jlt.agent——这是开放能力里爆炸半径最大的一项: 模型能用用户已启用的全部技能/系统工具(文件/Shell/代码/本地数据库),不要因为 平台每轮都弹确认就递还给用户自己判断——你自己的页面也应该在发起agent调用前 说清楚这一轮要让它做什么,别拼一个模糊的 prompt 让用户无从预测后果;不需要完整 Agent 能力时优先用localFetch/browserExtract/db这类精确能力- 隔离浏览器会话(
browserOpen…)不带用户登录态,但仍会在用户电脑上多步交互未确认 的网站——用完及时browserClose(),不要长时间占着会话不关(会话数有全局上限, 占用会影响其它应用)
7. 本机文件访问与分享(v1.6.0)
chooseFile/saveFileAs/share 都不需要申请任何 scope——许可边界是 用户在系统原生对话框/选人组件里的那次明确操作,不是我们发的权限。但这不 代表可以随便调用:
chooseFile/saveFileAs会弹出真实的系统对话框——不要在用户没有发起 相关操作(比如没点"导入文件"按钮)时主动调用,那样体验上等同于突然弹 一个文件选择框,用户会困惑甚至误触。share发出的消息是以用户自己的身份发给他选的同事——内容要跟用户 当前正在做的事强相关(比如分享正在协作的文档链接),不要塞入营销/推广 文案;这类消息对收件人而言看起来就是"同事发的一句话",滥用会破坏用户 对你应用、乃至对同事的信任。share仍然需要im能力(即使发起方是用户本人)——跟POST /im/send共用同一道闸门,见能力(scope)说明。
上线前 Checklist
| 项 | 检查 |
|---|---|
| secret 存放 | 仅服务器环境变量/密钥服务,未出现在任何前端产物 |
| IP 白名单 | 已配置后端出口 IP(多台机器用 CIDR) |
| 事件/工具验签 | 验签 + 时间窗 + event_id 幂等都在(tool_call 同款) |
| 密钥轮换 | 演练过一次,知道更新配置的流程与生效时间 |
| 会话独立 | 应用会话与机灵兔登录态完全分离,authCode 未被缓存 |
| db 查询 | 只 SELECT 业务必需的列/行,结果不回传敏感字段给模型或页面 |
| 本地能力 | 调用频率克制(每次都弹确认),目标域名收敛 |
| agent/浏览器会话 | 不滥用 ai+local 组合权限;会话用完即时 browserClose() |
| 本机文件/分享 | chooseFile/saveFileAs 只在用户主动操作时调用;share 内容与当前操作相关,不夹带营销文案 |
| scope | 只勾了用得到的能力;若要发布到应用市场,已为每个 scope 填写用途说明并通过平台审核(见能力(scope)说明) |