Nginx valid_referer配置实战:5分钟搞定CSRF防护 📅 2026/7/7 20:39:44 1. 项目概述从一次真实的CSRF攻击说起去年我们团队的一个内部管理后台差点出了大事。一个测试环境的接口因为疏忽没有做Referer校验被外部一个恶意构造的页面发起了一个POST请求差点就执行了批量删除操作。虽然最后因为其他权限校验拦住了但也惊出一身冷汗。这件事让我意识到很多开发者尤其是后端和运维同学总觉得CSRF跨站请求伪造是前端或者框架比如Spring Security该管的事却忽略了在流量入口——Web服务器层面我们有一个非常轻量、高效且几乎零成本的防护手段Nginx的valid_referer指令。这个项目标题“Nginx valid_referer配置实战5分钟搞定CSRF防护”核心就是解决这个问题。它不是什么高深莫测的“银弹”而是一个被严重低估的“守门员”技术。很多团队在Nginx配置里可能只关心负载均衡、缓存、反向代理却很少仔细琢磨valid_referer这个模块。它的原理很简单检查HTTP请求头中的Referer字段判断这个请求是否来自我们认可的“来源页面”。如果Referer为空、不合法或不在白名单内Nginx可以直接拒绝这个请求恶意网站伪造的请求在抵达应用服务器之前就被挡在了门外。这适合谁呢所有使用Nginx作为Web服务器或反向代理的开发者、运维和安全工程师。无论你的后端是Java、Go、Python还是PHP无论你用的是单体架构还是微服务只要流量经过Nginx这个防护就生效。它特别适合保护那些没有复杂用户交互状态、但执行敏感操作如修改、删除、支付的GET/POST接口比如管理后台的增删改查API、用户的状态修改接口等。5分钟不是夸张如果你对Nginx配置有基本了解从理解到配置生效真的就这么快。接下来我会带你从原理到踩坑彻底搞懂这个“守门员”该怎么用才稳。2. 核心原理与设计思路为什么是Referer在深入配置之前我们必须先搞清楚两个问题第一CSRF攻击到底是怎么发生的第二为什么校验Referer字段能有效防御这决定了我们配置的逻辑和边界。2.1 CSRF攻击的简单模型想象一下这个场景你登录了银行网站Awww.bank.com浏览器里保存了登录凭证比如Session Cookie。此时你不小心访问了一个恶意网站B。网站B的页面上隐藏了一个表单这个表单的提交地址指向银行网站A的转账接口https://www.bank.com/transfer并且参数已经填好比如转给攻击者账户10000元。由于浏览器会自动携带你在A站的Cookie当你访问B站时这个隐藏表单可能被自动提交例如通过img标签的src或JS脚本。对于银行服务器A来说它收到了一个带有合法用户Cookie的转账请求它无法区分这个请求是用户从A站自己的页面发起的还是从恶意网站B伪造的。这就是CSRF攻击的核心利用用户已登录的信任状态在用户不知情的情况下以用户的名义执行非本意的操作。2.2 Referer字段的防御逻辑HTTP协议中的Referer请求头注意拼写是Referer不是Referrer这是一个历史遗留的拼写错误指明了当前请求是从哪个页面链接过来的。在上面的攻击模型里正常请求用户从银行网站A的转账页面https://www.bank.com/transfer.html点击“确认”按钮发出的POST请求其Referer头应该是https://www.bank.com/transfer.html。CSRF恶意请求从恶意网站B的页面http://evil.com/trap.html发起的对A站接口的请求其Referer头应该是http://evil.com/trap.html或者在某些严格模式下为空比如用img src方式发起的GET请求可能不携带Referer。valid_referer指令的工作就是检查这个Referer值。我们可以配置一个“白名单”比如只允许来自www.bank.com及其子域名的请求。那么来自evil.com的请求就会被Nginx直接拦截返回403 Forbidden或自定义错误请求根本到不了后端的应用服务器攻击自然失效。2.3 方案选型与优劣权衡为什么选择Nginx层做这个事而不是全交给应用层性能与开销Nginx在流量入口处进行字符串匹配检查消耗极低比将请求转发到应用服务器再由应用框架如Spring解析Session、生成并比对Token要轻量得多。统一防护无论后端有多少种语言、多少个服务只要经过同一个Nginx一套配置即可统一防护避免各个服务重复实现或遗漏。快速失效攻击请求在进入业务逻辑前就被拒绝降低了后端服务的无效负载和潜在风险。配置灵活可以针对不同的location即不同的URL路径设置不同的Referer策略实现细粒度控制。当然它也有局限性这也是设计时必须考虑的依赖浏览器行为Referer头是由浏览器发送的可以被用户设置或浏览器插件禁用。如果用户禁用了Referer那么合法的请求也可能被拒绝。因此它通常不适合作为唯一的防护手段而应作为“纵深防御”的一环。对非浏览器客户端无效API调用来自移动端App、爬虫、命令行工具时可能没有或携带不规则的Referer。这类接口需要结合API Token、签名等其他认证方式。HTTPS到HTTP的Referer丢失出于安全考虑当从HTTPS页面链接到HTTP站点时浏览器通常不会发送Referer。这在今天全站HTTPS的时代问题不大但如果是混合协议环境需要注意。所以我们的设计思路是将valid_referer作为一道重要的前置防线主要用于防护源自其他网站尤其是恶意网站的跨站请求特别是针对浏览器访问的Web管理界面和用户操作界面。对于API等场景需额外评估。3. 配置解析与实操要点理解了原理我们来看valid_referer指令的具体语法和配置方法。这是实战的核心。3.1 指令语法全解valid_referers指令的语法如下valid_referers none | blocked | server_names | string ...;它后面可以跟多个参数参数间用空格分隔。这些参数定义了“合法Referer”的规则。指令通常与$invalid_referer变量配合使用在if判断中。关键参数解读none允许缺失Referer请求头的请求。这意味着如果请求根本没有Referer头也被认为是合法的。慎用因为CSRF攻击中Referer也可能为空。blocked允许Referer头存在但其值被防火墙或代理删除或修改通常以http://或https://开头但不包含有效主机名的请求。这主要处理一些隐私过滤场景。server_names允许Referer头中的主机名与当前server_name指令列出的任何一个服务器名称相匹配的请求。这是最常用的配置之一表示允许来自本站的请求。任意字符串可以是一个具体的域名如.example.com、一个完整的URL前缀如https://www.example.com/或一个正则表达式以~开头。用于构建白名单。$invalid_referer变量这是一个内建变量。当请求的Referer头符合valid_referers指令定义的任一规则时该变量值为空字符串如果不符合任何规则该变量值为1。我们就是通过判断if ($invalid_referer) { ... }来执行拦截动作。3.2 基础防护配置模板假设我们的网站域名为www.myapp.com我们要保护/api/admin/*下的所有管理接口。server { listen 80; server_name www.myapp.com; location /api/admin/ { # 定义合法的Referer来源 valid_referers none blocked server_names *.myapp.com myapp.com ~\.myapp\.com$; # 正则匹配以 .myapp.com 结尾的域名 # 如果Referer非法则拒绝请求 if ($invalid_referer) { # 可以返回403或者重定向到错误页 return 403 Invalid Referer; # 或者记录日志后返回 # access_log /var/log/nginx/csrf_block.log; # return 403; } # 正常的代理转发或其他配置 proxy_pass http://backend_server; proxy_set_header Host $host; # ... 其他proxy配置 } }配置解读与注意事项valid_referers行我们允许三种情况无Referernone、被修改的Refererblocked、来自本服务器名server_names即www.myapp.com的请求。此外还通过字符串和正则表达式明确允许了所有myapp.com的子域*.myapp.com和根域本身。if ($invalid_referer)块这是拦截动作。如果Referer非法直接返回403状态码和一段提示信息。这里有一个非常重要的Nginx最佳实践if指令在location中用于条件判断是可行的但要避免在if块内进行复杂的重写或多次修改变量通常只用于简单的return或rewrite ... last。我们的用法是符合规范的。none和blocked的取舍在上例中我们包含了它们这比较宽松。如果你的站点要求所有敏感操作必须从站内页面发起即必须携带本站Referer那么就应该去掉none blocked配置会更严格。正则表达式~\.myapp\.com$是一个正则匹配确保域名以.myapp.com结尾这能匹配www.myapp.com、admin.myapp.com、test.myapp.com等。注意正则表达式前的~符号。3.3 针对不同场景的精细化配置实际项目远比一个模板复杂我们需要根据不同的接口特性调整策略。场景一严格防护后台API必须携带本站Refererlocation ~ ^/api/(v1/)?(user|order|admin)/ { # 只允许来自本站及特定子域的请求不允许Referer为空或被屏蔽 valid_referers server_names *.myapp.com; if ($invalid_referer) { # 记录详细日志便于分析攻击来源 access_log /var/log/nginx/csrf_strict_block.log main; return 403; } proxy_pass http://backend; }这里去掉了none blocked意味着从浏览器地址栏直接输入API地址无Referer的请求也会被拒绝。这确保了请求必须源自站内页面跳转或表单提交。场景二保护静态资源防盗链但允许空Referer比如直接访问location ~* \.(jpg|jpeg|png|gif|css|js)$ { valid_referers none blocked server_names *.myapp.com static.myapp.com; if ($invalid_referer) { # 盗链请求可以返回403或者重写为一个“禁止盗链”的图片 # return 403; rewrite ^ /assets/anti-leech.png break; } # 正常情况下的资源服务配置 root /var/www/static; expires 30d; }这个配置常见于防止其他网站盗用你的图片、样式等静态资源。none参数在这里很有用因为它允许用户直接通过图片链接打开图片此时Referer为空。blocked参数则兼容了一些浏览器的隐私模式。场景三多域名或合作伙伴白名单如果你的服务需要被多个可信域名调用比如公司内部有多个不同域名的系统需要互调。location /api/webhook/ { valid_referers server_names *.myapp.com *.partner-a.com trusted-site.org ~^(https?://)?(www\.)?(domain1|domain2)\.com/; if ($invalid_referer) { return 403; } proxy_pass http://webhook_handler; }这里通过列出多个具体域名和一个更复杂的正则表达式构建了一个扩展的白名单。正则~^(https?://)?(www\.)?(domain1|domain2)\.com/可以匹配http://domain1.com、https://www.domain2.com等多种形式。重要提示if指令的局限性虽然我们在用if ($invalid_referer)但必须明白Nginx的if指令在location上下文中存在一些限制俗称“邪恶的if”。它不能随意嵌套且在某些阶段如rewrite阶段的行为可能与预期不符。好在我们这里的用法——在location内判断变量并直接return——是官方认可且稳定的用法。只要避免在if块内使用proxy_pass以外的指令如try_files、alias等通常没有问题。一个更稳妥的替代方案是使用map指令将$invalid_referer映射到一个自定义变量然后在location中判断该变量但这对于简单场景略显复杂。我们的当前用法在绝大多数生产环境中是安全可靠的。4. 完整实战部署流程现在我们从一个干净的Nginx开始完成一个完整的CSRF防护配置部署。假设我们正在为一个名为“ShopAdmin”的电商管理后台配置防护后台地址为admin.shop.com需要防护所有/api/开头的接口。4.1 环境准备与配置检查首先确认你的Nginx已经包含了ngx_http_referer_module模块。这个模块是Nginx核心模块默认是编译进去的但最好检查一下。nginx -V 21 | grep -o with-http_referer_module如果输出--with-http_referer_module则说明支持。几乎所有标准安装的Nginx都包含它。接下来规划你的防护策略。我们需要明确防护路径/api/合法来源admin.shop.com以及可能的后台前端地址如果前后端分离前端可能是dashboard.shop.com。严格程度管理后台要求严格不允许空Referer。拦截动作返回403状态码并记录日志。4.2 编写并应用Nginx配置我们编辑Nginx的站点配置文件通常位于/etc/nginx/conf.d/或/etc/nginx/sites-available/下。# /etc/nginx/conf.d/shopadmin.conf server { listen 443 ssl http2; server_name admin.shop.com; ssl_certificate /path/to/your/cert.pem; ssl_certificate_key /path/to/your/key.pem; # ... 其他SSL优化配置 # 全局日志格式添加Referer字段便于排查 log_format main_with_referer $remote_addr - $remote_user [$time_local] $request $status $body_bytes_sent $http_referer $http_user_agent; access_log /var/log/nginx/shopadmin_access.log main_with_referer; # 静态资源服务可以宽松一些 location ~* \.(ico|css|js|svg|woff2)$ { valid_referers none blocked server_name *.shop.com; if ($invalid_referer) { # 静态资源被盗链影响不大可以只记录不拦截或者返回一个默认资源 # access_log /var/log/nginx/static_leech.log main_with_referer; # 这里我们选择不拦截仅记录 } root /var/www/shopadmin/static; expires 1y; add_header Cache-Control public, immutable; } # 核心API接口严格防护 location /api/ { # 合法的Referer来源本服务器名、所有.shop.com子域 # 注意去掉了 none 和 blocked要求请求必须来自指定白名单 valid_referers server_name *.shop.com; # 如果Referer非法 if ($invalid_referer) { # 1. 记录到专门的安全日志包含更多细节 access_log /var/log/nginx/csrf_blocked.log main_with_referer; # 2. 返回403错误可以自定义错误页面 # return 403; # 3. 或者为了安全可以返回一个与正常错误无异的404迷惑攻击者可选 # return 404 Not Found; # 我们选择返回403并记录 return 403 Access Forbidden: Invalid Request Source; } # 以下是正常的反向代理配置 proxy_pass http://backend_api_upstream; # 你的后端服务器地址 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; proxy_set_header Referer $http_referer; # 可选将Referer传递给后端 # 超时设置 proxy_connect_timeout 30s; proxy_send_timeout 60s; proxy_read_timeout 60s; # 限制请求体大小防止恶意大请求 client_max_body_size 10m; } # 其他非API请求比如前端HTML入口 location / { root /var/www/shopadmin/frontend; try_files $uri $uri/ /index.html; # 这里通常不需要Referer检查因为是页面入口 } # 定义一个专门用于返回403错误页面的location可选美化错误页 error_page 403 /403.html; location /403.html { root /usr/share/nginx/html; # 或你的自定义错误页目录 internal; # 只允许内部重定向访问 } }4.3 配置测试与平滑重载配置写完后千万不要直接重启Nginx。先进行语法测试sudo nginx -t如果输出syntax is ok和test is successful说明语法没问题。然后平滑重载配置使更改生效而不中断现有连接sudo nginx -s reload4.4 功能验证测试配置生效后必须进行测试确保防护生效且不影响正常业务。测试1正常站内请求打开浏览器登录https://admin.shop.com。打开开发者工具F12切换到Network网络标签。在管理后台进行任意操作触发一个对/api/xxx的请求。查看该请求的请求头确认Referer字段类似https://admin.shop.com/some/page。请求应成功状态码200并且在csrf_blocked.log中不应该有相关记录。测试2模拟CSRF攻击从外部域名发起在本地或另一台服务器上创建一个简单的HTML文件evil.html。!DOCTYPE html html body h1恶意网站/h1 img srchttps://admin.shop.com/api/user/delete?id123 width0 height0 / script // 或者用JS发起一个POST请求 fetch(https://admin.shop.com/api/order/cancel, { method: POST, headers: {Content-Type: application/json}, body: JSON.stringify({orderId: 456}), // 注意浏览器同源策略会阻止JS读取跨域响应但请求已经发出 mode: no-cors }); /script /body /html将这个HTML文件放在一个非*.shop.com的域名或IP下访问例如http://your-test-ip/evil.html。访问这个页面时观察浏览器开发者工具的网络面板。对admin.shop.com的请求应该失败状态码为403。检查Nginx的csrf_blocked.log文件应该能看到一条或多条拦截记录其中$http_referer字段是你测试页面的地址或为空。测试3直接访问API无Referer在浏览器地址栏直接输入https://admin.shop.com/api/system/health假设存在此健康检查接口。由于是直接地址栏输入请求的Referer头为空。因为我们的配置没有包含none所以这个请求也应该被拦截返回403。这验证了防护的严格性。如果你的健康检查接口需要被外部监控系统如Prometheus无Referer调用那么这个配置就过于严格了你需要调整。这就是下一部分要讨论的“常见问题”。通过以上三步测试你可以基本确认valid_referer配置正在按预期工作。5. 常见问题、报错解决与避坑指南在实际部署和运维中你会遇到各种各样的问题。下面是我总结的常见“坑”及其解决方案。5.1 配置不生效或拦截了正常请求这是最常见的问题。排查思路如下检查Nginx配置语法和加载确保nginx -t通过并且已经执行了nginx -s reload。有时候修改了配置文件但忘了重载。确认location匹配优先级Nginx的location块有优先级精确匹配 前缀匹配^~ 正则~/~* 通用前缀/。如果你的防护配置在一个通用的location /api/中但有一个更具体的location /api/health在前面那么请求会匹配到更具体的规则从而跳过你的valid_referer检查。使用nginx -T大写T打印出完整的配置仔细检查location的匹配顺序。检查$invalid_referer逻辑确保你的判断是if ($invalid_referer) { ... }。新手有时会写反成if ($valid_referer)或if (!$invalid_referer)。验证Referer头的真实值在Nginx配置中临时添加一个调试头或者直接查看访问日志如果你按之前建议添加了Referer到日志格式。location /api/ { valid_referers ...; add_header X-Debug-Referer $http_referer always; # 输出Referer到响应头 add_header X-Debug-Invalid $invalid_referer always; # 输出是否非法 # ... 其他配置 }然后发起请求在浏览器开发者工具查看响应头看X-Debug-Referer和X-Debug-Invalid的值是否符合预期。$invalid_referer为1表示被判定为非法。注意server_names参数server_names匹配的是当前server块的server_name指令值。如果你有多个server_name如server_name admin.shop.com shop-admin.com;那么来自这两个域名的Referer都是合法的。确保你的前端页面域名在server_name列表内。5.2 特定场景下的误拦截场景从HTTPS页面跳转到HTTPReferer丢失导致被拦。原因这是浏览器安全策略。当从安全的HTTPS上下文导航到非安全的HTTP时浏览器通常不会发送Referer头或只发送不含路径的Origin头以防止信息泄漏。解决方案在今天最根本的解决方案是全站启用HTTPS。如果历史原因必须混合对于从HTTPS跳转到HTTP的特定接口可以考虑在对应的location配置中临时加入none参数但这会降低安全性。更好的做法是重构避免这种跳转。场景移动端App或第三方服务调用API被拦截。原因这些客户端可能不发送Referer或者发送的Referer不符合你的白名单如android-app://com.package.name。解决方案区分请求来源。不要对所有/api/路径一刀切。方法A路径区分。为内部/第三方API设计独立的路径前缀如/api/internal/或/api/v1/public/在这些路径的配置中不启用valid_referer而是使用API Key、签名等更强的认证方式。location /api/internal/ { # 不进行Referer检查 # 使用其他认证方式如校验特定的请求头 if ($http_x_api_key ! your-secret-key) { return 403; } proxy_pass http://backend; }方法B条件判断。结合map指令和请求头进行更复杂的判断。# 在http块中定义map http { map $http_user_agent $need_referer_check { # 如果User-Agent包含某些移动端App标识则不检查Referer ~*MyCompanyApp 0; # 默认情况检查 default 1; } # ... 其他配置 } server { location /api/ { # 只有需要检查时才执行 if ($need_referer_check) { valid_referers ...; if ($invalid_referer) { return 403; } } proxy_pass http://backend; } }注意依赖User-Agent并不完全可靠因为它可以被伪造。方法A路径区分是更清晰、更安全的做法。场景浏览器隐私模式或安全设置禁用Referer。原因部分用户或浏览器插件会禁用Referer发送以保护隐私。解决方案对于面向公众的、需要高兼容性的Web应用如电商主站下单不能仅依赖Referer校验作为唯一的CSRF防护手段。必须结合使用CSRF Token同步器令牌模式或双重Cookie验证等。Nginx的valid_referer可以作为第一道低成本防线而CSRF Token是应用层更可靠的保证。对于内部管理系统可以要求用户不启用此类极端隐私设置。5.3 性能与日志管理性能影响valid_referer检查是简单的字符串匹配性能开销微乎其微可放心使用。主要的性能考量在于正则表达式的复杂度。如果白名单列表非常长比如上千个域名建议使用map指令将列表加载到内存中进行哈希查找效率更高。日志爆炸如果拦截了大量恶意扫描或攻击csrf_blocked.log可能会快速增长。务必配置日志轮转logrotate。# /etc/logrotate.d/nginx-csrf /var/log/nginx/csrf_blocked.log { daily rotate 30 missingok notifempty compress delaycompress sharedscripts postrotate [ -f /var/run/nginx.pid ] kill -USR1 cat /var/run/nginx.pid endscript }安全日志分析定期检查csrf_blocked.log可以了解攻击来源、频率和模式。你可以使用简单的命令进行分析# 查看被拦截最多的IP awk {print $1} /var/log/nginx/csrf_blocked.log | sort | uniq -c | sort -rn | head -20 # 查看被拦截的请求路径 awk -F\ {print $2} /var/log/nginx/csrf_blocked.log | sort | uniq -c | sort -rn | head -205.4 高级技巧使用map指令进行更灵活的控制对于复杂的白名单或条件判断map指令比在location里写一长串valid_referers更优雅、更高效。http { # 定义一个map将$http_referer变量映射为$is_trusted_referer变量 map $http_referer $is_trusted_referer { # 默认值为0不信任 default 0; # 缺失Referer头的情况对应valid_referers的none 1; # 注意这里将空Referer视为信任根据你的安全策略调整 # 匹配server_names ~^https?://([^/]\.)?shop\.com/ 1; # 正则匹配所有.shop.com子域 # 匹配特定的合作伙伴域名 ~^https?://partner\.example\.com/ 1; # 匹配一个特定的路径更精确 ~^https?://www\.othersite\.org/trusted-path/ 1; # 注意map的正则匹配是按顺序的第一个匹配到的就返回其值。 } server { location /api/ { # 使用map的结果进行判断 if ($is_trusted_referer 0) { access_log /var/log/nginx/csrf_blocked.log main; return 403; } proxy_pass http://backend; } } }使用map的好处是逻辑更清晰白名单规则集中管理且匹配效率更高Nginx会优化map的查找。你可以根据$http_referer、$http_user_agent甚至$remote_addr等多个变量组合出复杂的信任条件。6. 与其他安全措施的联动最后必须强调Nginx的valid_referer配置是Web安全防御体系中的一环而不是全部。它主要针对基于浏览器的、跨站点的请求伪造。一个健壮的系统需要多层防护应用层CSRF Token对于执行重要状态变更的POST/PUT/DELETE请求必须使用CSRF Token。这是防御CSRF最主流、最可靠的方法。Token由服务器生成嵌入表单或Meta标签请求时携带服务器进行校验。Nginx的Referer检查可以作为Token校验前的一道快速过滤网。同源策略CORS对于需要跨域访问的API如前端单独部署正确配置CORS头如Access-Control-Allow-Origin至关重要。CORS和Referer检查是互补的CORS控制哪些“源”可以读取响应Referer检查控制请求来自哪个“页面”。一个配置不当的CORS如Access-Control-Allow-Origin: *可能会让Referer检查形同虚设因为恶意网站可以通过JS发起“简单请求”并读取响应。Cookie安全属性为会话Cookie设置Secure仅HTTPS、HttpOnly禁止JS访问和SameSite属性。SameSiteStrict或Lax可以在现代浏览器层面有效阻止大部分跨站请求携带Cookie从根源上缓解CSRF。SameSite是比Referer检查更底层的浏览器安全机制。输入验证与权限控制永远不要忘记最基本的安全原则在服务端对用户输入进行严格的验证和过滤实施最小权限原则确保即使用户会话被劫持攻击者能执行的操作也有限。我个人在实际部署中的体会是valid_referer就像你家门口的监控摄像头它能吓退和记录大部分漫无目的的“拉车门”式自动化攻击脚本。这些脚本往往不会精心构造Referer头。对于真正有目标的、针对性的攻击它可能被绕过如通过浏览器漏洞或用户配合但结合应用层的CSRF Token好比门锁和Cookie的SameSite属性好比小区的门禁就能构建一个立体的、纵深的安全防御体系。配置它花费不了5分钟但它带来的安全收益和攻击日志对于运维和安全团队来说价值远超这点时间成本。在每次安全审计或渗透测试报告里看到“缺乏Referer校验”这类低级漏洞时你都会庆幸自己早就把这部分工作做扎实了。