Skip to main content

内外网分离部署

管理员在内网设计问卷、查看答卷,填写者通过外网链接作答,也可以用带预览凭证的链接查看问卷。部署时需要完成三项配置:

  1. 建立外网网关能够访问的转发通道,把页面和接口请求送到内网 Nginx。
  2. 在外网 Nginx 限制可以访问的路径,只开放填写、预览及其所需资源和接口。
  3. 设置系统地址,让内网生成的发布链接和二维码使用外网域名。

本文使用当前 public / admin 两套前端交付包,按根目录部署。安装基础环境和应用的方法见基础软件环境安装与标准部署。

有内外网两台 Nginx 时,依次完成步骤 1、2、3;同一台服务器同时提供管理和填写入口时,使用单台 Nginx 配置,再设置系统地址。

先确定网络转发链路​

外网 Nginx 的上游要填写“从外网网关实际可达的映射地址和端口”。 外网服务器未必能直接连接内网 IP,不能把内网 Nginx 的监听地址照搬到外网 upstream 中。

本文采用以下链路:

其中 192.0.2.10 是文档示例地址,使用时必须替换。网络设备、负载均衡或转发服务需要事先建立这条映射;仅修改 Nginx 配置不会自动打通内外网。

配置项本文示例填写位置与用途
内网管理地址http://admin.example.internal:8080管理员访问设计、数据和系统设置
外网填写地址https://survey.example.com用户访问填写页和预览页;同时填写到系统地址
外网侧映射入口192.0.2.10:18080外网 Nginx 的 upstream,由网络侧提供
映射目标10.24.10.24:8080网络设备的转发目标,对应内网 Nginx
内网 Nginx 监听端口8080内网配置的 listen
内网实际看到的转发来源10.24.10.10内网来源限制与可信协议头判断;以 NAT / 转发后的地址为准
管理员办公网段10.24.20.0/24允许直接访问内网管理入口的网段
Java 后端127.0.0.1:48080只填写在内网 Nginx 的后端 upstream 中
前端目录/opt/surveyking/client/dist包含 public/ 与 admin/ 两个目录

这里有三个不同的服务端口:映射入口的 18080、内网 Nginx 的 8080、Java 的 48080。它们不需要相同。修改内网监听端口时,应修改网络映射的目标;只有映射入口本身变化时,才修改外网 upstream。

外网域名解析到外网 Nginx,用户从该域名进入。映射入口只允许外网网关访问,否则用户可能绕过路径白名单直达完整的内网站点。Java、数据库、Redis 不做公网端口映射。

本文的映射入口使用 HTTP。若网络侧提供的是 HTTPS 入口,应按其证书和域名配置上游 TLS。无论使用哪种转发服务,都应保留请求路径、查询参数及指定的 Host;HTTP 转发层还需保留原始 X-Forwarded-Proto,避免把外部 HTTPS 覆盖成链路内部的 HTTP。

1. 配置内网 Nginx​

先将前端交付包部署到内网服务器,确认以下文件存在:

/opt/surveyking/client/dist/
├── public/index.html
└── admin/index.html

内网 Nginx 负责提供两套前端文件,并将 API 请求转发给 Java 后端。本文配置中:

  • /s/...、360、练习等填写页面使用 public/index.html。
  • /preview?key=... 使用 admin/index.html,这是当前版本的路由归属。
  • 管理页面使用 admin/index.html,外网是否可以访问由外网白名单控制。

下面是内网服务器的完整 nginx.conf。替换监听端口、域名、目录、Java 后端地址,以及内网实际看到的可信转发来源 IP。将外网域名也加入 server_name,以接收网关转发的 Host。

下载内网完整配置

已有主 nginx.conf、希望用 include 引入时,请使用后文的内网站点片段与主配置示例,不要直接将这份完整配置放进 http。

内网 /etc/nginx/nginx.conf
# 内网服务器上的完整 nginx.conf。替换域名、网段和目录后使用。
# 含 events / http 外壳,用作主配置;通过现有 http 引入时使用 surveyking-internal.http.conf。
# 同一示例地址或目录出现多次时,请同步替换,保持一致。
worker_processes auto;
# pid 和日志目录如与本机 Nginx 安装位置不同,请按实际路径修改。
pid /var/run/nginx.pid;
error_log /var/log/nginx/error.log warn;

events {
worker_connections 1024;
}

