feat(tenant): 租户域名 —— 用子域前缀判断"这是哪个单位的站点"(t1.xxx.com / t2.xxx.com)
客户新提:不同的租户采用不同的地址前缀来获取租户ID。
一、它解决什么
在此之前"这次是哪个单位"都要人填或扫:PC 登录要手填租户码(跨单位重名时报 40012
「请填写租户码」,而用户根本不知道该填什么)、H5 与小程序靠扫码 scene。
而域名本身就是"我是谁"最自然的答案 —— 在 t1.xxx.com 上打开页面时,
系统不该再问"你是哪个单位的"。
前缀用 t{tenantId},与扫码 scene 的 t{tid} **同一套约定**(scene 早就是 t1-k17),
不引入第二套编号。
二、★ 唯一必须守住的规则:Host 定位,token 授权
未登录 → Host 决定"去哪个单位里找这个人"
已登录 → 一律以 token 里的 tenantId 为准,且 Host 不一致就拒绝
不校验后者的话:在 t1.xxx.com 上拿 t2 的 token 就能读 t2 的数据 ——
而界面一切正常(前端本来就会带 token),是个安静且极难发现的越权。
所以 AuthInterceptor 里那段判断比"登录免填租户码"重要得多,E2E 专门守着它。
三、落地
FillProperties.TenantDomain(enabled/base/scheme) 默认关,siteOf(tid) 拼站点地址
support/TenantDomainResolver 解析不出来返回 null(不是错误)
TenantMapper.selectActiveById 停用/已删的租户不解析
UserMapper.selectByTenantIdAndUsername 域名路径按 id 找用户
AuthService.locateUser 域名优先;手填租户码与域名矛盾 → 明确报错
AuthInterceptor Host 与 token 租户不一致 → 403
MpH5AuthController.redirectToApp 按 scene 的 t{tid} 跳回该单位站点
application-dev.yml enabled/base=localhost/scheme=http
null 的语义很重要:解析不出来 ≠ 出错,而是"这是统一入口/单域名部署",回退老路径。
所以单域名部署仍然完全合法(enabled=false 即可)。
四、两条来自微信的硬约束(决定架构,不是可选)
1. 公众号网页授权的「授权回调页面域名」**只能配一个** → 每租户一个子域时,
回调统一落在主域名,再按 scene 里的 t{tid} 302 回该单位站点。
正好复用既有的 state(scene 随 state 走)+ token 放 fragment 的机制。
2. 小程序的 request 合法域名是**固定白名单**,配不了 *.xxx.com →
小程序端用不了域名,继续靠 scene 里的 t{tid}。这点客户需要知道。
五、本地开发的坑(已处理)
开发期 base 是 localhost,真按它回跳会把手机上的学生从穿透域名扔到 t1.localhost ——
打不开,且是浏览器级失败,看不出是我们跳错了。所以回跳加了本机地址判断
(localhost/127.0.0.1/192.168.*/10.*/*.local),本机 base 一律回退到 h5-base-url。
六、E2E:scripts/e2e-m1-tenant-domain.py → 16/16
[1] t1.localhost + 不填租户码 → 登录成功(对照:不带域名 → 40012);
带端口/大小写/多一层子域都能解析
[2] 域名与手填租户码矛盾 → 明确报错;一致则正常
[3] ★ 拿 t1 的 token 访问 t14 域名 → 403,提示可理解
[4] 边界:x1.localhost / t999999.localhost / example.com → 都回退老路径
用 http.client 伪造 Host 头(urllib 会按 URL 自己算 Host,覆盖不掉)。
七、全量 676 条断言、0 失败(新增 16);verify-docs 19 项通过
八、⏳ 需要拍板的三件事(已写进交接摘要 §一·二十五)
1. 正式域名(base)+ 泛域名解析 + 泛域名证书 *.xxx.com(备案一个主域名即可覆盖子域)
2. 要不要保留"手填租户码"(我保留了:统一入口、老客户端、运维排查都需要)
3. 小程序端确认继续靠 scene
另:这条新需求应补进 PRD 与 03-接口设计(目前只落在代码与本摘要里)——下一轮补。
This commit is contained in:
@@ -138,6 +138,28 @@ fill:
|
||||
# 代码里还会再判一次 profile 不含 prod(双保险)。
|
||||
stub-enabled: ${WX_H5_STUB:true}
|
||||
|
||||
# ── 租户域名(用子域前缀区分租户)──────────────────────────────────────
|
||||
#
|
||||
# 形态:t{tenantId}.{base},如 t1.example.com / t2.example.com。
|
||||
# 前缀用的是 **tenantId**,与扫码 scene 的 t{tid} 是同一套约定(scene 早就是 t1-k17)。
|
||||
#
|
||||
# 打开之后:
|
||||
# · PC 登录在 t1.xxx.com 上**不用再填租户码** —— 域名已经回答了"你是哪个单位的",
|
||||
# 那句「该账号存在于多个单位,请填写租户码」在这条路径下不会再出现
|
||||
# · H5 微信授权回调仍落在主域名(微信的"授权回调页面域名"只能配一个),
|
||||
# 回调再按 scene 里的 t{tid} 跳回该单位的站点
|
||||
#
|
||||
# ⚠️ 已登录的请求**以 token 里的 tenantId 为准**,Host 只用于定位;
|
||||
# 两者不一致会被 AuthInterceptor 拒掉(403)—— 否则在 t1 域名上拿 t2 的 token
|
||||
# 就能读 t2 的数据,而界面看起来一切正常。
|
||||
# ⚠️ 小程序端**用不了它**:微信的 request 合法域名是固定白名单,配不了 *.xxx.com,
|
||||
# 所以小程序继续靠 scene 里的 t{tid}。
|
||||
# ⚠️ 生产要配**泛域名解析 + 泛域名证书**(*.example.com)。
|
||||
tenant-domain:
|
||||
enabled: ${XT_TENANT_DOMAIN_ENABLED:true}
|
||||
base: ${XT_TENANT_DOMAIN_BASE:localhost}
|
||||
scheme: ${XT_TENANT_DOMAIN_SCHEME:http}
|
||||
|
||||
springdoc:
|
||||
swagger-ui:
|
||||
enabled: true
|
||||
|
||||
Reference in New Issue
Block a user