4、租户路由机制:基于域名路由、基于子域名路由、基于请求头路由、基于路径路由、路由中间件实现

好,咱们接着聊多租户系统里一个非常核心的环节——租户路由

说白了,就是用户访问你的低代码平台时,系统怎么知道他是哪个租户的?

这个问题,我在早期做SaaS平台时踩过不少坑。一开始觉得不就是取个参数嘛,后来发现路由策略选不对,后面做权限、做数据隔离全都得返工。你想想看,这玩意儿要是设计不好,用户数据串了,那可就出大事了。

4.1 为什么需要租户路由?

多租户架构里,所有租户共享一套代码、一个数据库实例(或者多个实例)。但每个租户的数据必须严格隔离。

路由机制要解决的核心问题就两个:

  • 识别身份:当前请求来自哪个租户?
  • 分发请求:把请求导向该租户专属的资源、配置、数据源。

我见过有些团队直接在URL后面加个 ?tenant_id=xxx 参数,简单粗暴。但这样做,用户复制链接分享出去,租户信息就暴露了,安全风险很大。嗯,这里要注意,路由机制不仅要准,还要安全。

4.2 五种主流路由策略详解

下面这五种策略,我按实际项目中的使用频率排序来讲。每种都有它的适用场景,没有银弹。

4.2.1 基于域名路由

每个租户一个独立域名。比如:

  • 租户A:https://tenant-a.myapp.com
  • 租户B:https://tenant-b.myapp.com

优点

  • 最直观,用户一看就知道自己用的是哪个租户的系统。
  • SSL证书可以单独配置,安全性高。
  • DNS层面就可以做负载均衡,性能好。

缺点

  • 需要为每个租户配置DNS记录,运维成本高。
  • 域名数量多了之后,管理起来很麻烦。
我的经验:我在做一个面向大型企业的低代码平台时,客户要求必须用独立域名。因为他们的IT部门有严格的网络安全策略,不允许使用子域名。这种情况下,基于域名路由就是唯一选择。

4.2.2 基于子域名路由

这是目前最主流的做法。每个租户一个子域名:

  • 租户A:https://tenant-a.myapp.com
  • 租户B:https://tenant-b.myapp.com

实现起来很简单,在Nginx或网关层用通配符解析就行:

# Nginx 配置示例
server {
    listen 80;
    server_name ~^(?<tenant>[a-z0-9]+)\.myapp\.com$;

    location / {
        proxy_set_header X-Tenant-ID $tenant;
        proxy_pass http://backend;
    }
}

后端拿到 X-Tenant-ID 请求头,就知道是哪个租户了。

优点

  • 只需要配置一个通配符DNS记录,运维简单。
  • 用户友好,子域名可以体现品牌。

缺点

  • 子域名长度有限制,有些浏览器对子域名层级有限制。
  • 如果租户数量巨大(比如百万级),DNS解析可能会有性能瓶颈。
避坑指南:我曾经遇到过一个坑——子域名里用了下划线。结果某些旧版浏览器直接不认,导致用户无法访问。所以子域名最好只用字母、数字和连字符。

4.2.3 基于请求头路由

这种方式不依赖URL,而是通过HTTP请求头来传递租户信息。比如:

GET /api/dashboard HTTP/1.1
Host: myapp.com
X-Tenant-ID: tenant-a

后端从请求头里提取 X-Tenant-ID

优点

  • URL干净,不暴露租户信息。
  • 适合API网关模式,网关解析后转发给后端。

缺点

  • 需要客户端配合设置请求头,对前端开发有要求。
  • 浏览器直接访问时无法自动携带,通常需要配合登录态使用。

我个人习惯在微服务架构里用这种方式。因为服务间调用时,通过RPC框架的拦截器自动注入租户ID,非常方便。

4.2.4 基于路径路由

把租户信息放在URL路径里:

  • https://myapp.com/tenant-a/dashboard
  • https://myapp.com/tenant-b/settings

