跨平台统一 API 与 CORS 白名单
跨平台统一 API 与 CORS 白名单
学习目标
理解 Khoj 如何通过单一 FastAPI 后端服务五种不同平台的客户端,以及 CORS 白名单和认证策略的设计。
项目实践
五种客户端统一接入
Khoj 的客户端遍布五个平台,但全部通过同一个 REST API + WebSocket 接入:
| 客户端 | 技术 | 通信协议 | Origin |
|---|---|---|---|
| Web | Next.js SSG | HTTP + WebSocket | 同源(无 CORS 限制) |
| Desktop | Electron + ToDesktop | HTTP + WebSocket | app:// 协议 |
| Obsidian | TypeScript 插件 | HTTP + WebSocket | app://obsidian.md |
| Emacs | Elisp (khoj.el) | HTTP | 无浏览器 CORS 限制 |
| Android | TWA (PWA 包装) | HTTP + WebSocket | 同源 |
CORS 白名单配置
Khoj 在 main.py 中通过 CORSMiddleware 配置白名单:
app.add_middleware( CORSMiddleware, allow_origins=[ "app://obsidian.md", # Obsidian 插件 "capacitor://localhost", # Obsidian mobile "app://khoj.desktop", # Desktop 客户端 KHOJ_DOMAIN, # 自部署域名 "http://localhost:42110", # 本地开发 ], allow_credentials=True, allow_methods=["*"], allow_headers=["*"],)关键设计:通过 KHOJ_DOMAIN 环境变量支持自部署场景。管理员部署到自己的域名时,只需设置这个变量即可自动将该域名加入白名单,无需修改代码。
认证方式多样化
不同客户端需要不同的认证方式:
| 认证方式 | 适用场景 | 实现 |
|---|---|---|
| Google OAuth | Web 浏览器登录 | authlib Starlette client |
| Magic Link | 无密码登录 | Resend API 发送验证邮件 |
| API Token | 客户端认证 | KhojApiUser 模型,唯一 token |
| 电话号 | WhatsApp 客户端 | Twilio 集成 |
| 匿名模式 | 本地自部署 | --anonymous-mode 跳过认证 |
KhojApiUser 模型用于客户端认证:
class KhojApiUser(models.Model): user = models.ForeignKey(KhojUser, on_delete=models.CASCADE) token = models.CharField(max_length=50, unique=True) name = models.CharField(max_length=50) accessed_at = models.DateTimeField(null=True)用户在 Web 界面生成 API token,然后在 Obsidian 插件或 Emacs 配置中使用该 token 认证。
WebSocket 连接管理
WebSocketConnectionManager 管理每个用户的 WebSocket 连接:
每用户最大连接数: 5-10连接标识: user_id + connection_id断连处理: 自动从字典中移除当同一用户建立新连接时,旧的连接会被标记为断开——这确保了用户从不同客户端打开对话时不会出现消息串扰。
问题与规避
| 问题 | 影响 | 规避策略 |
|---|---|---|
| 新客户端 origin 未加入白名单 | CORS 预检失败 | 通过 KHOJ_DOMAIN 环境变量动态配置 |
| API Token 泄露 | 他人可冒充用户 | token 可单独撤销;accessed_at 追踪最后使用时间 |
| 匿名模式安全风险 | 无认证即可使用 | 仅建议本地自部署使用;公开部署必须启用认证 |
| 多客户端同时连接 | 消息可能发送到错误客户端 | 每用户独立连接管理,消息通过 user_id 路由 |
设计取舍
为什么不使用 GraphQL?
REST + WebSocket 的组合已经足够覆盖 Khoj 的所有交互模式:文件上传(REST multipart)、实时聊天(WebSocket)、设置管理(REST JSON)。GraphQL 的优势在于灵活的数据查询,但 Khoj 的 API 端点相对固定,不需要客户端自由组合查询字段。
为什么 Obsidian 插件使用 app:// 协议而非本地 HTTP?
Obsidian 基于 Electron 运行,其 fetch 请求的 origin 是 app://obsidian.md 而非 http://localhost。这是 Electron 的安全模型决定的——应用内请求的 origin 是应用协议而非本地回环地址。