diff --git a/docs/superpowers/specs/2026-10-19-shared-domain-routing-design.md b/docs/superpowers/specs/2026-10-19-shared-domain-routing-design.md new file mode 100644 index 0000000..9d7f8a3 --- /dev/null +++ b/docs/superpowers/specs/2026-10-19-shared-domain-routing-design.md @@ -0,0 +1,90 @@ +# 单域名多租户(路径前缀路由)+ 上线收口项 设计 + +- 日期:2026-10-19 +- 状态:待评审 +- 范围:后端路由层 + 静态面前缀自推导 + 平台管理面小改 + 登录限流 XFF + limiter 清扫 +- 前置:配置中心优化已合入 main(6d6d8a8) + +## 1. 背景与目标 + +现状租户按域名(Host)解析,一租户至少一个域名——给大量网站挂客服时 DNS/证书/反代条目线性增长,不适合运营。目标:**一个共享域名承载任意多租户**,租户标识在 URL 路径中;Host 模式原样保留,两种共存。 + +上线收口项(随本 spec 一并实施): +1. **登录限流 XFF**:反代形态下 `client.host` 恒为 127.0.0.1,登录限流键退化为纯用户名,多 IP 爆破同一账号不受 IP 维度限制——解析 XFF 首段入键; +2. **limiter 清扫**(日志/内存上限铁律的进程内面):登录限流器与聊天限流桶的过期键永不回收,周期清扫。 + +## 2. 方案总览 + +``` +Host 模式(不动):chat.xxx.com → registry.resolve_site(host) +路径模式(新增): kefu.xxx.com/t//... → 按 slug 解析 → 剥前缀 → 下游路由零改动 +``` + +嵌入代码形态(脚本地址即租户标识,无需新参数): + +```html + +``` + +## 3. 路由层设计(TenantHostMiddleware 扩展) + +优先级:**平台豁免 → 路径前缀解析 → Host 解析**。 + +1. **slug 形态校验先行**:前缀段须 `^[a-z0-9-]+$`(与租户创建 `SLUG_RE` 一致,tenant_routes.py:65 同款正则常量提取共用);不匹配(含 `..`、编码畸形、大小写不符)直接走 Host 解析(共享域名下即 404),**不做任何剥离**。 +2. **路径改写**:匹配 `/t//` 或精确 `/t/`(后者无尾斜杠 → 不剥离,走 Host 解析 → 共享域名 404,语义记录在案);剥离时同步改写 `scope["path"]` 与 `scope["raw_path"]`(bytes,存在才改);http 与 websocket 两类 scope 同构处理。 +3. **orig_path 留痕**:原始 path 存 `scope["state"]["orig_path"]`(排障/日志对账用;uvicorn 访问日志显示剥离后路径为已知性状)。 +4. **解析与错误语义**:slug → `registry._by_slug` 查租户(缺停用判断与 resolve_site 同款:未知 slug → http 404 / ws close(1008);停用 → 503 暂停页 / ws close(1013));站点 = 租户**首个站点**为默认(`?site=` 覆盖链路既有),`state["site"]` 同样写入。 +5. **平台豁免优先级在前**:`/health`、`/api/platform/`、`/admin` 不受影响;`/t/x/api/platform/...` 剥离后命中平台路由属 by design(认证守护,与未知 Host 下 admin 面可达同级)。 +6. 已知边界(记录不修):StaticFiles 目录 308 的 Location 不带前缀(挂件/页面只引用具体文件,不触发)。 + +## 4. 静态面前缀自推导 + +- **widget.js**:`var basePath = new URL(script.src).pathname` 剥掉尾部 `/static/widget.js...`(得 "" 或 `/t/`);brand / avatar(含 DEFAULT_AVATAR_PATH)/ WS 三类端点构造统一 `wsBase + basePath + 原路径`。Host 模式 basePath="" 零变化;`tests/test_widget_static.py` 风格静态断言钉住。 +- **chat.html**:新增 15 行内联引导脚本——按 `location.pathname` 推导 basePath(`/t//` → `/t/`;`/` → ""),改写自身 `/