跳转到内容

跨平台统一 API 与 CORS 白名单

跨平台统一 API 与 CORS 白名单

学习目标

理解 Khoj 如何通过单一 FastAPI 后端服务五种不同平台的客户端,以及 CORS 白名单和认证策略的设计。

项目实践

五种客户端统一接入

Khoj 的客户端遍布五个平台,但全部通过同一个 REST API + WebSocket 接入:

客户端技术通信协议Origin
WebNext.js SSGHTTP + WebSocket同源(无 CORS 限制)
DesktopElectron + ToDesktopHTTP + WebSocketapp:// 协议
ObsidianTypeScript 插件HTTP + WebSocketapp://obsidian.md
EmacsElisp (khoj.el)HTTP无浏览器 CORS 限制
AndroidTWA (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 OAuthWeb 浏览器登录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 是应用协议而非本地回环地址。

参考来源