我的博客是 WordPress 搭的,跑 Sakurairo(iro)主题。上一篇折腾完这个主题的评论区图床之后,注意到站里还有几个自己写的图片接口一直裸奔,谁都能访问。于是又花时间把它们加固了一下,记录如下。
起因
Sakurairo 主题的文章封面和站点背景图是动态的。实现方式是自己在网站根目录写了个 api/ 目录,里面放几个 PHP 脚本,每次访问随机挑一张图,302 跳转过去。
一共三个:
img-Article.php:文章封面图img-pc.php:电脑端站点背景img-moblie.php:手机端站点背景
纯静态图片文件放在同目录下的子文件夹里,PHP 只负责"随机挑一张然后跳转"。
这接口从写好那天起就没管过。直到这次弄图床,才意识到一个问题:这几个 PHP 没有任何访问控制,理论上谁都能拿着地址高频刷,纯消耗服务器资源。
第一反应:限制成只能服务器内部调用?
一开始的想法很简单:这几个图是我自己站的封面和背景,别人又用不着,直接把接口锁死,只允许服务器自己调用不就行了?
但动手前查了一下调用方式,发现这个思路根本不成立。
文章封面的调用是这样的——首页文章列表里,每一篇的封面图是这个:
<img class="lazyload" data-src="https://<你的域名>/api/img-Article.php?64">
注意这个 data-src 加 lazyload:图是浏览器在访客滚动页面到那一篇的时候,自己去请求的。背景图同理,img-pc.php 填在 Sakurairo 后台的"随机背景图"设置里,也是浏览器把整个地址当背景图拉。
也就是说,这三个接口本来就是给浏览器用的。一旦限制成"只准服务器内部调用",等于每个访客打开页面,封面和背景图全部裂掉。
这条路堵死。
换个思路:不挡人,挡频率
既然不能拦调用方,那就拦调用频率。
正常访客打开一次首页,懒加载触发大概十来次请求,集中在首屏那一下,是一次性的。脚本刷量不一样,它是持续的高频请求,一秒几十次那种。两者在频率特征上区分度很高,用 Nginx 自带的 limit_req 按 IP 限速就能拦。
参考正常首页的请求量,把阈值定在:每 IP 每秒 5 次,允许 40 次的突发。这样正常访客那十几二十次首屏请求随便过,持续刷量的会被稳稳挡在 503。
实施
Nginx 的限流要先在 http 层声明一个 zone,再在具体的 server 里引用。分两步。
第一步,在 nginx.conf 的 http 块里加一行:
limit_req_zone $binary_remote_addr zone=img_api_req:10m rate=5r/s;
第二步,在站点配置里给这三个接口单独开一个 location,套上限流:
location ~ ^/api/(img-Article|img-pc|img-moblie)\.php$ {
limit_req zone=img_api_req burst=40 nodelay;
add_header Cache-Control "public, max-age=300";
# ... 其余 fastcgi 处理照旧
}
正则里 ^ 和 $ 把匹配范围卡死在这三个文件名上,其他路径一概不碰。也就是说这个限流只对这三个接口生效,站里其他接口、WordPress 的 REST API 都不受影响。
顺带加了个缓存头。不过这里有个小坑:PHP 只要开了 session_start(),就会自动往响应里塞一个 Cache-Control: no-store, no-cache,把缓存禁掉。img-Article.php 和 img-pc.php 为了不连续出同一张图都开了 session,所以我在这两个文件里 session_start() 之后手动覆盖一行:
session_start();
header("Cache-Control: public, max-age=300"); // 覆盖 PHP session 默认的 no-store
img-moblie.php 没开 session,不用改。
实测
改完在服务器上直接压了一轮,70 个请求连发打 img-pc.php:
| 状态码 | 次数 |
|---|---|
| 302(正常放行) | 46 |
| 503(被限流拦下) | 24 |
前 40 个是 burst 额度,放行了;后面的超出每秒 5 次的速率,被拦。符合预期。正常访客那点请求量远远够不到这个门槛,浏览器端测试也正常,封面背景都没裂。
小结
几点经验:
- 先搞清楚接口是谁在调用,再谈限制。 一开始想当然觉得"我自己写的接口,锁死内部调用就行",查完才发现是浏览器直连,差点把封面搞裂。这种问题动手前翻一眼调用方最稳妥。
- 不能拦人,就拦频率。 只要正常流量和恶意流量在频率上有区分度,Nginx 一个
limit_req就能解决,不用上多复杂的方案。 - 正则锚点要写死。 location 匹配范围控制好,别一个正则误伤整站其他接口。
目前几个接口运行正常,浏览器端实测无异常,封面背景都没受影响。这次加固就算告一段落了。

Comments NOTHING