composer客户端本身不支持限流防刷,因其无服务进程、不连redis、无法执行原子命令;限流必须由镜像服务端(如nginx+lua或openresty)基于redis实现滑动窗口或令牌桶,并结合请求路径、状态码和行为特征精准识别攻击。

Composer 镜像本身不支持限流或防刷,所谓“基于 Redis 原子操作的 Composer 镜像限流”是概念混淆 —— 限流防刷只能落在镜像服务端(如 Nginx、OpenResty 或自研代理层),而非 Composer 客户端。
为什么不能在 Composer 客户端用 Redis 做限流
Composer 是纯客户端工具,运行时无服务进程、不监听端口、不维护状态。它既不连接 Redis,也无法执行 SETNX 或 INCR 这类原子命令。所有“限流”“防刷”逻辑必须由镜像服务提供方在 HTTP 层实现。
- Composer 执行
composer install时,只发起大量 GET 请求(如/packages.json、/p2/vendor/package/1.0.0.json) - 这些请求最终落到镜像服务器(如 Nginx + Lua 或 Go 编写的反向代理)
- 只有服务端才能读写 Redis、判断 IP/Token 限流、返回 429
镜像服务端如何用 Redis 实现原子限流
以 OpenResty + Redis 为例,核心是用 Lua 脚本封装令牌桶或滑动窗口逻辑,确保 GET 请求计数与过期时间设置原子执行。
- Key 设计建议:
rate:ip:{client_ip}:composer或rate:token:{auth_token}:composer - 避免用固定窗口:用滑动窗口(ZSET)或令牌桶(Lua + EXPIRE)更抗突发
- 必须设置
EXPIRE,否则 Redis 内存持续增长;推荐 TTL 与窗口周期一致(如 60 秒窗口 →EXPIRE key 60) - 示例 Lua 脚本片段(用于 OpenResty 的
access_by_lua_block):
local redis = require "resty.redis"
local red = redis:new()
red:set_timeout(100)
red:connect("127.0.0.1", 6379)
local key = "rate:ip:" .. ngx.var.remote_addr .. ":composer"
local limit = 100
local window = 60
local current = red:incr(key)
if current == 1 then
red:expire(key, window)
end
if current > limit then
ngx.exit(429)
end
防刷重点不是频率,而是请求模式识别
单纯按 IP 限 100 次/分钟没用 —— 攻击者用代理池或 Botnet 可轻松绕过。真正有效的防刷需结合行为特征:
- 拦截高频请求
/p2/*但无对应/packages.json的请求(典型爬虫特征) - 拒绝 User-Agent 包含
composer/以外字符串的请求(伪造 UA) - 对连续 5 次 404 的 IP,自动加入黑名单 1 小时(Redis 中用
SET+EXPIRE) - 不依赖单次请求限流,而用
ZADD记录每秒请求数,再用ZREMRANGEBYSCORE清理旧数据做滑动统计
你最容易忽略的坑:镜像元数据同步延迟导致误限流
当用户请求 /p2/laravel/framework/10.0.0.json,而镜像尚未同步该文件,Nginx 返回 404。若此时你还对该 IP 计数并限流,等于把合法失败请求当攻击处理。
- 务必区分:404(资源不存在)≠ 异常请求,应跳过限流计数
- 在 Lua 脚本中,先
HEAD或GET检查目标路径是否存在,再决定是否 incr - 更稳妥做法:仅对返回 200 的
/packages.json和/p2/*.json请求计数,其余一律放行或单独日志告警
实际部署时,限流阈值和窗口粒度必须随镜像同步延迟动态调整 —— 同步滞后 2 小时的镜像,比实时镜像需要更宽松的限流策略。











