Nginx 配置模块化可以复用,但安全边界不能照搬
围绕 include 复用、CORS 白名单、try_files、TLS 1.2/1.3、HSTS、PHP-FastCGI 真实文件限制和 nginx -t 回滚,整理一份可直接落地的 Nginx 配置安全边界指南。
先说结论:Nginx 适合模块化,但安全配置不能拿来就抄
把 Nginx 拆成多个 include 片段,是很自然的做法:公共响应头放一处,PHP 入口放一处,SSL 参数放一处,站点自己的规则再单独放一处。问题不在于“能不能拆”,而在于“拆完以后,哪些写法还安全,哪些只是看起来简洁”。很多老教程的坑都出在这里:CORS 直接反射 $http_origin,if 用在 location 里做路由,PHP 只凭正则放行,TLS 参数停留在能连上就行的阶段。真正稳妥的做法,是把复用和边界一起设计。
一、先把可复用片段拆开,但不要把业务规则和安全规则混在一起
最常见的模块化方式,是把公共能力拆成独立文件,例如 CORS、PHP、TLS、站点入口和通用安全头。这样做的好处是:多个站点可以复用同一套策略,变更时也不会到处复制粘贴。坏处也很直接:一旦某个片段写错,所有站点一起中招。所以建议把“纯配置能力”和“站点个性化逻辑”分层。
http {
include /etc/nginx/conf.d/mime.types;
include /etc/nginx/snippets/cors.map.conf;
include /etc/nginx/snippets/security-headers.conf;
include /etc/nginx/snippets/php-fastcgi.conf;
}这类拆法的目标不是炫技,而是把风险面缩小:全局只放共识规则,站点内只放必须知道域名、路径和入口文件的内容。
二、CORS 不要反射任意 Origin,白名单要放在 map 里
最危险的写法,是把请求头里的 Origin 原样回给浏览器,再顺手开上凭据。只要出现 Access-Control-Allow-Origin: $http_origin 和 Access-Control-Allow-Credentials: true 的组合,就要立刻停手。这样等于允许任意站点借用户登录态跨站发请求,风险非常高。
更稳的做法是:在 http 级用 map 定义白名单,命中才返回允许的 Origin,没命中就留空;再配合 add_header ... always,保证 4xx/5xx 也带上你需要的安全头。预检请求也最好单独处理,不要交给 PHP 或框架绕一圈。
map $http_origin $cors_origin {
default "";
"https://app.example.com" $http_origin;
"https://admin.example.com" $http_origin;
}
server {
location /api/ {
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Access-Control-Allow-Credentials true always;
add_header Access-Control-Allow-Methods GET,POST,PUT,DELETE,OPTIONS always;
add_header Access-Control-Allow-Headers Content-Type,Authorization always;
add_header Content-Length 0;
return 204;
}
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Access-Control-Allow-Credentials true always;
add_header Vary Origin always;
}
}注意这里的重点不是“把头加上”,而是“只有白名单命中才加”。如果业务根本不需要跨站带凭据,就不要开 credentials。
三、路由优先用 try_files,别把 if 和 rewrite 当万能胶
老式写法里常见这种判断:文件不存在就重写到入口文件。它在一些场景能工作,但可读性差,和 Nginx 的执行上下文也容易打架。Nginx 的 if 不是不能用,而是不要拿来做复杂流程控制,尤其不要把它当成应用路由引擎。
现代写法更直接:静态文件先找,找不到再落到入口。这样规则更短,行为更可预测,也更容易和框架配合。
location / {
try_files $uri $uri/ /index.php?$query_string;
}如果是 ThinkPHP、Laravel、WordPress 这类入口清晰的应用,优先让框架的标准入口来接管,不要在 Nginx 里写一堆“猜路径”的 rewrite。
四、TLS 至少明确启用 1.2 和 1.3,HSTS 也要有边界
SSL/TLS 配置不要停留在“能访问就行”。首先,协议版本要显式写出来,不要把旧协议混进来;其次,站点应该明确走 HTTPS,避免浏览器在回退场景里继续尝试明文;最后,HSTS 不是越早越好,只有确认全站都能稳定 HTTPS,子域和回源链路也都没问题,才适合开启。
server {
listen 443 ssl http2;
server_name example.com;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
}这里的 includeSubDomains 拼写不能错。HSTS 一旦发出去,浏览器会记住它;如果子域里还有没准备好的站点,后续排障会很麻烦。所以 HSTS 要当成一个“发布后的约束”,不是默认开关。
五、PHP / FastCGI 要限制真实文件,别让正则把解析面放大
PHP 相关配置最容易被忽略的点,是“哪些文件真的能交给 PHP 解释”。老教程里常把 location ~ \.php.*?$ 之类的写法直接搬上来,这会把 PATH_INFO、附加后缀和一些边界情况一起卷进来。更稳妥的方式,是只匹配真正的 .php 文件,并在交给 PHP-FPM 前先确认文件存在。
location ~ \.php$ {
try_files $fastcgi_script_name =404;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_pass unix:/run/php/php-fpm.sock;
}这里有两个关键点。第一,try_files 先挡住不存在的脚本,避免把任意路径送进 FastCGI。第二,SCRIPT_FILENAME 用 $realpath_root 比简单拼接更稳,尤其是在软链接、发布目录切换、复杂 root 或 alias 场景里。
只有在应用确实需要 PATH_INFO 时,才考虑单独设计 split 规则,而且要把它当成例外,不要默认开启。
六、上线前先跑 nginx -t,回滚也要提前准备
安全配置的最后一步,不是“改完就重载”,而是先做语法和引用检查,再决定是否切流。nginx -t 能帮你尽早发现拼写错误、包含路径错位、证书路径不存在、变量名写错等问题。它不能证明业务逻辑完全正确,但至少能挡住最基础的配置事故。
nginx -t
systemctl reload nginx如果你是在线上改配置,回滚策略也要提前写好。最简单的方式,是保留上一版配置备份,或者用版本控制管理 snippets/ 目录。建议在变更前后至少保留这三样东西:当前配置快照、可回退版本、以及本次变更的检查记录。这样一旦出现 502、CORS 失效或证书异常,回滚不会靠记忆。
七、一份更现代的组合示例
下面这组配置把上面几件事拼在一起:公共片段可复用,CORS 只对白名单开放,静态和入口通过 try_files 收口,PHP 只接真实文件,TLS 和 HSTS 明确声明。
http {
map $http_origin $cors_origin {
default "";
"https://app.example.com" $http_origin;
}
server {
listen 443 ssl http2;
server_name example.com;
ssl_protocols TLSv1.2 TLSv1.3;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location /api/ {
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Access-Control-Allow-Credentials true always;
add_header Vary Origin always;
}
location ~ \.php$ {
try_files $fastcgi_script_name =404;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_pass unix:/run/php/php-fpm.sock;
}
}
}八、最后再自检一遍:复用可以,风险不要复用
这类 Nginx 配置最值得记住的一句话是:include 适合复用能力,不适合复用旧习惯。能复用的是白名单、固定头、TLS 基线、PHP 真实文件限制和回滚流程;不能直接复用的是任意 Origin 反射、宽松正则、危险 if、含糊的 SSL 参数和没验证过的重载动作。
如果你准备把配置拆成多个模块,建议每次只改一个边界,再跑一次 nginx -t,确认没问题再重载。安全配置不是“写得短”就算好,而是每个片段的职责都清楚,出问题时也知道该回哪一层。