← 路由设计 · 稳定入口与可变目标 / 多租户 · 用 host 而不是 path 路由到租户 待审核 15 / 23

多租户 · 用 host 而不是 path 路由到租户

前面的路由都看 path。SaaS 多租户换了个维度:看 host。同一套代码、同一批服务器,给每个客户一个专属子域——acme.app.comglobex.app.com——路由层从 Host 请求头里把租户标识解析出来,再决定加载哪个租户的数据与配置。入口(一套程序)稳定,目标(是哪个租户的数据)由 host 决定,这是「稳定入口 ↔ 可变目标」落在租户隔离这根轴上。

它靠一条通配子域 *.app.com 撑起:DNS 上一条泛解析、TLS 用一张通配证书,新客户开通即得子域、零运维。但通配会吞掉一切,于是要处理三类例外:保留子域www / api 不能被当成租户)、站点根域app.com 是官网),以及客户想用自己的域名dash.acme-corp.com)时的 CNAME 映射。输入一个 host 看它被解析成哪个租户。

保留子域必须在注册时拉黑。 通配 *.app.com 会把任意子域都当成租户,包括 apiwwwadminmail。如果不在开通流程里把这些拦下,某个用户注册了名叫 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:客户改不动你的程序,你改一行映射就能把它接到任意租户。