三种身份,三种后台
先分清三类用户:
| 身份 | 是谁 | 关心什么 |
|---|---|---|
| 厂商(vendor) | 卖这套软件的我们 | 授权、客户上线情况、版本分发 |
| 平台(platform) | 买了 SaaS 的运营公司 | 租户开通、平台级配置、跨租户报表 |
| 租户(tenant) | 平台下的商户 | 自己的商品、订单、会员、财务 |
三者共用同一套业务能力(商品、订单、结算),但入口与可见范围完全不同。最常见的事故是:租户超管登录后,在菜单里看到了"租户管理"甚至"授权中心"。
为什么不拆成三个代码库
拆库看起来干净,但代价是:
- 商品、订单、结算这些核心逻辑要复制三份,改一次要改三处;
- 权限模型要维护三套;
- 数据库结构要处理三套演进。
所以选择一份代码 + 三个入口 + 一层闸门:
- 后端按 Host 解析当前身份与租户,注入上下文;
- 前端按身份构建不同的产物(平台端 / 租户端各自一套
dist); - 所有平台专属接口加统一闸门,租户上下文调用直接拒绝。
入口与构建产物分离
前端视图目录按三份组织:
common:登录、个人中心、错误页等共用页面;platform:平台运营专属(租户管理、平台配置);tenant:商户业务(商品、订单、库存、财务)。
构建时按入口产出两套静态资源,分别部署到平台域名与租户域名。这样即使租户端被反编译,也拿不到平台端的页面代码——物理分离比运行时判断更可靠。
租户端还支持自有域名:客户希望用自己的域名对外,所以租户解析不能只靠子域,还要支持"域名 → 租户"的映射表。
闸门:默认拒绝
平台专属功能在三个层面各拦一次:
- 菜单装配:按身份过滤,租户上下文不出现平台菜单;
- 接口中间件:命中平台专属路由且当前是租户上下文 → 直接返回无权限;
- 页面守卫:前端路由守卫再判一次,防止直接输 URL。
为什么三层都要做?因为前两层都可能被绕过或漏配:菜单靠数据(漏插一条就露出来)、接口靠注解(漏标一个就放行)。三层叠加后,漏一个也不至于越权。
一个真实教训
上线初期,租户超管确实能看到平台功能页面。根因是"菜单可见性只按角色权限判断,没判断身份"——租户超管在自己的租户里是超级管理员,天然拥有全部角色权限,于是平台菜单也被判定为可见。
修法就是上面那套闸门:把"身份"作为独立维度参与判断,而不是复用角色权限。这条经验后来也用在授权受限模式上(见另一篇)。
一套代码的边界在哪
一份代码服务三类用户,代价是复杂性集中在"上下文解析"上:每个请求都要知道"我现在是谁、属于哪个租户"。
我们的约束是:
- 上下文由中间件统一注入,业务代码不自己解析;
- 数据访问层统一加租户条件,禁止业务代码手写租户过滤(否则迟早漏一处);
- 平台跨租户查询走只读视图,不允许平台端直接改租户数据。
第三条很关键:允许平台端随意改租户数据,等于给了一个绕过租户隔离的后门,一旦出问题就是数据归属纠纷。
小结
三端分离的本质不是三套界面,而是三种可见范围。入口、构建产物、权限闸门、数据访问四个层面同时收口,才能做到"共用一套业务逻辑,却互不越界"。