<!DOCTYPE html>
Nginx 访问控制实战:HTTP Basic Auth 基础认证、IP 黑白名单与防爬虫防扫描
网站上线之后,很多资源并不适合对所有人开放:后台管理页、API 接口、临时预览环境、内部统计页面。如果只靠"路径足够隐蔽"来防,迟早会被扫描器摸到。Nginx 自带一套非常灵活的访问控制能力,从最简单的 HTTP Basic Auth 密码认证,到 IP 黑白名单、按目录限流、按 User-Agent 过滤爬虫,都可以在配置层面直接完成,不需要改任何应用代码。本文从原理讲起,给出可直接落地的配置模板和常见排错方法。
一、访问控制的三层模型:先想清楚要拦谁
动手写配置之前,先明确你要控制的资源分几层。Nginx 的访问控制可以在三个层面叠加生效:
第一层:网络层 —— 按客户端 IP 判断(allow / deny) 第二层:身份层 —— 按账号密码判断(auth_basic / auth_request) 第三层:行为层 —— 按请求特征判断(User-Agent、Referer、请求频率 limit_req)
三者是"与"的关系:请求必须同时通过所有已启用的层才会被放行。理解这一点,排错时就清楚——401 是身份层拒绝,403 是网络层拒绝,而不是"配置没生效"。
一个典型的应用场景:后台路径 /admin 只允许办公室固定 IP 访问,同时叠加密码认证,即使 IP 被伪造也能兜底;对外 API 则用 limit_req 限制单个 IP 的请求频率,防止被脚本刷爆。
二、HTTP Basic Auth 基础认证:一行 auth_basic 搞定密码门
Basic Auth 是 HTTP 协议内置的最简单的认证方式:浏览器弹出账号密码框,把 "用户名:密码" 做 Base64 编码放进请求头 Authorization。Nginx 侧只需要两步:生成密码文件,然后在配置里启用。
第一步,生成密码文件。用 htpasswd 工具(Debian/Ubuntu 上属于 apache2-utils 包):
# Debian / Ubuntu apt install -y apache2-utils # 创建密码文件(-c 表示新建,-B 表示用 bcrypt 算法,比默认的 MD5 更安全) htpasswd -c -B /etc/nginx/.htpasswd admin # 追加第二个用户(去掉 -c,否则会覆盖文件) htpasswd -B /etc/nginx/.htpasswd ops # 查看文件内容,每行是 "用户名:哈希值" cat /etc/nginx/.htpasswd
第二步,在 server 或 location 中启用认证:
server {
listen 80;
server_name admin.example.com;
# 整个站点都要密码
auth_basic "Restricted Area";
auth_basic_user_file /etc/nginx/.htpasswd;
root /var/www/admin;
index index.html;
}
只保护某个路径(比如后台目录),把指令放进 location:
server {
listen 80;
server_name example.com;
location /wp-admin {
auth_basic "Admin Login";
auth_basic_user_file /etc/nginx/.htpasswd;
}
# 其他路径不受影响
location / {
proxy_pass http://127.0.0.1:8080;
}
}
改完配置后记得校验并重载:
nginx -t && systemctl reload nginx
三、IP 黑白名单:allow / deny 的匹配顺序是坑
IP 控制用 allow 和 deny 指令,它们按书写顺序从上到下匹配,命中即停。最容易踩的坑是:deny 写在了 allow 前面,导致白名单完全失效。
# 错误示范:deny 在前,所有 IP 都被拒绝,后面的 allow 永远轮不到
location /admin {
deny all;
allow 203.0.113.10;
}
# 正确写法:先放行白名单,最后再 deny all
location /admin {
allow 203.0.113.10;
allow 192.168.1.0/24;
deny all;
}
按 CDN 场景说明一个进阶坑:如果你的站点前面挂了 CDN 或负载均衡,Nginx 看到的客户端 IP 是 CDN 节点的 IP,而不是真实用户 IP。此时直接写 allow 真实 IP 列表会全部失效。需要先在 http 块里用 set_real_ip_from 和 real_ip_header 还原真实 IP:
http {
# 信任 CDN 的回源 IP 段(按你实际 CDN 文档填写)
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
# 从 X-Forwarded-For 头取最右边的真实客户端 IP
real_ip_header X-Forwarded-For;
real_ip_recursive on;
}
注意:set_real_ip_from 只应填写你真正信任的代理 IP。如果信任了任意来源,攻击者可以直接伪造 X-Forwarded-For 绕过 IP 白名单。
四、防爬虫防扫描:按 User-Agent 过滤与按目录限频
很多扫描器会带着特征明显的 User-Agent 来探测路径(如 sqlmap、nmap script、爬虫框架默认 UA)。可以写一个 map 把它们归一化,再统一拒绝:
http {
# 把 UA 中的特征关键字映射成 1,其他映射成 0
map $http_user_agent $bad_bot {
default 0;
~*sqlmap 1;
~*nmap 1;
~*nikto 1;
~*python-requests 1;
~*go-http-client 1;
~*masscan 1;
~*zgrab 1;
~*curl/7.6 1; # 老版本 curl 常被扫描器使用,按需开启
}
}
server {
listen 80;
server_name example.com;
# 命中特征的 UA 直接 444(Nginx 直接断开连接,不返回任何内容)
if ($bad_bot) {
return 444;
}
}
对高频访问的登录接口、搜索接口,叠加 limit_req 限制每个 IP 的请求速率,防止被脚本暴力刷:
http {
# 定义限流区域:按 IP 计数,每秒平均 2 个请求,突发最多 5 个
limit_req_zone $binary_remote_addr zone=login_limit:10m rate=2r/s;
}
server {
listen 80;
server_name example.com;
location /api/login {
limit_req zone=login_limit burst=5 nodelay;
proxy_pass http://127.0.0.1:8080;
}
}
超出速率的请求会收到 503,配合下面第五节讲的错误页,可以把"被封禁"的提示做得友好一些。
五、组合实战:后台目录的完整访问控制模板
把前三节的能力组合起来,得到一套"IP 白名单 + 密码认证 + 限频 + 友好报错"的完整后台保护配置,直接复制修改即可:
http {
map $http_user_agent $bad_bot {
default 0;
~*sqlmap 1;
~*nmap 1;
~*nikto 1;
}
limit_req_zone $binary_remote_addr zone=admin_zone:10m rate=3r/s;
server {
listen 80;
server_name admin.example.com;
# 爬虫扫描器直接断开
if ($bad_bot) {
return 444;
}
location / {
# 网络层:只允许白名单 IP
allow 203.0.113.10;
allow 198.51.100.0/24;
deny all;
# 身份层:密码认证
auth_basic "Admin Area - Authorized Only";
auth_basic_user_file /etc/nginx/.htpasswd;
# 行为层:限频
limit_req zone=admin_zone burst=10 nodelay;
# 被拒绝时返回的页面
error_page 401 /401.html;
error_page 403 /403.html;
error_page 503 /503.html;
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
}
三层的效果叠加:非白名单 IP 直接 403;白名单 IP 但没有密码返回 401;有密码但请求太快返回 503;UA 像扫描器的直接 444 断连,连探针响应都不给。
六、常见排错:401 循环、403 白名单失效、密码文件权限
1. 输入正确密码却反复弹窗(401 循环)
最常见原因是密码文件的哈希算法太弱或文件内容被破坏。确认用 -B 生成 bcrypt 哈希,且文件权限正确:
# nginx worker 进程需要能读到密码文件 chown root:root /etc/nginx/.htpasswd chmod 640 /etc/nginx/.htpasswd # 重新生成一个用户测试 htpasswd -B /etc/nginx/.htpasswd admin nginx -t && systemctl reload nginx
另外注意:浏览器会缓存旧密码。清掉浏览器站点数据或换隐身窗口测试,避免误判。
2. 该拒绝的 IP 却访问成功了(403 不生效)
先查是不是走了 CDN/反代导致拿到的是代理 IP(见第三节 real_ip 配置);再查 allow/deny 的书写顺序;最后用 curl 带来源 IP 测试:
# 模拟从指定 IP 发请求看响应码 curl -I -H "X-Forwarded-For: 203.0.113.10" http://127.0.0.1/ curl -I -H "X-Forwarded-For: 6.6.6.6" http://127.0.0.1/ # 确认 real_ip 解析生效 tail -f /var/log/nginx/access.log
3. 访问返回 404 而不是 401
如果认证指令写在 location 里,但请求没进这个 location(比如被 try_files 或正则 location 提前接管),认证就不会触发。用 nginx -T 查看最终生效的配置,确认请求实际命中的 location 块:
nginx -T | grep -A 20 "location /admin"
4. 限频后正常用户也被 503
说明 rate 或 burst 设置过小。rate=2r/s 对登录接口足够,但对有前端轮询的页面会造成误伤。调大 burst,或把限流区域按 URL 维度拆分(limit_req_zone 的 key 改成 $request_uri 而非 IP)。
# 按请求路径限流,避免单个用户并发时互相误伤 limit_req_zone $binary_remote_addr$request_uri zone=api_zone:10m rate=5r/s;
5. 日志里看不到被拒绝的记录
return 444 的请求默认不会写 access 日志条目(或写为 444)。排错时临时去掉 444 改 return 403,看日志确认命中规则后再改回。
七、小结
Nginx 访问控制的价值在于:不需要改应用代码,纯配置就能给后台、接口、预览环境加多层保护。记住三个要点:一是三层模型(IP 层、身份层、行为层)叠加使用而不是只配一种;二是 allow/deny 的顺序和 real_ip 还原是 IP 控制的两个关键坑;三是任何生产改动都要 nginx -t 校验后再 reload。把上面的模板按自己的场景裁剪,再配合排错小节的方法验证,基本能覆盖日常 90% 的访问控制需求。


