Files
yunzerwebsiteallinone/docs/关于租户官网服务器端nginx的配置.md
T
2026-08-20 01:11:43 +08:00

21 KiB
Raw Blame History

关于租户官网服务器端 nginx 的配置

适用场景:多租户官网系统。访客访问租户域名(如 ceshi.yunzer.com.cn)时,由 nginx 转发给 Go 服务,Go 按请求 Host 识别租户,用标签引擎解析 themes/{模板编码}/ 模板后直出 HTML。

本文给出宝塔环境下两个主域名(dh2.fun、yunzer.com.cn)的完整 nginx 配置、设计原则与注意事项。


一、架构原理

访客浏览器
   │  https://ceshi.yunzer.com.cn/
   ▼
nginx(宝塔)
   │  全部请求 proxy_pass → Go 服务(upstream 统一管理地址)
   │  透传 Host 头(Go 靠它识别租户)
   ▼
Go 服务(beego,默认 8081 端口)
   │  resolveTid:按 Host 查库识别租户
   │  tagengine:渲染 themes/{模板编码}/ 下的模板
   ▼
直出 HTML(无需前端 SPA)

1.1 Go 侧提供的站点路由

路径 说明
GET / 租户官网首页(渲染 index.html 模板)
GET /news 新闻列表(?page=N 分页)
GET /news/:id 文章详情
GET /page/:path 单页(如 /page/about)
/themes/* 模板静态资源(beego SetStaticPath("/themes", "themes"),对应 go/themes/ 目录)
/uploads/* 上传文件静态访问
/backend/*、/platform/* 等 管理端接口(nginx 以 /api/ 前缀转发并剥掉前缀)

Go 的官网渲染路由在 APP_MODE=all(默认值)或 APP_MODE=index 时注册。

1.2 Go 按 Host 识别租户的规则(tenant_site.go)

按顺序匹配:

  1. dev 环境:支持 X-Tenant-ID 请求头切换租户(生产无效);
  2. localhost / 127.0.0.1 → 租户 1;
  3. yz_system_domain_pool.main_domain 命中(status=1)→ 租户 1(平台主站);
  4. yz_system_tenant_domain.full_domain 命中(status=1、未软删)→ 对应租户;
  5. 全部未命中 → 返回"站点不存在"404 页(不再默认回落租户 1)。

所以平台端"域名发放"时必须保证 yz_system_tenant_domain.full_domain 精确等于实际访问域名(如 ceshi.yunzer.com.cn)。


二、配置核心策略(为什么这么写)

2.1 upstream 统一管理后端地址 —— 迁移只改一行

所有 proxy_pass 不写死 IP,统一引用 upstream。迁移服务器时只改 upstream 里的 server 一行。

upstream 写在哪里:直接写在宝塔对应站点的配置文件里、整个文件最顶部(第一个 server { 块之前),不需要配置到其他地方。宝塔站点配置文件会被 include 进主配置的 http {} 上下文,upstream 定义在 server 块之外即合法。常见报错:unknown "go_backend_dh2" upstream(顶部漏了 upstream 定义)、"upstream" directive is not allowed here(误写进了 server 块内)。

注意:两个站点配置文件都会被 include 进同一个 http 上下文,upstream 重名会导致 duplicate upstream 启动失败,必须各起各的名字(本文用 go_backend_dh2 / go_backend_yunzer)。

2.2 默认全量转发 —— 以后加功能零配置

不采用"白名单式"逐个转发(location /news、location /page/……),而是:

  • nginx 只保留必须拦截的规则:敏感文件/目录返回 404、/api/ 剥前缀;
  • 其余一切请求走兜底 location / 转发给 Go;
  • Go 对不认识的路径返回自己的 404 页,行为闭环。

这样以后 Go 增加任何路由(新功能页面、新接口页),nginx 一行都不用改。

2.3 管理端 SPA 的放置建议

主域名与所有子域名都转发给 Go 后,这些域名上就不能再跑 history 模式的 SPA 了。建议:

  • platform / backend 管理端部署在专属子域名(如 admin.yunzer.com.cn)或独立站点;
  • nginx 里为它单独建一个 server_name 精确匹配的 server 块。nginx 匹配顺序是精确 server_name 优先于正则,不会被"所有子域名"的正则块抢走;
  • 若确实需要静态文件与 Go 共存(纯静态、无 SPA 深链接),可用折中写法:
location / {
    try_files $uri @go;   # 本地存在的文件先出静态,其余转 Go
}
location @go {
    proxy_pass http://go_backend_yunzer;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
}

2.4 /api/ 的尾部斜杠不能删

location /api/ {
    proxy_pass http://go_backend_xxx/;   # 尾部 / 负责把 /api/backend/login 映射成 /backend/login
    ...
}

Go 的路由注册在 /backend/*、/platform/*(没有 /api 前缀),靠这个尾部斜杠剥前缀。


三、完整配置 —— 宝塔站点 dh2.fun

文件位置:宝塔 → 网站 → dh2.fun → 设置 → 配置文件。整份替换。

# ===== 后端 Go 服务地址统一定义,迁移时只改这一行 =====
upstream go_backend_dh2 {
    server 10.31.100.2:8081;   # 若 nginx 与 Go 同机,建议改为 127.0.0.1:8081(迁移零改动)
}

# ==================== 主域名 dh2.fun(全量托管给 Go)====================
server {
    listen 80;
    listen 443 ssl;
    listen 443 quic;
    http2 on;

    # 只匹配主域名(子域名由下方独立 server 块处理)
    server_name dh2.fun;

    #CERT-APPLY-CHECK--START
    include /www/server/panel/vhost/nginx/well-known/dh2.fun.conf;
    #CERT-APPLY-CHECK--END
    include /www/server/panel/vhost/nginx/extension/dh2.fun/*.conf;

    #SSL-START SSL相关配置,请勿删除或修改下一行带注释的404规则
    #error_page 404/404.html;

    #HTTP_TO_HTTPS_START
    set $isRedcert 1;
    if ($server_port != 443) {
        set $isRedcert 2;
    }
    if ( $uri ~ /\.well-known/ ) {
        set $isRedcert 1;
    }
    if ($isRedcert != 1) {
        rewrite ^(/.*)$ https://$host$1 permanent;
    }
    #HTTP_TO_HTTPS_END

    ssl_certificate    /www/server/panel/vhost/cert/dh2.fun/fullchain.pem;
    ssl_certificate_key    /www/server/panel/vhost/cert/dh2.fun/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
    ssl_prefer_server_ciphers on;
    ssl_session_tickets on;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;
    add_header Strict-Transport-Security "max-age=31536000";
    error_page 497  https://$host$request_uri;
    #SSL-END

    # --- 安全拦截:敏感文件 ---
    location ~* (\.user.ini|\.htaccess|\.htpasswd|\.env.*|\.project|\.bashrc|\.bash_profile|\.bash_logout|\.DS_Store|\.gitignore|\.gitattributes|LICENSE|README\.md|CLAUDE\.md|CHANGELOG\.md|CHANGELOG|CONTRIBUTING\.md|TODO\.md|FAQ\.md|composer\.json|composer\.lock|package(-lock)?\.json|yarn\.lock|pnpm-lock\.yaml|\.\w+~|\.swp|\.swo|\.bak(up)?|\.old|\.tmp|\.temp|\.log|\.sql(\.gz)?|docker-compose\.yml|docker\.env|Dockerfile|\.csproj|\.sln|Cargo\.toml|Cargo\.lock|go\.mod|go\.sum|phpunit\.xml|phpunit\.xml|pom\.xml|build\.gradl|pyproject\.toml|requirements\.txt|application(-\w+)?\.(ya?ml|properties))$ {
        return 404;
    }

    # --- 安全拦截:敏感目录 ---
    location ~* /(\.git|\.svn|\.bzr|\.vscode|\.claude|\.idea|\.ssh|\.github|\.npm|\.yarn|\.pnpm|\.cache|\.husky|\.turbo|\.next|\.nuxt|node_modules|runtime)/ {
        return 404;
    }

    # SSL 证书验证目录
    location ~ \.well-known {
        allow all;
    }

    if ( $uri ~ "^/\.well-known/.*\.(php|jsp|py|js|css|lua|ts|go|zip|tar\.gz|rar|7z|sql|bak)$" ) {
        return 403;
    }

    # /api/ 转发并剥掉 /api 前缀(尾部斜杠不能删)
    location /api/ {
        proxy_pass http://go_backend_dh2/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # 其余一切请求全部交给 Go:首页、/news、/page/xxx、/themes/ 静态资源、
    # /uploads/ 以及以后新增的任何路由,自动生效,nginx 永远不用再改
    location / {
        proxy_pass http://go_backend_dh2;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    access_log  /www/wwwlogs/dh2.fun.log;
    error_log   /www/wwwlogs/dh2.fun.error.log;
}

# ==================== 租户官网渲染:*.dh2.fun 所有子域名 ====================
server {
    listen 80;
    listen 443 ssl;
    http2 on;

    # 匹配所有子域名,如 ceshi.dh2.fun(租户域名发放后无需再改 nginx)
    server_name ~^(?<subdomain>.+)\.dh2\.fun$;

    #CERT-APPLY-CHECK--START
    include /www/server/panel/vhost/nginx/well-known/dh2.fun.conf;
    #CERT-APPLY-CHECK--END

    #HTTP_TO_HTTPS_START
    set $isRedcert 1;
    if ($server_port != 443) {
        set $isRedcert 2;
    }
    if ( $uri ~ /\.well-known/ ) {
        set $isRedcert 1;
    }
    if ($isRedcert != 1) {
        rewrite ^(/.*)$ https://$host$1 permanent;
    }
    #HTTP_TO_HTTPS_END

    # 注意:子域名 HTTPS 要求泛域名证书(*.dh2.fun),见文档"注意事项"
    ssl_certificate    /www/server/panel/vhost/cert/dh2.fun/fullchain.pem;
    ssl_certificate_key    /www/server/panel/vhost/cert/dh2.fun/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
    ssl_prefer_server_ciphers on;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;
    add_header Strict-Transport-Security "max-age=31536000";
    error_page 497  https://$host$request_uri;

    location ~ \.well-known {
        allow all;
    }

    # 全部请求转发给 Go,由 Go 按 Host(如 ceshi.dh2.fun)识别租户并渲染官网
    location / {
        proxy_pass http://go_backend_dh2;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    access_log  /www/wwwlogs/dh2.fun.tenant.log;
    error_log   /www/wwwlogs/dh2.fun.tenant.error.log;
}

四、完整配置 —— 宝塔站点 yunzer.com.cn

文件位置:宝塔 → 网站 → yunzer.com.cn → 设置 → 配置文件。整份替换。

# ===== 后端 Go 服务地址统一定义,迁移时只改这一行 =====
# 注意:不能与 dh2.fun 配置里的 upstream 同名,否则 nginx 报 duplicate upstream
upstream go_backend_yunzer {
    server 10.31.100.2:8081;   # 若 nginx 与 Go 同机,建议改为 127.0.0.1:8081(迁移零改动)
}

# ==================== 主域名 yunzer.com.cn(全量托管给 Go)====================
server {
    listen 80;
    listen 443 ssl;
    listen 443 quic;
    http2 on;

    # 只匹配主域名(子域名由下方独立 server 块处理)
    server_name yunzer.com.cn;

    #CERT-APPLY-CHECK--START
    include /www/server/panel/vhost/nginx/well-known/yunzer.com.cn.conf;
    #CERT-APPLY-CHECK--END
    include /www/server/panel/vhost/nginx/extension/yunzer.com.cn/*.conf;

    #SSL-START SSL相关配置,请勿删除或修改下一行带注释的404规则
    #error_page 404/404.html;

    #HTTP_TO_HTTPS_START
    set $isRedcert 1;
    if ($server_port != 443) {
        set $isRedcert 2;
    }
    if ( $uri ~ /\.well-known/ ) {
        set $isRedcert 1;
    }
    if ($isRedcert != 1) {
        rewrite ^(/.*)$ https://$host$1 permanent;
    }
    #HTTP_TO_HTTPS_END

    ssl_certificate    /www/server/panel/vhost/cert/yunzer.com.cn/fullchain.pem;
    ssl_certificate_key    /www/server/panel/vhost/cert/yunzer.com.cn/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
    ssl_prefer_server_ciphers on;
    ssl_session_tickets on;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;
    add_header Strict-Transport-Security "max-age=31536000";
    error_page 497  https://$host$request_uri;
    #SSL-END

    # --- 安全拦截:敏感文件 ---
    location ~* (\.user.ini|\.htaccess|\.htpasswd|\.env.*|\.project|\.bashrc|\.bash_profile|\.bash_logout|\.DS_Store|\.gitignore|\.gitattributes|LICENSE|README\.md|CLAUDE\.md|CHANGELOG\.md|CHANGELOG|CONTRIBUTING\.md|TODO\.md|FAQ\.md|composer\.json|composer\.lock|package(-lock)?\.json|yarn\.lock|pnpm-lock\.yaml|\.\w+~|\.swp|\.swo|\.bak(up)?|\.old|\.tmp|\.temp|\.log|\.sql(\.gz)?|docker-compose\.yml|docker\.env|Dockerfile|\.csproj|\.sln|Cargo\.toml|Cargo\.lock|go\.mod|go\.sum|phpunit\.xml|phpunit\.xml|pom\.xml|build\.gradl|pyproject\.toml|requirements\.txt|application(-\w+)?\.(ya?ml|properties))$ {
        return 404;
    }

    # --- 安全拦截:敏感目录 ---
    location ~* /(\.git|\.svn|\.bzr|\.vscode|\.claude|\.idea|\.ssh|\.github|\.npm|\.yarn|\.pnpm|\.cache|\.husky|\.turbo|\.next|\.nuxt|node_modules|runtime)/ {
        return 404;
    }

    # SSL 证书验证目录
    location ~ \.well-known {
        allow all;
    }

    if ( $uri ~ "^/\.well-known/.*\.(php|jsp|py|js|css|lua|ts|go|zip|tar\.gz|rar|7z|sql|bak)$" ) {
        return 403;
    }

    # /api/ 转发并剥掉 /api 前缀(尾部斜杠不能删)
    location /api/ {
        proxy_pass http://go_backend_yunzer/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # 其余一切请求全部交给 Go:首页、/news、/page/xxx、/themes/ 静态资源、
    # /uploads/ 以及以后新增的任何路由,自动生效,nginx 永远不用再改
    location / {
        proxy_pass http://go_backend_yunzer;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    access_log  /www/wwwlogs/yunzer.com.cn.log;
    error_log   /www/wwwlogs/yunzer.com.cn.error.log;
}

# ==================== 租户官网渲染:*.yunzer.com.cn 所有子域名 ====================
server {
    listen 80;
    listen 443 ssl;
    http2 on;

    # 匹配所有子域名,如 ceshi.yunzer.com.cn(租户域名发放后无需再改 nginx)
    server_name ~^(?<subdomain>.+)\.yunzer\.com\.cn$;

    #CERT-APPLY-CHECK--START
    include /www/server/panel/vhost/nginx/well-known/yunzer.com.cn.conf;
    #CERT-APPLY-CHECK--END

    #HTTP_TO_HTTPS_START
    set $isRedcert 1;
    if ($server_port != 443) {
        set $isRedcert 2;
    }
    if ( $uri ~ /\.well-known/ ) {
        set $isRedcert 1;
    }
    if ($isRedcert != 1) {
        rewrite ^(/.*)$ https://$host$1 permanent;
    }
    #HTTP_TO_HTTPS_END

    # 注意:子域名 HTTPS 要求泛域名证书(*.yunzer.com.cn),见文档"注意事项"
    ssl_certificate    /www/server/panel/vhost/cert/yunzer.com.cn/fullchain.pem;
    ssl_certificate_key    /www/server/panel/vhost/cert/yunzer.com.cn/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
    ssl_prefer_server_ciphers on;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;
    add_header Strict-Transport-Security "max-age=31536000";
    error_page 497  https://$host$request_uri;

    location ~ \.well-known {
        allow all;
    }

    # 全部请求转发给 Go,由 Go 按 Host(如 ceshi.yunzer.com.cn)识别租户并渲染官网
    location / {
        proxy_pass http://go_backend_yunzer;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    access_log  /www/wwwlogs/yunzer.com.cn.tenant.log;
    error_log   /www/wwwlogs/yunzer.com.cn.tenant.error.log;
}

若管理端(platform/backend 前端)部署在专属子域名(如 admin.yunzer.com.cn),在对应站点再加一个 server_name admin.yunzer.com.cn; 精确匹配的 server 块指向其静态目录即可——精确匹配优先于子域名正则块,互不干扰。


五、重要注意事项

  1. 子域名必须用泛域名证书。租户域名是二级域名(ceshi.yunzer.com.cn),单域名证书会导致 HTTPS 证书错误。需在宝塔为 *.yunzer.com.cn、*.dh2.fun 申请泛域名证书(DNS 验证),并替换配置中的证书路径。
  2. upstream 不能重名。两个站点配置都在同一 http 上下文,本文分别用 go_backend_dh2、go_backend_yunzer;重名会 duplicate upstream 启动失败。
  3. /api/ 的 proxy_pass 尾部斜杠不能删,它负责剥掉 /api 前缀(Go 路由注册在 /backend/*、/platform/*)。兜底 location / 的 proxy_pass 则不带尾部斜杠(原样转发 URI)。
  4. 不要再给官网域名加 js|css 正则缓存 location。正则 location 优先级高于兜底前缀 location,会拦截 /themes/xxx/static/main.js 去本地 root 找文件导致 404。模板静态资源由 Go(beego)直接提供。
  5. proxy_set_header Host $host; 必须保留。Go 完全依赖 Host 识别租户;若透传成 upstream 地址,所有请求都会被识别失败。
  6. Go 侧前置条件:
    • 启动模式 APP_MODE 为 all(默认)或 index,否则官网渲染路由未注册;
    • go/themes/ 目录与 Go 服务启动目录同级(模板文件在 Go 项目目录 go/themes/{编码}/,与 platform/backend 前端项目无关);
    • 数据库已执行 sql/yz_cms_frontend_tables.sql。
  7. 域名发放数据必须精确。yz_system_tenant_domain.full_domain 要与实际访问域名完全一致(不带协议、不带端口、不带路径),且 status=1、delete_time 为 NULL。
  8. DNS 泛解析。*.yunzer.com.cn、*.dh2.fun 需要配置泛解析 A 记录指向服务器,否则新发放的子域名无法访问(与 nginx 无关)。
  9. HSTS 提醒。配置了 Strict-Transport-Security,一旦浏览器记住,证书出问题期间该域名将无法用 HTTP 访问;测试阶段可先注释掉该行。
  10. 宝塔面板操作提示。直接编辑站点配置文件后,用 nginx -t 校验再重载;宝塔后续"站点设置"的某些操作可能重写配置文件,改前建议备份。

六、验证与排查

6.1 部署后验证顺序

# 1. 绕过 nginx 直接验证 Go(在服务器上执行)
curl -H "Host: ceshi.yunzer.com.cn" http://10.31.100.2:8081/

# 2. 验证 nginx → Go 链路
curl -k -H "Host: ceshi.yunzer.com.cn" https://127.0.0.1/

# 3. 浏览器访问 https://ceshi.yunzer.com.cn

6.2 常见现象对照表

现象 原因 处理
Go 页面显示"站点不存在" Host 在 yz_system_tenant_domain / 域名池中查不到 核对 full_domain 拼写、status、delete_time
beego 默认 404 APP_MODE 不含 all/index,渲染路由未注册 检查 Go 启动环境变量
nginx 404(未到 Go) 请求被本地静态/正则规则拦截 检查是否残留 try_files /index.html、`js
浏览器证书错误 非泛域名证书 申请 *.域名 泛域名证书并替换路径
502 Bad Gateway Go 服务未启动或 upstream 地址错误 curl http://127.0.0.1:8081/ 验证服务存活,改 upstream
首页正常但样式丢失 /themes/ 未到达 Go 确认兜底转发存在且无 js/css 正则拦截

七、迁移服务器清单(以迁移到 192.168.1.101 为例)

  1. 新机器部署 Go:复制 go/ 目录(含 themes/、conf/app.conf),修改 app.conf 数据库连接为新库地址,启动服务;
  2. 新库初始化:导入业务数据,执行 sql/ 下全部建表/变更 SQL(尤其 yz_cms_frontend_tables.sql);
  3. nginx:
    • nginx 与 Go 同机 → upstream 用 127.0.0.1:8081,配置零改动;
    • 跨机 → 只改两个 upstream 块中的 server 一行;
  4. 证书:新机器宝塔上为两个域名重新申请/导入证书(含泛域名),路径与配置保持一致;
  5. DNS:将 dh2.fun、yunzer.com.cn 及泛解析 * 的 A 记录改指向新服务器 IP;
  6. 验证:按第六节顺序逐一验证;
  7. 确认无误后到域名服务商处调低旧解析 TTL、观察切换。

本文档对应代码:go/controllers/tenant_site.go(Host 识别与渲染)、go/pkg/tagengine/(标签引擎)、go/themes/default/(内置默认模板)、sql/yz_cms_frontend_tables.sql(建站数据表)。