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记录,运维成本高。
- 域名数量多了之后,管理起来很麻烦。
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/dashboardhttps://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 我的实战建议
最后,分享几点我在多个项目里总结出来的经验:
- 优先选子域名路由。除非客户有特殊要求,否则子域名路由是性价比最高的方案。运维简单,用户也容易接受。
- 中间件要支持策略切换。我习惯把路由策略做成配置项,这样不同环境(开发、测试、生产)可以用不同策略。开发环境用路径路由方便调试,生产环境用子域名路由保证安全。
- 别忘了兜底逻辑。如果解析不到租户ID,一定要有默认处理。我见过有些系统直接抛500错误,用户体验极差。至少应该返回一个友好的提示页面。
- 缓存租户信息。每次请求都去数据库查租户配置太慢了。我一般会在中间件里加一层本地缓存或Redis缓存,把租户ID和对应的配置信息缓存起来,TTL设个5分钟就够了。
嗯,租户路由这块就讲到这里。下一节咱们聊聊更细的——租户数据隔离策略。到时候我会讲讲共享数据库和独立数据库各自的坑,以及我当年是怎么从共享数据库迁移到独立数据库的,那可真是一段血泪史啊。