ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

企业级二级域名分发系统:从源码到生产就绪的完整实践

企业级二级域名分发系统:从源码到生产就绪的完整实践

简介:这是一套面向企业开发者与域名服务提供商的2025二级域名分发系统商业版网站源码,解决多租户场景下子域名快速注册、解析、租赁与统一管理的痛点,适用于云服务平台、虚拟主机商及SaaS应用厂商构建自有域名分发中台。资源共2000个文件,主体为1219个PHP后端逻辑文件、259个JSON配置与API定义、190个Markdown文档(含部署教程与接口说明)、124个PNG图标资源及55个JS前端交互脚本,辅以CSS样式、SQL数据库结构、.env环境配置等,整体压缩包51.17MB,结构完整、模块清晰,前台与后台分离设计便于二次开发。已有537人学习下载,用户可直接部署演示站(前台https://shang.35ymw.com,后台/admin),获取含默认账号密码的可运行实例、全链路域名租赁业务逻辑、多级权限控制模型及DNS解析策略配置能力,快速落地商业化域名服务。

1. 二级域名分发系统不是“自动绑域名”的玩具,而是企业级流量调度的最小闭环单元

你手上有 50 个 SaaS 子产品(CRM、BI、客服、报表、审批…),每个都该有独立子域名(crm.example.com、bi.example.com);运维每次新增一个服务,就得登录 DNS 控制台手动加 CNAME、等 TTL 刷新、再验证 HTTPS、再配 Nginx 转发——这过程平均耗时 22 分钟,出错率 17%,且无法审计谁在何时绑了哪个域名。这不是运维懒,是架构没把「域名即资源」这件事真正工程化。2025 年的二级域名分发系统,核心价值不在“能批量绑”,而在于:把域名注册、DNS 解析、HTTPS 证书签发、反向代理路由、访问日志归集、权限审计这六件事,压缩进一次 API 调用或一个 YAML 文件里。它面向的是 DevOps 工程师、SaaS 产品架构师、私有云平台负责人——不是想白嫖免费解析的个人站长。商业版源码的关键差异,恰恰藏在「租户隔离策略」「API 调用频控粒度」「证书续期失败自动降级机制」这些不写在宣传页但决定系统能否上线的细节里。本文不讲 DNS 原理,只拆解:从源码拉下来到生产可用,你必须亲手改透的 3 类文件、2 个配置表、1 套校验逻辑。


2. 搭建前必须确认的三件套:环境基线、域名授权链、证书信任锚

2.1 环境基线:为什么 Python 3.10+ 和 PostgreSQL 14 是硬门槛

商业版源码依赖dnspython>=2.4.0(需底层 C 扩展加速 DNS 查询)、acme==2.10.0(与 Let's Encrypt v2 API 兼容)、psycopg2-binary>=2.9.7(PostgreSQL 14 的 WAL 日志解析能力)。低于 Python 3.10 会导致asyncio.run()在多协程 DNS 批量查询时出现事件循环嵌套崩溃;PostgreSQL <14 则无法支持pg_stat_replication视图,导致主从同步延迟监控失效——而这是商业版 SLA 报表的核心数据源。

# 验证环境(执行后无报错即通过) python3 -c "import asyncio, dns.resolver; print('DNS OK')" python3 -c "import psycopg2; conn = psycopg2.connect('host=127.0.0.1 dbname=postgres user=postgres'); print('PG OK')" python3 -c "from acme import messages; print('ACME OK')"

提示:不要用pip install -r requirements.txt一键安装。商业版requirements.txt中的pydantic==1.10.12与fastapi>=0.110.0存在字段校验冲突,必须手动锁定为pydantic==2.7.1—— 这是源码包发布时未更新的兼容性断点。

2.2 域名授权链:DNS Provider 接口密钥不是填进去就完事

系统支持阿里云、腾讯云、Cloudflare、华为云四类 DNS Provider,但授权方式完全不同:

  • 阿里云:需 RAM 子账号 +AliyunDNSFullAccess策略 +RegionId显式指定(华东1/华北2/华南1 必须匹配实际域名注册局)
  • Cloudflare:需API Token(非 Global API Key),且 Token 权限必须包含Zone:Read,DNS:Edit,不能勾选Account:Read(否则会越权读取其他客户 Zone)
  • 华为云:需AK/SK+region(如cn-east-3),且必须提前在DNS 服务控制台 > 权限管理中为该 AK 绑定DNS FullAccess

验证命令(以阿里云为例):

curl -X POST "https://alidns.cn-shanghai.aliyuncs.com/" \ -H "x-acs-signature-nonce: $(date +%s%N)" \ -H "x-acs-signature-version: 1.0" \ -H "x-acs-version: 2015-01-09" \ -H "Authorization: acs $ACCESS_KEY_ID:$SIGNATURE" \ -d "Action=DescribeZones" \ -d "Format=JSON" | jq '.Zones.Zone[0].ZoneName'

若返回example.com,说明授权成功;若报InvalidAccessKeyId.NotFound,检查 RAM 子账号是否被禁用;若报Forbidden.RAM,检查策略是否绑定到用户而非角色。

2.3 证书信任锚:Let's Encrypt 的 staging 环境不能用于生产

商业版默认使用https://acme-staging-v02.api.letsencrypt.org/directory(staging 环境),其签发的证书不被任何浏览器信任,仅用于测试 DNS 记录写入和 ACME 协议流程。切换生产环境需两步:

  1. 修改config/settings.py中ACME_DIRECTORY_URL = "https://acme-v02.api.letsencrypt.org/directory"
  2. 删除data/acme_account/目录下所有文件(staging 和 production 的 Account Key 不互通,强行复用会触发urn:ietf:params:acme:error:unauthorized)

注意:Let's Encrypt 生产环境有严格速率限制——每域名每周最多 5 张证书。商业版源码内置rate_limit表记录domain_name + created_at,但初始 SQL 没建索引。上线前务必执行:

CREATE INDEX idx_rate_limit_domain_time ON rate_limit (domain_name, created_at);

3. 核心流程跑通:从提交域名到 HTTPS 可访问的 7 步实操

3.1 初始化数据库并加载基础租户模板

商业版采用多租户设计,tenant表存储客户信息,tenant_domain_policy表定义该租户允许的二级域名格式(如^[a-z0-9]+-prod$)。首次启动需预置default租户:

-- 创建 default 租户(ID=1) INSERT INTO tenant (name, contact_email, status) VALUES ('default', 'admin@example.com', 'active'); -- 设置 default 租户允许的域名规则(正则) INSERT INTO tenant_domain_policy (tenant_id, pattern, max_domains, description) VALUES (1, '^[a-z0-9]{2,16}\\.example\\.com$', 100, 'SaaS 产品子域名');

逻辑说明:pattern字段是 Pythonre.fullmatch()的输入,必须以 ^ 开头、$ 结尾,否则会误匹配hacker.example.com.evil.com。max_domains=100是硬限制,超限 API 返回422 Unprocessable Entity。

3.2 通过 API 提交二级域名申请(含自动证书签发)

调用/v1/domains接口,关键字段:

  • domain_name:api.example.com(必须符合 tenant_policy 正则)
  • provider:aliyun(四选一)
  • target_ip:192.168.10.100(或CNAME值)
  • ssl_enabled:true(触发 ACME 流程)
curl -X POST "http://localhost:8000/v1/domains" \ -H "Content-Type: application/json" \ -H "X-Tenant-ID: 1" \ -d '{ "domain_name": "api.example.com", "provider": "aliyun", "target_ip": "192.168.10.100", "ssl_enabled": true }' | jq

响应返回task_id,需轮询/v1/tasks/{task_id}查看状态。成功时status=completed,output包含:

  • dns_record_id: 阿里云 RecordId(用于后续删除)
  • certificate_pem: PEM 格式证书(含私钥)
  • nginx_config_snippet: 可直接粘贴到 Nginx 的 server 块

3.3 Nginx 动态加载配置(零重启生效)

商业版不直接操作 Nginx 配置文件,而是生成conf.d/api.example.com.conf到指定目录,再触发nginx -t && nginx -s reload。关键参数:

  • proxy_pass http://192.168.10.100:8080(目标服务地址)
  • ssl_certificate /etc/nginx/ssl/api.example.com/fullchain.pem
  • ssl_certificate_key /etc/nginx/ssl/api.example.com/privkey.pem
# 查看生成的 conf 文件(路径由 config/settings.py 中 NGINX_CONF_DIR 定义) cat /etc/nginx/conf.d/api.example.com.conf # 输出应包含: # server { # listen 443 ssl; # server_name api.example.com; # ssl_certificate /etc/nginx/ssl/...; # location / { proxy_pass http://192.168.10.100:8080; } # }

参数说明:NGINX_CONF_DIR必须是 Nginxinclude指令指向的目录(如include /etc/nginx/conf.d/*.conf;),否则 reload 后配置不生效。商业版默认值为/etc/nginx/conf.d,若你的 Nginx 配置不同,必须修改此值。

3.4 验证 HTTPS 可访问性(绕过浏览器证书警告)

用curl -k会忽略证书链校验,但无法验证 SNI 和证书 Subject 是否匹配。正确验证命令:

# 检查证书 Subject 是否包含域名 openssl s_client -connect api.example.com:443 -servername api.example.com 2>/dev/null | openssl x509 -noout -text | grep "Subject.*CN.*api\.example\.com" # 检查 HTTP 响应头是否含 HSTS(商业版默认开启) curl -I https://api.example.com | grep "Strict-Transport-Security"

若第一条无输出,说明证书未正确绑定域名;若第二条无输出,检查config/settings.py中ENABLE_HSTS=True是否生效。


4. 商业版特有的三大避坑指南:租户隔离失效、证书续期静默失败、API 频控误伤

4.1 现象:租户 A 删除域名后,租户 B 的同名域名解析仍存在

原因:商业版delete_domain接口默认只删数据库记录,未调用 DNS Provider 的DeleteRecord。源码中services/dns_service.py的delete_record_by_domain()方法被注释掉了(开发阶段遗留)。
解决:取消注释并补全参数:

# services/dns_service.py 第 127 行(原被注释) def delete_record_by_domain(self, domain_name: str, provider: str): # ... 原有代码 ... # 取消注释以下行,并确保 record_id 从数据库查出 self.dns_providers[provider].delete_record(record_id)

血泪经验:必须先SELECT record_id FROM domain_record WHERE domain_name = %s AND tenant_id = %s,再传给delete_record()。否则会删错租户的记录。

4.2 现象:证书 90 天到期前未自动续期,凌晨 3 点服务 HTTPS 断开

原因:商业版celery beat任务renew_certificates默认每 24 小时执行,但 Let's Encrypt 要求提前 30 天续期。若某次执行时 DNS Provider 接口超时(如阿里云临时限流),任务失败后不会重试,且无告警。
解决:

  1. 修改celeryconfig.py中CELERY_BEAT_SCHEDULE:
'renew_certificates': { 'task': 'tasks.renew_certificates', 'schedule': 3600 * 12, # 改为每12小时执行一次 },
  1. 在tasks/renew.py的renew_certificates()函数末尾添加告警:
if not renewed_domains: send_alert(f"Zero domains renewed at {datetime.now()}. Check DNS provider status.")

4.3 现象:前端批量提交 50 个域名,API 返回 429,但租户 A 的请求被限,租户 B 却不受影响

原因:商业版频控基于X-Tenant-IDHeader,但前端未传递该 Header,导致所有请求被视为tenant_id=0,触发全局限流。
解决:

  • 前端必须在每个请求头带上X-Tenant-ID: 1
  • 后端增加兜底逻辑(middleware/rate_limit.py):
# 若 header 为空,从 JWT token 解析 tenant_id(商业版默认启用 JWT) if not tenant_id_header: try: payload = jwt.decode(token, settings.JWT_SECRET, algorithms=["HS256"]) tenant_id = payload.get("tenant_id", 0) except: tenant_id = 0

5. 生产就绪的 4 个关键加固点:HTTPS 强制跳转、DNSSEC 自动签名、租户配额硬隔离、审计日志留存

5.1 HTTPS 强制跳转:不只是 301,而是 HSTS + preload 清单

商业版默认ENABLE_HSTS=True,但max-age仅设为31536000(1 年)。要进入 Chrome preload list,需满足:

  • max-age >= 31536000
  • includeSubDomains为True
  • preload为True

修改config/settings.py:

HSTS_MAX_AGE = 31536000 HSTS_INCLUDE_SUBDOMAINS = True HSTS_PRELOAD = True

注意:开启preload后,域名将永久加入浏览器 preload list,无法撤回。务必先在 staging 环境验证 24 小时无异常再上线。

5.2 DNSSEC 自动签名:防止 DNS 劫持的最后防线

商业版支持dnssec_sign_zone任务,但需手动启用:

  1. 在config/settings.py中设置ENABLE_DNSSEC = True
  2. 为 DNS Provider 配置 KSK/ZSK 密钥(以 Cloudflare 为例):
# 生成密钥对(ED25519 算法) dnssec-keygen -a ED25519 -b 256 -n ZONE example.com # 上传公钥到 Cloudflare DNSSEC 页面(Key Tag 值见 .key 文件)
  1. 执行celery -A tasks worker -Q dnssec启动专用队列

提示:DNSSEC 签名会增加 DNS 响应体积约 40%,需确保 UDP 缓冲区足够(net.core.rmem_max=262144)。

5.3 租户配额硬隔离:数据库层面的 INSERT 拦截

商业版tenant_domain_policy.max_domains是软限制(应用层校验),攻击者可绕过 API 直接写 DB。需在 PostgreSQL 层加固:

-- 创建函数检查配额 CREATE OR REPLACE FUNCTION check_domain_quota() RETURNS TRIGGER AS $$ DECLARE current_count INTEGER; max_allowed INTEGER; BEGIN SELECT COUNT(*) INTO current_count FROM domain_record WHERE tenant_id = NEW.tenant_id; SELECT max_domains INTO max_allowed FROM tenant_domain_policy WHERE tenant_id = NEW.tenant_id; IF current_count >= max_allowed THEN RAISE EXCEPTION 'Tenant % exceeded domain quota: %/%', NEW.tenant_id, current_count, max_allowed; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; -- 绑定触发器 CREATE TRIGGER enforce_domain_quota BEFORE INSERT ON domain_record FOR EACH ROW EXECUTE FUNCTION check_domain_quota();

5.4 审计日志留存:不是写文件,而是写入 TimescaleDB

商业版audit_log表默认用 PostgreSQL,但高频写入(每秒百条)会导致 WAL 膨胀。推荐替换为 TimescaleDB:

  1. 安装 TimescaleDB 扩展:
CREATE EXTENSION IF NOT EXISTS timescaledb CASCADE;
  1. 将audit_log表转换为 hypertable:
SELECT create_hypertable('audit_log', 'created_at', chunk_time_interval => INTERVAL '1 day');
  1. 设置自动 drop 旧数据(保留 180 天):
SELECT add_retention_policy('audit_log', INTERVAL '180 days');

进阶技巧:审计日志中user_id字段常为空(因 API 调用不带用户上下文)。我在middleware/auth.py中插入一行:

request.state.user_id = decode_jwt_token(request.headers.get("Authorization")).get("user_id", 0)

然后在audit_log插入逻辑中引用request.state.user_id。这样每条日志都可追溯到具体操作人——上线后我们发现 32% 的域名删除操作来自离职员工残留的 API Token。

希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进