http {
# 改为本机 Nginx 的 mime.types 文件路径。
include /etc/nginx/mime.types;
default_type application/octet-stream;
server_tokens off;
sendfile on;

# 访问日志保留实际连接来源,便于核对映射后的 IP;不记录预览 key 等查询参数。
log_format survey_internal '$remote_addr [$time_local] '
'$request_method $uri $status $body_bytes_sent';
access_log /var/log/nginx/access.log survey_internal;

# 仅接受可信转发来源传入的协议,内网直接访问使用自身协议。
map "$remote_addr:$http_x_forwarded_proto" $sk_original_scheme {
default $scheme;
# 改为经过映射 / NAT / 转发后,内网 Nginx 实际看到的可信来源 IP。
# 不一定是外网 Nginx 的 IP,也不是外网侧映射入口的 IP;与下方 allow 同步修改。
"10.24.10.10:https" https;
"10.24.10.10:http" http;
}

upstream surveyking_backend {
# 改为 Java 后端的实际 IP 和端口;此例为同机部署、端口 48080。
server 127.0.0.1:48080;
}

server {
# 内网 Nginx 的实际监听端口;修改后同步更新网络映射的目标端口。
# 此端口无需与外网侧映射入口 192.0.2.10:18080 的端口相同。
listen 8080 default_server;
# 分别改为实际的内网管理域名、外网填写域名,不带协议或路径。
server_name admin.example.internal survey.example.com;
client_max_body_size 100m;
index index.html;

# 改为内网实际看到的可信转发来源 IP,与上方协议 map 一致。
# 只有外网网关能通过映射入口到达此站点,避免绕过外网路径白名单。
allow 10.24.10.10;
# 改为允许访问后台的管理员办公网段,保留正确的 CIDR 掩码。
allow 10.24.20.0/24;
allow 127.0.0.1;
deny all;
# 如本机已由网络层隔离,且决定不在 Nginx 中限制内网来源:
# 删除上面整组 allow / deny;协议 map 的可信来源判断仍需保留。

proxy_http_version 1.1;
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 $sk_original_scheme;
proxy_set_header Connection "";
proxy_connect_timeout 60s;
proxy_read_timeout 1800s;
proxy_send_timeout 1800s;
proxy_cache off;

location = /pub/index.html {
# 改为 public 构建文件的实际目录,所有 public 路径同步修改。
root /opt/surveyking/client/dist/public;
add_header Cache-Control "no-store" always;
try_files /index.html =404;
}

location = /adm/index.html {
# 改为 admin 构建文件的实际目录,所有 admin 路径同步修改。
root /opt/surveyking/client/dist/admin;
add_header Cache-Control "no-store" always;
try_files /index.html =404;
}

location ^~ /pub/ {
# 改为实际 public 目录,保留末尾斜杠。
alias /opt/surveyking/client/dist/public/;
}

location ^~ /adm/ {
# 改为实际 admin 目录,保留末尾斜杠。
alias /opt/surveyking/client/dist/admin/;
}

location = /favicon.ico {
# 改为实际 public 目录,与上方 public 路径一致。
root /opt/surveyking/client/dist/public;
try_files /favicon.ico =404;
}

# 当前版本扫码预览使用 admin 入口,不能改到 public 目录。
location ~ ^/preview/?$ {
# 改为实际 admin 目录,与其他 admin 路径一致。
root /opt/surveyking/client/dist/admin;
add_header Cache-Control "no-store" always;
try_files /index.html =404;
}

location ~ ^/(s|e|t|q|share|redeem)(/|$) {
# 改为实际 public 目录,与上方 public 路径一致。
root /opt/surveyking/client/dist/public;
add_header Cache-Control "no-store" always;
try_files /index.html =404;
}

location ~ ^/user/(login|register|binding)/?$ {
# 改为实际 public 目录,与上方 public 路径一致。
root /opt/surveyking/client/dist/public;
add_header Cache-Control "no-store" always;
try_files /index.html =404;
}

location ^~ /admin-api/ {
# surveyking_backend 对应上方 upstream 名称,后端地址在 upstream 中修改。
proxy_pass http://surveyking_backend;
proxy_buffering off;
}

location ^~ /captcha/ {
# 与上方 Java 后端的 upstream 名称保持一致。
proxy_pass http://surveyking_backend;
}

# 管理页面只在内网使用,外网网关不会放行这些路径。
location / {
# 改为实际 admin 目录,与上方 admin 路径一致。
root /opt/surveyking/client/dist/admin;
add_header Cache-Control "no-store" always;
try_files /index.html =404;
}
}
}

内网不限制来源 IP 时​

双机方案中的内网站点如果已经由网络层隔离,并决定不再由 Nginx 限制访问来源,可以删除 server 中整组:

allow 10.24.10.10;
allow 10.24.20.0/24;
allow 127.0.0.1;
deny all;

不要只删除 allow 而留下 deny all。上方判断可信 X-Forwarded-Proto 的 map 仍应保留:它用于识别原始访问协议,与是否允许访问是两件事。

此选项针对已隔离的内网站点。单台 Nginx 方案的管理站点仍需保留来源限制,因为同一实例同时接收外网请求。

先从外网网关验证映射入口​

网络映射就绪、内网配置生效后,在外网 Nginx 所在服务器执行以下请求。将映射入口、Host 和问卷 ID 换为实际值:

# 此处连接的是网络侧提供的映射入口,不是内网 IP,也不是 Java 端口。
curl -i --connect-timeout 5 --max-time 15 \
-H 'Host: survey.example.com' \
'http://192.0.2.10:18080/s/实际问卷ID'

正常应返回问卷入口 HTML。若连接超时或被拒绝,先核对网络映射、防火墙与目标服务;若返回错误站点,核对映射目标、Host 和内网 server_name。只有这一段可达,外网 Nginx 的代理才能正常工作。

2. 配置外网 Nginx​

外网 Nginx 只连接映射入口,本机不需要上传前端文件。请求经过白名单后,由网络通道转发到内网 Nginx。

/admin-api/survey/ 下的接口统一放行,无需逐个维护问卷加载、校验、提交、暂存、上传与预览接口。请求方法、项目参与权限和数据权限仍由后端校验。

