多租户 · 用 host 而不是 path 路由到租户
前面的路由都看 path。SaaS 多租户换了个维度:看 host。同一套代码、同一批服务器,给每个客户一个专属子域——acme.app.com、globex.app.com——路由层从
Host 请求头里把租户标识解析出来,再决定加载哪个租户的数据与配置。入口(一套程序)稳定,目标(是哪个租户的数据)由 host 决定,这是「稳定入口 ↔ 可变目标」落在租户隔离这根轴上。
它靠一条通配子域 *.app.com 撑起:DNS 上一条泛解析、TLS 用一张通配证书,新客户开通即得子域、零运维。但通配会吞掉一切,于是要处理三类例外:保留子域(www /
api 不能被当成租户)、站点根域(app.com 是官网),以及客户想用自己的域名(dash.acme-corp.com)时的 CNAME 映射。输入一个 host 看它被解析成哪个租户。
保留子域必须在注册时拉黑。 通配 *.app.com 会把任意子域都当成租户,包括 api、www、admin、mail。如果不在开通流程里把这些拦下,某个用户注册了名叫 api 的租户,就会劫持
api.app.com——你的 API 网关子域。所以多租户系统都维护一张保留子域黑名单,优先级排在通配规则之前。试试 api.app.com 看它命中保留规则、而非被当成租户。
1 · 客户想用自己的域名:CNAME + Host 头反查
大客户往往不愿意用 acme.app.com,想要 dash.acme-corp.com 这样的自有域名。做法:客户在自己 DNS 把该域 CNAME 指到你的 cname.app.com,你这边记一条「自定义域 → 租户」映射,并为该域签发证书(ACME
自动化)。请求到达时,Host 头是 dash.acme-corp.com——它不在 *.app.com 里,于是先查自定义域映射表,查到就解析到对应租户。这张表的优先级要排在通配之前。
路由层从 Host 头解析租户,四条规则按优先级自上而下(自定义域 → 站点根 → 保留子域 → 通配),第一条命中即返回:
const APEX = 'app.com';
const RESERVED = ['www', 'api', 'admin', 'mail']; // 注册时也禁止占用
const CUSTOM = { 'dash.acme-corp.com': 'acme' }; // 自定义域 → 租户 映射表
function tenantOf(host) {
if (CUSTOM[host]) return { tenant: CUSTOM[host] }; // 自定义域: 查映射表 (优先)
if (host === APEX) return { site: 'marketing' }; // 站点根域: 官网
if (host.endsWith('.' + APEX)) {
const sub = host.slice(0, -(APEX.length + 1));
if (sub.includes('.')) throw '多级子域: 通配证书只覆盖一层'; // *.app.com 保不了 a.b.app.com
if (RESERVED.includes(sub)) return { service: sub }; // 保留子域: 内部服务
return { tenant: sub }; // 通配: 子域即租户
}
return null; // 不属于本服务
}
一处常见的隐患:多级子域。 通配证书 *.app.com 只覆盖一层——它能保 acme.app.com,但保不了 api.acme.app.com(那需要 *.acme.app.com 另一张证书)。所以多数 SaaS
规定租户子域只能一层,解析时遇到多级 label 直接判非法。试试 api.acme.app.com 看它被拒。host 路由的边界,常常是被证书覆盖范围而不是代码逻辑划定的。
2 · 相关页
- 本系列 · 反向代理 / API 网关——网关的 location 也能按
server_name(即 host)分流;多租户是把 host 维度用到极致——一条通配规则覆盖无限个租户子域。 - URL Anatomy · 一条网址怎么拆开——host 也有结构:subdomain · 二级域 · TLD。那个系列把它和 path / query / fragment 一起逐段拆开,理解通配匹配的前提。
- 本系列 · 远程配置入口——自定义域的「客户 CNAME → 你的映射表」也是一层 indirection:客户改不动你的程序,你改一行映射就能把它接到任意租户。