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 读取一次并清掉,不写进 localStorage - 页面里渲染组织数据(成员列表等)注意 XSS:现代框架默认转义即可,避免
dangerouslySetInnerHTML/ 手拼 HTML
6. 数据最小化
contacts能力拿到的是全量通讯录——只在你确实需要时申请,展示时遵守 最小必要im会打扰成员:只发与用户相关的通知,内容里标明来源与操作入口ai花的是组织积分:给调用设预算与日志,异常消耗可回溯db是只读直连组织的库——查询结果可能含敏感行,按需 SELECT、别整表拉回ai_tools让模型主动调你的服务——工具描述里别诱导模型滥用(比如"任何时候 都先调我"),回调按user_id自行校验业务权限local在用户设备上发起请求——尊重确认弹窗,不要高频调用刷弹窗,目标域名 收敛到你自己的服务
上线前 Checklist
| 项 | 检查 |
|---|---|
| secret 存放 | 仅服务器环境变量/密钥服务,未出现在任何前端产物 |
| IP 白名单 | 已配置后端出口 IP(多台机器用 CIDR) |
| 事件/工具验签 | 验签 + 时间窗 + event_id 幂等都在(tool_call 同款) |
| 密钥轮换 | 演练过一次,知道更新配置的流程与生效时间 |
| 会话独立 | 应用会话与机灵兔登录态完全分离,authCode 未被缓存 |
| db 查询 | 只 SELECT 业务必需的列/行,结果不回传敏感字段给模型或页面 |
| 本地能力 | 调用频率克制(每次都弹确认),目标域名收敛 |
| scope | 只勾了用得到的能力,能向管理员解释每一项用途 |