默认放行内容用途
GET/HEAD /s/:id 及其子路径填写、答卷查看、考试与维度结果、证书、抽奖、模板打印等已配置页面
GET/HEAD /preview打开带 key 的问卷预览链接
GET/HEAD /pub/ 下指定类型的静态文件加载填写端的脚本、样式、图片与字体
GET/HEAD /adm/ 下指定类型的静态文件加载当前版本预览页所需的 admin 构建资源
GET /admin-api/system/auth/get-sys-info读取系统名称、主题等基础信息
/admin-api/survey/ 下的全部接口填写、校验、提交、预览、关联回填与上传
验证码接口、指定项目文件读取接口人机验证、题目素材和受保护的答题附件

后台首页、/project/、/infra/、/adm/index.html、源码映射文件,以及项目、答卷和系统管理 API 均不开放。 放行 /adm/ 下指定类型的静态资源,是为了让预览页面正常加载;它不等于开放管理页面或管理接口。

下面是外网网关的完整 nginx.conf。逐项替换外网域名、证书路径和映射入口。注意 upstream surveyking_forward 中的 192.0.2.10:18080,应改为网络侧提供的接入地址,不能照抄内网服务器地址或 Java 端口。

下载外网完整配置

已有主 nginx.conf、希望用 include 引入时,请使用后文的外网站点片段与主配置示例。

外网 /etc/nginx/nginx.conf
# 外网网关上的完整 nginx.conf。替换域名、证书和映射入口后使用。
# 含 events / http 外壳,用作主配置;通过现有 http 引入时使用 surveyking-external.http.conf。
# 基线:匿名问卷、问卷预览、/s/ 下的结果页与项目图片/附件;其他功能按文档扩展。
# 外网 Nginx -> 外网侧可达的映射入口 -> 内网 Nginx -> 前端文件 / Java 后端。
# 映射由网络设备或转发服务提前建立,本文件不会自动建立内外网通道。
# 同一示例域名出现多次时,请同步替换为实际的外网填写域名。
worker_processes auto;
# pid 和日志目录如与本机 Nginx 安装位置不同,请按实际路径修改。
pid /var/run/nginx.pid;
error_log /var/log/nginx/error.log warn;

events {
worker_connections 1024;
}

http {
# 改为本机 Nginx 的 mime.types 文件路径。
include /etc/nginx/mime.types;
default_type application/octet-stream;
server_tokens off;
# 为按需增加的登录权限等较长的白名单键预留空间。
map_hash_bucket_size 128;

# 不记录查询参数,避免保存预览 key、填写凭证或附件凭证。
# upstream 字段用于核对实际连接的映射入口及其响应状态。
log_format survey_gateway '$remote_addr [$time_local] '
'$request_method $uri $status $body_bytes_sent '
'upstream=$upstream_addr upstream_status=$upstream_status';
access_log /var/log/nginx/access.log survey_gateway;

upstream surveyking_forward {
# 改为网络侧提供的、从外网 Nginx 所在服务器能够访问的映射 IP/域名和端口。
# 192.0.2.10 是文档示例地址,必须替换;18080 是映射入口端口,也须按实际修改。
# 映射关系示例:192.0.2.10:18080 -> 内网 Nginx 10.24.10.24:8080。
# 两端端口可以不同;不要直接照抄内网地址,也不要映射到 Java 后端端口。
# 本例映射入口使用 HTTP;该入口仅允许外网网关访问,不能供填写者绕过白名单直连。
server 192.0.2.10:18080;
}

# 使用规范化路径 $uri 判断,查询参数不参与路由匹配。
# survey 答题接口统一放行;其他路径默认拒绝。
map "$request_method:$uri" $sk_public_allowed {
default 0;

# 普通问卷及 /s/ 下的答卷、结果、证书、抽奖、模板打印等页面。
~^(GET|HEAD):/s/[A-Za-z0-9_-]+(/.*)?$ 1;

# 问卷预览。?key=... 不参与 $uri 匹配,仍会原样转发到内网。
~^(GET|HEAD):/preview/?$ 1;

# public 构建资源,以及当前版本预览页所需的 admin 构建资源。
# 仅允许指定类型的静态文件,不放行 /adm/index.html、后台页面或 .map 文件。
~^(GET|HEAD):/pub/.+\.(js|mjs|css|png|jpe?g|gif|svg|webp|avif|ico|woff2?|ttf|otf|eot|wasm|json)$ 1;
~^(GET|HEAD):/adm/.+\.(js|mjs|css|png|jpe?g|gif|svg|webp|avif|ico|woff2?|ttf|otf|eot|wasm|json)$ 1;
~^(GET|HEAD):/favicon\.ico$ 1;

# public 入口启动时读取的系统基础信息。
"GET:/admin-api/system/auth/get-sys-info" 1;

# 统一放行 survey 下的全部接口,无需逐个维护接口名称。
# 请求方法、参与权限与数据权限继续由后端校验。
~^[A-Z]+:/admin-api/survey/ 1;

# 位于 survey 前缀之外的滑动验证码接口。
"POST:/captcha/get" 1;
"POST:/captcha/check" 1;

# 当前版本的项目素材与受保护答题附件读取入口。
# 私有附件仍需后端校验 attachmentToken,不能用 alias 绕过。
~^(GET|HEAD):/admin-api/infra/file/[0-9]+/get/project/[A-Za-z0-9_-]+/(editor|answer-private)/.+$ 1;
}

# 非指定域名的 HTTP 请求不会转发。
server {
listen 80 default_server;
server_name _;
return 404;
}

server {
listen 80;
# 改为实际外网填写域名,与下方 HTTPS 配置、Host 校验保持一致。
server_name survey.example.com;
if ($sk_public_allowed = 0) { return 404; }
# 改为实际外网 HTTPS 地址,保留 $request_uri;非 443 端口需在域名后注明。
return 308 https://survey.example.com$request_uri;
}

server {
# HTTPS 监听端口;如改为非 443,系统地址和上方重定向地址也需带端口。
listen 443 ssl default_server;
# 改为同一个外网填写域名,不带协议或端口。
server_name survey.example.com;
# 改为该外网域名对应的完整证书链文件路径。
ssl_certificate /etc/nginx/certs/survey.example.com/fullchain.pem;
# 改为与上述证书配套的私钥文件路径。
ssl_certificate_key /etc/nginx/certs/survey.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
client_max_body_size 100m;

# 即使伪造内网域名,也不能经此网关访问后台。
# 此处也要改为实际外网填写域名,与 server_name 一致。
if ($host != survey.example.com) { return 404; }

# 本站只有这一处转发入口;不添加其他可绕过白名单的 location。
location / {
if ($sk_public_allowed = 0) { return 404; }

# 不附加 URI 或尾斜杠,保留请求路径和原有查询参数。
# 只连接上方配置的映射入口;到内网 Nginx 的转发由网络侧完成。
proxy_pass http://surveyking_forward;
proxy_http_version 1.1;
# 改为实际外网填写域名,并将该域名加入内网 Nginx 的 server_name。
# 映射入口如为 HTTP 代理,也必须保留此 Host 和下面的原始协议信息。
proxy_set_header Host survey.example.com;
proxy_set_header X-Real-IP $remote_addr;
# 外网边界覆盖客户端自带的转发头。
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Connection "";
proxy_connect_timeout 60s;
proxy_read_timeout 1800s;
proxy_send_timeout 1800s;
proxy_buffering off;
proxy_cache off;
}
}
}

