
咱们搞运维的,Nginx那是吃饭的家伙,平时大家可能更多关注的是负载均衡、反向代理,或者怎么调优性能。但说句掏心窝子的话,安全这东西,平时不出事你觉得它多余,一出事那就是“救命稻草”。今天咱们不整那些虚头巴脑的理论,直接上生产环境的实战,聊聊Nginx访问控制那些事儿。这些配置都是我在无数次填坑过程中总结下来的,希望能给大伙提个醒,别让你的服务器在互联网上“裸奔”。
咱们先从最简单的说起,很多人觉得访问控制不就是设个密码,或者封个IP嘛。其实没那么简单。
拒绝不速之客:基于IP的访问控制
这应该是最直接的手段了。就像咱们小区的门禁,只让业主进,外人一律挡在门外。Nginx里的allow和deny指令就是干这个活的。这俩模块是ngx_http_access_module自带的,默认就装了,不用你去折腾编译安装。
我记得有一次,公司有个内部管理系统,原本是挂在内网域名下的,结果开发哥们为了方便测试,临时挂到了公网,也没做任何限制。结果没过两天,就被安全扫描器扫到了,虽然没出大事故,但那个报警邮件看得我后背发凉。后来我就给所有的内部后台都加上了IP白名单。
配置起来其实特简单,打开你的nginx.conf或者对应的站点配置文件,在server块或者location块里加上几行就行:
location /admin/ {
allow 192.168.1.0/24; # 允许内网网段
allow 10.0.0.1; # 允许某个特定的跳板机
deny all; # 拒绝其他所有人
}
这里有个小细节,很多人容易踩坑。Nginx匹配规则是从上往下匹配,一旦匹配成功就停止。所以顺序千万不能乱。你要是把deny all写在第一行,那后面所有的allow都白搭,谁都进不去,这叫“误伤友军”。我就干过这种傻事,配置完 reload 了一下,结果把自己关在外面了,只能灰溜溜地去机房连控制台改配置,那场面,别提多尴尬了。
还有个事儿得注意,如果你用了CDN,比如阿里云CDN或者Cloudflare,那你在这个allow里配置的IP可能就不是用户的真实IP了,而是CDN节点的IP。这时候你得先用set_real_ip_from把CDN的IP段设置一下,用X-Forwarded-For或者X-Real-IP头来获取真实用户IP,然后再做限制。不然你限制了个寂寞,或者把正常用户给封了。
给目录加把锁:基于用户的认证
有时候,我们需要给某个测试环境、或者某个临时的API接口加个密码,不想让随便什么人都能访问。这时候IP限制就不太灵活了,比如出差在外,IP是动态的,咋办?这时候就得请出ngx_http_auth_basic_module模块了。
这玩意儿就像给目录加了一把锁,只有输入对了用户名和密码才能进。
配置分两步走。第一步,得先有个密码文件。别想着自己去手写加密密码,Linux底下有现成的工具,Apache的htpasswd工具最方便。一般服务器装了Apache或者httpd-tools就有。没有的话装一下:
yum install httpd-tools -y
然后生成密码文件,比如我们建个/etc/nginx/.htpasswd文件,添加一个叫pyy的用户:
htpasswd -c /etc/nginx/.htpasswd pyy
回车后它会让你输入两次密码。注意那个-c参数是create的意思,如果你是第一次创建文件用-c,如果是追加用户,千万别带-c,不然原来的文件会被覆盖掉,我就因为这个被同事投诉过,把他们的账号给弄丢了。
文件有了,接下来就是Nginx配置:
location /test/ {
auth_basic "Restricted Area"; # 这个是提示信息,随便写
auth_basic_user_file /etc/nginx/.htpasswd;
}
配置完nginx -t测试一下,没问题就nginx -s reload。这时候你再去访问那个目录,浏览器就会弹出一个原生的登录框,丑是丑了点,但胜在实用。
这个功能在临时分享一些敏感数据,或者保护还没上线的功能时特别好用。不过要注意,这个Basic认证是Base64编码传输的,安全性并不是特别高,如果不配合HTTPS,密码其实相当于明文传输,抓个包就能看见。所以,能上HTTPS尽量上HTTPS。
更高级的玩法:结合GeoIP做地域封禁
有时候我们不仅想封IP,还想封地区。比如公司的业务只在国内开展,但是每天日志里全是来自大洋彼岸的扫描攻击,看着都烦。这时候就可以用GeoIP模块。
现在的Nginx默认应该都支持ngx_http_geoip_module,如果不支持,那你可能得重新编译安装一下,带上--with-http_geoip_module参数。不过现在很多都是动态加载模块,看情况而定。
首先你得下载GeoIP的数据库文件。以前MaxMind提供免费的GeoLiteCountry,现在虽然收费了,但GeoLite2还是能用的,就是得注册个账号下载。下载解压后,放到服务器某个目录下,比如/usr/share/GeoIP/。
配置文件里这么写:
http {
# 加载数据库文件
geoip_country /usr/share/GeoIP/GeoLite2-Country.mmdb;
# 定义一个变量,判断是否允许访问
# 这里假设我们只允许中国(代码CN)访问
map $geoip_country_code $allowed_country {
default no;
CN yes;
}
server {
location / {
if ($allowed_country = no) {
return 403 "Service not available in your region.";
}
# 其他配置...
}
}
}
这招在应对某些特定地域的攻击时特别有效。之前有个客户,是个本地生活服务的APP,只做本地生意,我直接给他配置了只允许本地IP段访问,外加GeoIP过滤,日志瞬间干净了,CPU负载都降了好几个点。
不过这玩意儿也有坑。那个GeoIP数据库不是实时更新的,IP库这东西变动很频繁,偶尔会有误杀的情况。比如有些用户用了国外的代理,或者IP库定位不准,就会被挡在外面。所以配置完,一定要留好监控,看看有没有大量的403报错,或者给个友好的提示页面,别让用户以为网站挂了。
防止恶意刷接口:请求频率限制
刚才说的都是怎么控制“谁能进”,接下来咱们聊聊怎么控制“怎么进”。这就是限流,也是访问控制的重要一环。
如果你的接口被人家用脚本狂刷,或者有人恶意CC攻击,你的服务器资源瞬间就会被耗尽。Nginx的ngx_http_limit_req_module模块就是专门干这个的。它用的是“漏桶算法”,不管你发请求多快,到了我这儿都得按规矩排队漏下去。
配置分两块。第一块在http块里定义限流规则:
http {
# 定义一个叫 my_limit 的限流区域
# $binary_remote_addr 是根据客户端IP来限制
# zone=my_limit:10m 定义区域名字和共享内存大小,10M大概能存16万个IP状态
# rate=10r/s 表示每秒允许10个请求
limit_req_zone $binary_remote_addr zone=my_limit:10m rate=10r/s;
}
然后在server或者location里应用这个规则:
location /api/ {
# burst=20 允许突发流量,也就是短时间内可以多来20个请求排队
# nodelay 超过burst限制直接拒绝,不进行延迟处理
limit_req zone=my_limit burst=20 nodelay;
# 被限制时返回的状态码,默认是503,我们可以改成429 Too Many Requests
limit_req_status 429;
proxy_pass http://backend;
}
这里的burst和nodelay参数特别有讲究。如果不加burst,请求非常平滑,每秒严格控制在10个,多一个都不行,用户体验可能不太好,感觉网站一卡一卡的。加了burst=20,就像给桶扩容了,瞬间来30个请求,前10个正常处理,后20个排队。但是如果不加nodelay,那排队的20个请求处理起来会很慢,因为要按rate的速度漏出去。加了nodelay,就是排队的那20个请求也会立马转发给后端,但是桶里的“令牌”用光了,再来请求就直接拒绝。
这块参数怎么调,得看你的业务。如果是秒杀场景,burst设大点,nodelay必须加,保证瞬时并发。如果是普通的API,设小点,保护后端。
我有次把burst设得太小,结果公司搞营销活动,流量稍微一上来,全是503报错,被运营妹子追着问是不是服务器炸了。所以说,这玩意儿得压测,得根据实际情况慢慢调优。
神奇的Nginx变量与Map指令
有些时候,我们的需求比较变态。比如,我想封禁所有User-Agent里包含"Java"或者"Python"字样的请求,防止爬虫;或者我想只允许特定URL带特定参数的请求。这时候if指令就派上用场了。
虽然网上都说if is evil,但在location里做些简单的判断还是挺好用的。
比如封杀特定UA:
location / {
# $http_user_agent 代表请求头里的User-Agent
if ($http_user_agent ~* (python|curl|java|wget)) {
return 403;
}
}
这招对付那些不伪装UA的简单爬虫一抓一个准。
再高级点的,比如我们想根据请求参数做白名单。有个接口,只有带着特定token参数的请求才能访问。这时候可以用map指令,这可是个好东西,能帮我们减少很多if判断,让配置更清爽。
http {
# $arg_token 就是url里的token参数
map $arg_token $is_allowed {
default 0;
"my_secret_key" 1;
}
server {
location /private_api/ {
if ($is_allowed = 0) {
return 403;
}
proxy_pass http://backend;
}
}
}
这样配置,只有当URL是/private_api/?token=my_secret_key时,$is_allowed才是1,否则都是0,直接403。这比写一堆正则匹配要清晰得多,性能也更好。
还有个大杀器:auth_request
最后再提一个比较高级的功能,ngx_http_auth_request_module。这个模块允许你把认证逻辑委托给一个外部服务。
啥意思呢?比如你们公司有个统一的SSO登录中心,或者有个专门的鉴权微服务。你不想在Nginx里维护复杂的密码文件,也不想写Lua脚本去连数据库。那就可以用这个。
配置大概是这样的:
location /private/ {
auth_request /auth;
# 这里的 /auth 是一个内部location
error_page 401 = @error401;
proxy_pass http://backend_service;
}
location = /auth {
internal; # internal表示这个location只能内部重定向访问,外部直接访问404
proxy_pass http://auth_service/validate;
proxy_pass_request_body off; # 不转发请求体,只转发头
proxy_set_header Content-Length "";
proxy_set_header X-Original-URI $request_uri;
}
流程是这样的:用户访问/private/,Nginx会先偷偷发一个子请求到/auth,也就是你的鉴权服务。如果鉴权服务返回200,Nginx就继续把请求转发给后端;如果返回401或403,Nginx就直接拦截。
这个玩法扩展性极强。你可以把IP黑白名单、用户Token校验、甚至复杂的权限逻辑都放在那个auth_service里去写,用什么语言都行,只要返回特定的HTTP状态码就行。Nginx只负责挡在前面做调度。这在微服务架构里特别常见,实现了业务逻辑和网关配置的解耦。
填坑总结
写了这么多,其实Nginx的访问控制无非就是“你是谁”、“你从哪来”、“你要干什么”这三个问题。
我们在生产环境落地的时候,往往不是单一使用某一种手段,而是组合拳。比如,先封禁恶意UA,再限制IP地域,然后对关键接口做频率限制,最后对后台管理页面做IP白名单+密码认证。
还有几个小坑大家注意一下:
- 1. 配置完一定要测试:
nginx -t是保命符,但有时候语法对不代表逻辑对,最好有个测试环境先验证。 - 2. 日志是关键:不管你配置得多么完美,总会有漏网之鱼或者误杀。一定要盯着日志看,特别是配置变更后的那段时间。
- 3. 版本差异:Nginx版本更新挺快,有些模块的指令在不同版本下可能有细微差别,遇到报错多看官方文档,别光百度。
运维这活儿,就是要在便利性和安全性之间找平衡。锁得太死,业务跑不起来;放得太宽,半夜起来救火。咱们能做的,就是尽量把墙筑得高一点,门留得合适一点。
今天的分享就到这儿吧,希望能给大家在Nginx安全加固方面提供点思路。这些都是我平时一点一点攒下来的经验,不一定是最完美的,但绝对是实战中能用得上的。
阅读原文:原文链接
该文章在 2026/4/21 10:17:05 编辑过