如何基于Nginx实现类似Slack/GitLab的动态子域名功能
实现类似Slack/GitLab的子域名多租户功能(Nginx+代码方案)
我之前帮不少团队落地过类似的子域名多租户方案,刚好能给你捋清楚整个流程——核心就是通配符路由+租户识别的组合,下面拆解原理和具体实现步骤:
核心原理拆解
这类子域名机制本质分三步走:
- 通配符DNS:把所有
*.abc.com格式的子域名都指向你的服务器集群,确保任何自定义子域名都能被解析到 - Nginx反向代理:统一接收所有子域名的请求,转发给后端服务的同时传递关键标识
- 后端租户匹配:解析子域名对应的组织/租户ID,从数据库拉取专属配置并返回对应内容
具体实现步骤
1. 先搞定DNS配置
首先要让所有子域名都能找到你的服务器:
去你的域名服务商后台,添加一条通配符A记录:
- 主机记录:填
* - 记录值:你的服务器公网IP(或负载均衡域名)
这样不管用户访问www.cop.abc.com还是www.myteam.abc.com,都会指向你的服务器。
2. Nginx配置:统一接收子域名请求
Nginx是整个流程的入口,这里提供两种常见配置方案:
方案1:全量转发子域名请求到后端
适合不想在Nginx层做复杂解析,把所有判断逻辑交给后端的场景:
server { listen 80; listen 443 ssl; server_name *.abc.com; # HTTPS必备:通配符SSL证书(可以去Let's Encrypt申请免费的) ssl_certificate /path/to/wildcard.abc.com.crt; ssl_certificate_key /path/to/wildcard.abc.com.key; # 转发请求到后端服务(比如Flask/Django/Node.js) location / { proxy_pass http://your-backend:8000; proxy_set_header Host $host; # 把原始请求的Host头传给后端 proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # 单独处理主域名(www.abc.com),避免和子域名逻辑混淆 server_name www.abc.com; location / { proxy_pass http://your-backend:8000/main; # 主域名跳转专属路由 } }
方案2:Nginx提前提取子域名传给后端
如果想减少后端的解析工作量,可以用正则匹配直接提取组织名,通过自定义请求头传递:
server { listen 80; # 正则匹配www.xxx.abc.com格式,提取xxx作为组织名 server_name ~^www\.(?<org_name>[a-zA-Z0-9-]+)\.abc\.com$; location / { proxy_pass http://your-backend:8000; proxy_set_header X-Org-Name $org_name; # 把提取到的组织名传给后端 proxy_set_header Host $host; } }
3. 后端代码逻辑:识别子域名并匹配组织
以Python Flask为例,展示两种处理方式:
方式1:从Host头解析子域名
from flask import Flask, request, abort import re app = Flask(__name__) # 模拟数据库存储组织信息(实际项目用真实数据库) org_database = { "cop": {"full_name": "Cop技术团队", "owner": "张三"}, "myteam": {"full_name": "我的研发小组", "owner": "李四"} } def extract_org_from_host(host): # 匹配www.xxx.abc.com的子域名格式 match_result = re.match(r"www\.([a-zA-Z0-9-]+)\.abc\.com$", host) if match_result: return match_result.group(1) return None @app.route("/") def org_homepage(): current_host = request.host org_name = extract_org_from_host(current_host) if not org_name: abort(404) # 不是合法子域名,返回404 org_info = org_database.get(org_name) if not org_info: return f"组织 {org_name} 不存在,请先注册" return f"欢迎访问 {org_info['full_name']} 的专属页面!" if __name__ == "__main__": app.run(host="0.0.0.0", port=8000)
方式2:从Nginx自定义头获取组织名
如果用了方案2的Nginx配置,后端直接读取自定义头即可,更高效:
@app.route("/") def org_homepage(): org_name = request.headers.get("X-Org-Name") if not org_name: abort(404) org_info = org_database.get(org_name) # 后续逻辑同上
4. 数据库设计关键要点
为了避免子域名冲突,组织表必须加唯一约束:
CREATE TABLE organizations ( id INT PRIMARY KEY AUTO_INCREMENT, org_name VARCHAR(50) NOT NULL UNIQUE, -- 子域名标识(比如cop、myteam) full_name VARCHAR(100) NOT NULL, -- 组织全名 owner_id INT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY (org_name) );
用户创建组织时,要先校验org_name的合法性:
- 只能包含字母、数字、连字符
- 不能以连字符开头/结尾
- 不能使用敏感词(比如admin、www、mail)
- 检查是否已存在于数据库中
5. 额外注意事项
- HTTPS证书:必须使用通配符SSL证书(
*.abc.com),否则子域名会出现证书错误 - 主域名隔离:确保主域名(www.abc.com)的逻辑和子域名完全分开,避免路由冲突
- 缓存优化:高并发场景下,可以用Redis缓存组织信息,减少数据库查询压力
- 子域名回收:如果组织注销,要及时清理数据库中的对应记录,避免后续被其他用户占用
内容的提问来源于stack exchange,提问作者cop
相关产品推荐
相关产品推荐