白名单使用 $uri,查询参数不参与路径匹配;proxy_pass 不附加 URI,使填写参数、预览 key 和附件凭证随请求继续转发。规则说明见 Nginx map 文档和 proxy_pass 文档。

问卷预览需要同时放行页面和资源​

完整配置已经包含以下规则;如果是在现有外网配置上补充预览,只需将这两条加入原来的 map,保留已经验证可用的映射入口和其他配置:

# 查询参数不参与匹配,/preview?key=... 会命中。
~^(GET|HEAD):/preview/?$ 1;

# 当前版本预览页需要 admin 构建中的脚本、样式、字体等。
~^(GET|HEAD):/adm/.+\.(js|mjs|css|png|jpe?g|gif|svg|webp|avif|ico|woff2?|ttf|otf|eot|wasm|json)$ 1;

内网 /preview 必须返回 dist/admin/index.html。只放行页面却拦截 /adm/ 资源,会导致页面空白或加载失败;改用 public 入口,则会缺少预览路由。

预览页面通过 POST /admin-api/survey/preview 加载内容,该接口已包含在 survey 前缀中。后端会校验预览 key,key 过期时应从设计端重新生成;不需要开放整个认证接口或项目管理接口。预览链接包含访问凭证,不应放进公开示例;本文配置的访问日志不记录查询参数。

主 nginx.conf 如何引入内外网配置​

nginx -t 从主配置文件开始检查,并继续加载其中所有 include。只把一个文件复制到服务器、但没有被主配置引入,不会让它生效。

前面下载的 nginx-internal.conf 和 nginx-external.conf 已包含 worker_processes、events、http,可以各自作为对应服务器的主配置。不要把完整文件再 include 到另一个 http 中。 使用现有主配置时,改用以下已拆好的 HTTP 配置片段:

部署位置下载文件保存位置示例
内网 Nginx 服务器内网 HTTP 配置片段/etc/nginx/surveyking/surveyking-internal.http.conf
外网 Nginx 服务器外网 HTTP 配置片段/etc/nginx/surveyking/surveyking-external.http.conf

这两份片段保留各自的 log_format、map、upstream 和 server,去掉了主配置外壳及公共设置;访问日志设置在各自站点中。双机部署时,两台服务器分别引入自己的文件;同一实例同时提供内外网入口时,使用单台 Nginx 方案。

下面给出最小主配置示例。已有其他网站时,在现有 http 内补充对应的 include 即可,保留其他业务配置;mime.types、default_type 等公共设置不要重复添加。示例中的路径都应按实际安装位置调整。

内网服务器的主配置:

内网主配置 /etc/nginx/nginx.conf
worker_processes auto;
# 按实际 Nginx 安装位置修改 pid、日志和 mime.types 路径。
pid /var/run/nginx.pid;
error_log /var/log/nginx/error.log warn;

events {
worker_connections 1024;
}

http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
server_tokens off;
sendfile on;

# 在内网服务器引入内网片段;不要放到 server 或 location 内。
include /etc/nginx/surveyking/surveyking-internal.http.conf;
}

外网服务器的主配置:

外网主配置 /etc/nginx/nginx.conf
worker_processes auto;
# 按实际 Nginx 安装位置修改 pid、日志和 mime.types 路径。
pid /var/run/nginx.pid;
error_log /var/log/nginx/error.log warn;

