Webman投票系统防刷需全局中间件挂载并排除静态资源、用Lua原子脚本防Redis超限、穿透代理获取真实IP、自然日切分时间窗口、数据库唯一索引+事务兜底,缺一不可。

Webman 投票系统防刷,不能只靠单层拦截——关键看限流策略是否落在请求入口、是否能穿透代理、是否在高并发下不失效。
限流中间件必须挂载在全局入口,且排除静态资源
Webman 的中间件注册顺序决定它能否真正拦住恶意请求。如果把限流中间件写在 config/middleware.php 的 '' 键下(即全局中间件),它会拦截所有请求,包括 /static/js/app.js 这类资源路径。这会导致前端加载失败或误触阈值。
- 务必在中间件逻辑里用
if (str_starts_with($request->getUri()->getPath(), '/static/')) return $next($request);显式跳过静态路径 - 若只对 API 路径限流,应改用路由级中间件:
Route::post('/api/v1/vote', [VoteController::class, 'store'])->middleware([RateLimiterMiddleware::class]); - Webman 1.0.12 以下版本不支持
Route::group()自动继承中间件,必须每个子路由单独绑定,否则形同虚设
Redis 驱动下必须用 Lua 原子脚本,否则高并发超限
配置写成 [100, 600](每 10 分钟最多 100 次)看似合理,但若底层用 INCR + EXPIRE 两步操作,在并发场景下会漏判:两个请求同时读到计数 99,各自 INCR 后变成 101,突破限制。
-
tinywan/limit-traffic默认封装 Lua 脚本,webman/rate-limiter1.1.0+ 版本才默认启用原子操作 - 检查驱动是否生效:用
redis-cli monitor观察是否出现EVAL命令,而非零散的INCR和EXPIRE - 若用
webman/rate-limiter旧版本,需手动替换 Redis 驱动为 Lua 实现,或升级包
用户身份验证必须穿透 session + DB 双校验,不能只信 cookie
仅靠 $_COOKIE['voted'] 或 $_SESSION['voted'] 标记,刷票者清 Cookie、换浏览器、禁用 JS 就能绕过。真实业务中必须查库确认该用户是否已投过本轮投票。
立即学习“PHP免费学习笔记(深入)”;
- 投票前执行 SQL:
SELECT 1 FROM votes WHERE user_id = ? AND poll_id = ? LIMIT 1,存在则拒绝 - 写入时用
INSERT ... ON DUPLICATE KEY UPDATE防止重复插入(user_id + poll_id设唯一索引) - 不要依赖
$_SERVER['REMOTE_ADDR']获取 IP,需用request()->getRealIP()兼容 NginxX-Forwarded-For场景
时间窗口必须按自然日切分,不能只靠 time() + 86400
“每人每天限投 1 票”这类规则,若用 time() + 86400 计算过期,会导致跨天边界错乱:用户 23:59 投票,token 到次日 23:59 才失效,实际覆盖了两天。
- 正确做法是用日期字符串作 key,例如
vote:20260523:uid_123,每日凌晨自动清理或靠 TTL 自然过期 - 数据库中存
voted_at DATE字段,查询时用WHERE voted_at = CURDATE(),避免时区和夏令时干扰 - 若用 Redis 存自然日计数,设置 TTL 为
86400 - (当前秒数 % 86400),确保午夜重置
最易被忽略的是:所有防刷逻辑必须在事务内完成,且数据库唯一约束不可省略。哪怕限流中间件拦住了 99% 请求,剩下 1% 的并发写入若没唯一索引兜底,照样产生脏数据。



