实现也简单,前端路由或后端路由解析第一个路径段即可。

优点

  • 不需要额外配置DNS,部署最简单。
  • 适合演示环境或内部系统。

缺点

  • URL冗长,用户体验一般。
  • 路径深度增加后,路由规则容易混乱。
我的建议:路径路由适合做快速原型验证。但生产环境,尤其是面向客户的产品,我强烈建议用子域名或域名路由。路径路由会让用户觉得你的系统不够专业。

4.2.5 路由中间件实现

不管用哪种策略,最终都需要一个统一的中间件来解析租户信息。我以Node.js的Express框架为例,写一个通用的租户路由中间件:

// tenant-router.js
const getTenantFromDomain = (req) => {
    const host = req.get('host');
    const match = host.match(/^([a-z0-9]+)\./);
    return match ? match[1] : null;
};

const getTenantFromHeader = (req) => {
    return req.get('X-Tenant-ID') || null;
};

const getTenantFromPath = (req) => {
    const parts = req.path.split('/');
    return parts[1] && parts[1].length > 0 ? parts[1] : null;
};

const tenantRouter = (options = {}) => {
    const {
        strategy = 'subdomain',  // 'domain' | 'subdomain' | 'header' | 'path'
        defaultTenant = null
    } = options;

    return (req, res, next) => {
        let tenantId = null;

        switch (strategy) {
            case 'domain':
            case 'subdomain':
                tenantId = getTenantFromDomain(req);
                break;
            case 'header':
                tenantId = getTenantFromHeader(req);
                break;
            case 'path':
                tenantId = getTenantFromPath(req);
                break;
            default:
                tenantId = getTenantFromDomain(req);
        }

        if (!tenantId) {
            tenantId = defaultTenant;
        }

        if (!tenantId) {
            return res.status(400).json({
                error: '无法识别租户信息'
            });
        }

        // 把租户ID挂到请求对象上,后续所有处理都能用
        req.tenantId = tenantId;
        next();
    };
};

module.exports = tenantRouter;

使用起来也很简单:

const express = require('express');
const tenantRouter = require('./tenant-router');

const app = express();

// 使用子域名路由策略
app.use(tenantRouter({ strategy: 'subdomain' }));

app.get('/api/dashboard', (req, res) => {
    // 这里就能拿到 req.tenantId
    res.json({
        tenant: req.tenantId,
        message: '欢迎回来'
    });
});

app.listen(3000);

4.3 策略选择对照表

为了方便你决策,我整理了一个对照表:

策略 运维成本 安全性 用户体验 适用场景
域名路由 最好 大型企业客户、安全要求高
子域名路由 大多数SaaS产品
请求头路由 一般 微服务架构、API网关
路径路由 最低 内部系统、快速原型

4.4 我的实战建议

最后,分享几点我在多个项目里总结出来的经验:

  1. 优先选子域名路由。除非客户有特殊要求,否则子域名路由是性价比最高的方案。运维简单,用户也容易接受。
  2. 中间件要支持策略切换。我习惯把路由策略做成配置项,这样不同环境(开发、测试、生产)可以用不同策略。开发环境用路径路由方便调试,生产环境用子域名路由保证安全。
  3. 别忘了兜底逻辑。如果解析不到租户ID,一定要有默认处理。我见过有些系统直接抛500错误,用户体验极差。至少应该返回一个友好的提示页面。
  4. 缓存租户信息。每次请求都去数据库查租户配置太慢了。我一般会在中间件里加一层本地缓存或Redis缓存,把租户ID和对应的配置信息缓存起来,TTL设个5分钟就够了。

嗯,租户路由这块就讲到这里。下一节咱们聊聊更细的——租户数据隔离策略。到时候我会讲讲共享数据库和独立数据库各自的坑,以及我当年是怎么从共享数据库迁移到独立数据库的,那可真是一段血泪史啊。