events {
worker_connections 1024;
}

http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
server_tokens off;
map_hash_bucket_size 128;

# 在外网服务器引入外网片段;片段中的 upstream 仍填写可达的映射入口。
include /etc/nginx/surveyking/surveyking-external.http.conf;
}

先创建 /etc/nginx/surveyking/ 目录,将对应片段放入其中,并替换片段内的域名、地址、前端目录、证书和日志路径。若已有 /etc/nginx/conf.d/*.conf 等通配引入规则,不要再把同一个片段放入该目录并重复 include,否则可能出现 upstream、日志格式或默认站点重复定义。include 的语法见 Nginx 官方说明。

校验时指定主配置,不能把 HTTP 片段作为 -c 的目标:

# 在内网、外网服务器上分别执行;路径换成各自正在使用的主配置。
sudo nginx -t -c /etc/nginx/nginx.conf

# 查看展开后的主配置和所有 include 文件,确认对应片段确实被加载。
sudo nginx -T -c /etc/nginx/nginx.conf

# 校验成功后,向使用该主配置和 pid 文件的 Nginx 实例发送重载信号。
sudo nginx -s reload -c /etc/nginx/nginx.conf

-t 不会自动加载当前目录下的所有 .conf;-c 指定从哪份主配置开始,-T 会额外输出实际加载的配置文件。配置校验也不会检查跨网映射能否连接,仍需完成前面的映射入口访问验证。参数说明见 Nginx 命令行文档。

**宝塔环境:**主配置通常是 /www/server/nginx/conf/nginx.conf,其 http 内常见以下规则:

# 已有此规则时,将对应 HTTP 片段放入该目录即可,不再重复 include。
include /www/server/panel/vhost/nginx/*.conf;

在内网机器只放内网片段,外网机器只放外网片段;合并替换同一站点原有规则,不额外保留另一份重复的站点配置。确认该通配规则实际位于 http 内,并使用宝塔自身的 Nginx 检查:

sudo /www/server/nginx/sbin/nginx -t -c /www/server/nginx/conf/nginx.conf
sudo /www/server/nginx/sbin/nginx -T -c /www/server/nginx/conf/nginx.conf
# 上面的校验通过后再执行,也可以通过宝塔面板重载。
sudo /www/server/nginx/sbin/nginx -s reload -c /www/server/nginx/conf/nginx.conf

若不确定实际安装位置,先查看 nginx -V 中的 --conf-path,并核对服务启动命令是否另行指定了 -c。宝塔“伪静态”输入框不适合粘贴完整站点片段。

location 应位于对应的 server 内。出现 location "/pub" is outside location "\.(html|htm)$" 时,检查是否把新规则粘贴进了旧 HTML 规则的花括号中。合并时清理本网站旧的通配转发规则,保留其他业务配置。

图片、附件和旧文件路径​

默认只放行 project/<项目 ID>/editor/ 和 project/<项目 ID>/answer-private/ 的文件读取,不开放文件管理的上传、分页、删除等接口。问卷中的文件上传使用已放行的 /admin-api/survey/ 接口。

在 基础设施 → 文件管理 → 文件配置 中,确认图片和附件 URL 从外网可达。可以使用外网填写域名或独立文件服务域名。修改“系统地址”不会自动替换已保存的图片、附件地址或对象存储域名。

旧文件、系统 Logo、公共素材可能采用其他目录。先通过浏览器网络面板确认实际 URL,再补充所需配置编号和目录的只读规则。私有答题附件仍需后端校验短期 attachmentToken,不能通过 Nginx alias 直接暴露私有文件目录。详情见文件存储。

同一台 Nginx:使用一个配置文件​

如果前端文件和 API 代理本来就在同一台 Nginx 上,可以用同一个 nginx.conf 中的多个 server 区分内网管理和外网填写。此方案不经过前面的跨网映射入口。

入口行为
http://admin.example.internal仅允许办公网段访问,保留完整管理功能
https://survey.example.com使用与双机方案相同的填写、预览和接口白名单

内网域名解析到本机内网地址,外网域名解析到公网地址或对应的公网映射地址。管理站点同时使用 allow / deny 限制来源,不能仅靠域名区分访问权限。Nginx 会按监听地址和 Host 选择站点,见请求处理说明。

这份配置由本机直接提供 public 与 admin 文件,只把 API 代理到本机 Java 的 127.0.0.1:48080。外网 /preview 有单独的 admin 入口,其他填写页面使用 public 入口。

下载单台 Nginx 完整配置

单台 Nginx /etc/nginx/nginx.conf
# 同一台 Nginx 的完整 nginx.conf:内网管理、外网填写及预览,共用 Java 后端。
# 仅用于本机同时提供前端和 API 代理的场景;跨内外网请使用 external / internal 配置。
# 同一示例域名或目录出现多次时,请同步替换,保持一致。
worker_processes auto;
# pid 和日志目录如与本机 Nginx 安装位置不同,请按实际路径修改。
pid /var/run/nginx.pid;
error_log /var/log/nginx/error.log warn;

events {
worker_connections 1024;
}

http {
# 改为本机 Nginx 的 mime.types 文件路径。
include /etc/nginx/mime.types;
default_type application/octet-stream;
server_tokens off;
sendfile on;
map_hash_bucket_size 128;
client_max_body_size 100m;

# 不记录查询参数,避免保存预览 key、填写凭证或附件凭证。
log_format survey_gateway '$remote_addr [$time_local] '
'$request_method $uri $status $body_bytes_sent';
access_log /var/log/nginx/access.log survey_gateway;

upstream surveyking_backend {
# 改为 Java 后端的实际 IP 和端口;此例为同机部署、端口 48080。
# 此单文件方案直接连接 Java 后端,此处不要填写 Nginx 自身的监听端口。
server 127.0.0.1:48080;
}

# 仅用于外网填写站点,管理站点不使用这份路径白名单。
map "$request_method:$uri" $sk_public_allowed {
default 0;

# 普通问卷及 /s/ 下的结果、证书、打印等页面。
~^(GET|HEAD):/s/[A-Za-z0-9_-]+(/.*)?$ 1;

# 扫码预览使用 admin 入口,同时放行它所需的指定类型静态资源。
~^(GET|HEAD):/preview/?$ 1;
~^(GET|HEAD):/pub/.+\.(js|mjs|css|png|jpe?g|gif|svg|webp|avif|ico|woff2?|ttf|otf|eot|wasm|json)$ 1;
~^(GET|HEAD):/adm/.+\.(js|mjs|css|png|jpe?g|gif|svg|webp|avif|ico|woff2?|ttf|otf|eot|wasm|json)$ 1;
~^(GET|HEAD):/favicon\.ico$ 1;

# 答题端初始化,以及 survey 前缀下的全部接口。
"GET:/admin-api/system/auth/get-sys-info" 1;
~^[A-Z]+:/admin-api/survey/ 1;

# 位于 survey 前缀之外的滑动验证码接口。
"POST:/captcha/get" 1;
"POST:/captcha/check" 1;

# 项目素材与受保护答题附件;私有附件仍由后端校验 attachmentToken。
~^(GET|HEAD):/admin-api/infra/file/[0-9]+/get/project/[A-Za-z0-9_-]+/(editor|answer-private)/.+$ 1;
}

# 所有 API 代理共用这些设置;本例由客户端直接连接此 Nginx。
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# 覆盖客户端自带的转发头,访问控制始终按实际来源地址判断。
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Connection "";
proxy_connect_timeout 60s;
proxy_read_timeout 1800s;
proxy_send_timeout 1800s;
proxy_buffering off;
proxy_cache off;

# 未匹配到配置域名的 HTTP 请求直接拒绝。
server {
listen 80 default_server;
server_name _;
return 404;
}

# 内网管理站点:完整管理功能,且所有 location 都继承来源地址限制。
server {
listen 80;
# 改为实际内网管理域名,只在内网 DNS 中解析到本机内网 IP。
server_name admin.example.internal;

# 改为实际管理员办公网段;不要把公网代理或 NAT 网关的地址当成办公网段。
allow 10.24.20.0/24;
allow 127.0.0.1;
deny all;

# 改为 admin 构建文件的实际目录,下方 admin 路径同步修改。
root /opt/surveyking/client/dist/admin;

location = /pub/index.html {
# 改为 public 构建文件的实际目录,下方 public 路径同步修改。
root /opt/surveyking/client/dist/public;
add_header Cache-Control "no-store" always;
try_files /index.html =404;
}

location = /adm/index.html {
add_header Cache-Control "no-store" always;
try_files /index.html =404;
}

location ^~ /pub/ {
# 改为实际 public 目录,保留末尾斜杠。
alias /opt/surveyking/client/dist/public/;
}

location ^~ /adm/ {
# 改为实际 admin 目录,保留末尾斜杠。
alias /opt/surveyking/client/dist/admin/;
}

location = /favicon.ico {
# 改为实际 public 目录,与其他 public 路径一致。
root /opt/surveyking/client/dist/public;
try_files /favicon.ico =404;
}

location ~ ^/(s|e|t|q|share|redeem)(/|$) {
# 改为实际 public 目录,与其他 public 路径一致。
root /opt/surveyking/client/dist/public;
add_header Cache-Control "no-store" always;
try_files /index.html =404;
}

location ~ ^/user/(login|register|binding)/?$ {
# 改为实际 public 目录,与其他 public 路径一致。
root /opt/surveyking/client/dist/public;
add_header Cache-Control "no-store" always;
try_files /index.html =404;
}

location ^~ /admin-api/ {
# 后端 IP 和端口在上方 surveyking_backend 中修改,保留完整请求路径。
proxy_pass http://surveyking_backend;
}

location ^~ /captcha/ {
proxy_pass http://surveyking_backend;
}

location / {
# 使用本 server 上方配置的 admin 目录。
add_header Cache-Control "no-store" always;
try_files /index.html =404;
}
}

# 外网 HTTP 入口:只将已放行的路径跳转到 HTTPS。
server {
listen 80;
# 改为实际外网填写域名,与下方 HTTPS 站点和 Host 校验保持一致。
server_name survey.example.com;
if ($sk_public_allowed = 0) { return 404; }
# 改为实际外网 HTTPS 地址,保留 $request_uri;非 443 端口需在域名后注明。
return 308 https://survey.example.com$request_uri;
}

# 外网 HTTPS 入口:所有页面、静态资源和 API 都先经过白名单。
server {
# 如改为非 443 端口,系统地址和上方重定向地址也需带端口。
listen 443 ssl default_server;
# 改为同一个外网填写域名,不带协议或端口。
server_name survey.example.com;
# 改为该外网域名对应的完整证书链和私钥文件路径。
ssl_certificate /etc/nginx/certs/survey.example.com/fullchain.pem;
ssl_certificate_key /etc/nginx/certs/survey.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;

# 此处也要改为实际外网填写域名,与 server_name 一致。
if ($host != survey.example.com) { return 404; }
# 在 server 级统一检查,覆盖下方每个 location。
if ($sk_public_allowed = 0) { return 404; }

# 改为实际 public 目录,与内网站点的 public 路径一致。
root /opt/surveyking/client/dist/public;

location ^~ /pub/ {
# 改为实际 public 目录,保留末尾斜杠。
alias /opt/surveyking/client/dist/public/;
}

# 预览页所需的 admin 静态资源;HTML 直达入口仍被上方白名单拒绝。
location ^~ /adm/ {
# 改为实际 admin 目录,与内网管理站点一致,保留末尾斜杠。
alias /opt/surveyking/client/dist/admin/;
}

# 当前版本 /preview 属于 admin 构建,必须单独返回 admin 入口。
location ~ ^/preview/?$ {
# 改为实际 admin 目录,与其他 admin 路径一致。
root /opt/surveyking/client/dist/admin;
add_header Cache-Control "no-store" always;
try_files /index.html =404;
}

location = /favicon.ico {
try_files /favicon.ico =404;
}

location ^~ /admin-api/ {
# 只转发已通过上方白名单的 API;后端地址在 upstream 中修改。
proxy_pass http://surveyking_backend;
}

location ^~ /captcha/ {
proxy_pass http://surveyking_backend;
}

location / {
# 除上方单独处理的预览页外,其余已放行页面返回 public 入口。
add_header Cache-Control "no-store" always;
try_files /index.html =404;
}
}
}

按实际修改域名、证书、办公网段、前端目录和 Java 后端地址。不要把前面两份主配置直接拼接,只保留一套 events、http。

本例按 Nginx 收到的真实连接来源判断办公网段。前面还有负载均衡、CDN 或其他代理时,应先按可信代理恢复来源 IP,再配置访问限制;不要把共享公网代理地址直接当成办公网段。需要内网 HTTPS 时,为管理域名配置对应证书并保留来源限制。

除了正常填写与预览,再从办公网外验证:即使指定内网 Host,也不能访问管理站点。将 Nginx公网IP 换成实际值:

# 应返回 403。
curl -i --resolve 'admin.example.internal:80:Nginx公网IP' \
'http://admin.example.internal/project/实际问卷ID/edit'

# 外网域名下的管理页面应返回 404。
curl -I 'https://survey.example.com/project/实际问卷ID/edit'

3. 设置系统地址,让发布链接指向外网​

未配置时使用当前访问地址​

未配置“系统地址”时,发布链接使用当前浏览器访问地址的协议、主机和端口。管理员从 http://admin.example.internal:8080 进入设计页面,默认生成的也是内网链接。

下面是 Chrome 中的实际演示:管理员从 localhost:8001 设计问卷,未配置时发布链接同样显示 localhost:8001。

未配置系统地址时,发布链接使用当前访问地址 localhost:8001

保存外网填写地址​

  1. 在内网登录管理后台,进入 基础设施 → 配置管理 → 界面配置。
  2. 找到 系统地址,点击编辑图标。
  3. 填写用户实际访问的外网地址,例如 https://survey.example.com,然后保存。
  4. 刷新已经打开的问卷设计页面,让页面重新读取配置。

这里填写的是用户访问的外网域名,不是映射入口 192.0.2.10:18080、内网地址或 Java 地址;也不附加 /admin-api、/pub 或具体问卷路径。外网 HTTPS 使用非 443 端口时,应在地址中包含实际端口。

在基础设施、配置管理、界面配置中,将系统地址保存为示例外网域名

截图使用示例域名演示,部署时替换为已完成 DNS、证书和网关配置的外网域名。

核对发布链接和二维码​

返回问卷,打开 发放问卷。同一问卷的链接应变为 https://survey.example.com/s/Srfpdx,二维码也使用这一地址,管理员仍可继续从内网设计问卷。

保存系统地址并刷新后,发布链接和二维码使用 survey.example.com

两张发布截图来自同一问卷。截图中的问卷处于“未开启”状态,用于展示链接变化;正式收集前需要完成发布检查并开启问卷。

系统地址负责生成链接,不会创建网络映射、DNS 或证书。之前分发的链接和已下载的二维码也不会自动更新,需要重新复制、下载和分发。

4. 按项目功能扩展白名单​

普通问卷、/s/ 下的结果等子页面、扫码预览已经包含在完整配置中。其他功能按实际需要,将规则加入外网配置现有的 map,保留 default 0。

账号密码登录​

问卷确实要求账号登录时,添加以下规则:

~^(GET|HEAD):/user/login/?$ 1;
"POST:/admin-api/system/auth/login" 1;
"POST:/admin-api/system/auth/logout" 1;
"POST:/admin-api/system/auth/refresh-token" 1;
"GET:/admin-api/system/auth/get-permission-info" 1;

登录接口与管理端共用,Nginx 无法仅凭路径区分“填写者登录”和“管理员登录”;管理页面和管理 API 仍由白名单限制。短信、微信、企业微信、钉钉与 OAuth2 等方式需要分别核对其接口和回调地址,不要整体开放 /admin-api/system/auth/。

360、练习、公开查询与分享​

# 360 专属评价链接;提交后的登出也需要下方 logout 规则。
~^(GET|HEAD):/e/[A-Za-z0-9_-]+/?$ 1;
"POST:/admin-api/system/auth/logout" 1;

# 练习、练习结果。
~^(GET|HEAD):/t/[A-Za-z0-9_-]+(/[A-Za-z0-9_-]+)?/?$ 1;

# 公开查询、数据分享、兑奖。
~^(GET|HEAD):/(q|share|redeem)/[A-Za-z0-9_-]+/?$ 1;

按需要选取,不必整段启用。logout 如果已经在登录规则中添加,只保留一条。需要账号的流程还应配置登录规则。

/admin-api/survey/ 下的接口,包括考试监考命令流,已经统一放行;配置中的 proxy_buffering off 支持流式响应。课程中心等其他入口可能还需要不同的页面与接口,按实际请求和前端路由归属核对。

二级目录部署​

本文配置适用于根目录部署。使用 /survey/ 等前缀时,先取得匹配的前端交付包,再参考二级目录部署配置站点。

白名单中的页面、资源和接口应一起增加前缀,例如 /survey/s/...、/survey/preview、/survey/pub/...、/survey/adm/... 与 /survey/admin-api/...。外网和网络转发层保留完整路径,由内网站点按二级目录配置处理。

发布链接应类似 https://survey.example.com/survey/s/问卷ID,且 /survey 只出现一次。按所用交付包核对系统地址与路由前缀的拼接结果。

5. 验证配置与实际流程​

在各台 Nginx 上从实际使用的主配置开始检查,成功后再重载。主配置如何引入对应文件见include 示例:

sudo nginx -t -c /etc/nginx/nginx.conf
# 检查通过后再重载;主配置和 Nginx 程序路径须与运行中的实例一致。
sudo nginx -s reload -c /etc/nginx/nginx.conf

宝塔可在配置校验通过后保存并重载。若提示端口、默认站点或 map 重复,先检查是否把完整配置叠加到了已有站点;不要反复追加整份文件。

检查外网允许与拒绝的路径​

将问卷 ID 和预览 key 换成设计端实际生成的值,在办公网外检查:

# 填写页应返回 public 入口 HTML,正常为 200。
curl -i 'https://survey.example.com/s/实际问卷ID'

# 预览页应返回 admin 入口 HTML;问卷内容由后续预览接口按 key 加载。
curl -i 'https://survey.example.com/preview?key=实际预览key'

# 系统基础信息应正常返回。
curl 'https://survey.example.com/admin-api/system/auth/get-sys-info'

# 以下管理页面、HTML 直达入口和接口应在网关返回 404。
curl -I 'https://survey.example.com/'
curl -I 'https://survey.example.com/project/实际问卷ID/edit'
curl -I 'https://survey.example.com/adm/index.html'
curl -I 'https://survey.example.com/infra/config'
curl -i 'https://survey.example.com/admin-api/project/page'
curl -i 'https://survey.example.com/admin-api/system/user/page'
curl -i -X POST 'https://survey.example.com/admin-api/answer/page'
curl -i 'https://survey.example.com/admin-api/infra/file/page'

# survey 前缀有末尾斜杠,不放行名称相近的其他接口组。
curl -i 'https://survey.example.com/admin-api/survey-admin/page'

页面 HTML 返回 200 只说明入口能够访问。还应在 Chrome 或手机浏览器中完成以下流程:

  1. 内网创建并发布测试问卷,确认发布链接与二维码使用外网域名。
  2. 外网打开、刷新填写链接,检查脚本、样式、题目图片与附件。
  3. 从设计端生成一个新的预览链接,在外网打开;确认 /adm/ 资源与 /admin-api/survey/preview 加载正常。
  4. 完成填写与提交;按项目功能验证验证码、登录、上传、关联回填与结果页。
  5. 内网查看刚提交的答卷,确认数据完整;外网管理路径仍被拒绝。

常见问题​

现象排查方向
外网连接超时、502 或 504从外网 Nginx 服务器检查映射入口是否可达,确认映射服务、目标地址和内网监听端口;不要只测试内网本机能否访问
页面指向错误站点或返回接口 JSON核对映射是否指向内网 Nginx,及 Host 是否匹配该站点;页面请求不应直接送到 Java
预览返回 404检查外网 map 是否包含 /preview;查询参数 key 不应写进路径匹配规则
预览空白、脚本 404检查 /adm/ 静态资源白名单,以及内网预览是否返回 admin 入口
提示预览已过期从设计端重新生成预览链接;添加登录白名单不能恢复已过期的 key
发布链接还是内网地址保存系统地址后刷新设计页面,再重新复制链接和下载二维码
文字正常,图片或附件打不开核对文件 URL、只读目录白名单和私有附件凭证,系统地址不会改写旧文件 URL
内网站点返回 403核对内网收到的实际来源 IP,映射或 NAT 后可能与外网网关 IP 不同
外网还能访问管理页面或接口检查映射入口是否可被外部直连,以及其他端口、站点或网关是否存在绕过白名单的转发

升级时,survey 前缀下新增接口仍由后端校验,新增页面或其他接口前缀则需要按实际用途补充白名单。