本文目录导读:

- 基础层:Nginx 层面限流(最高效)
- 应用层:Token + 时间窗口(最常用)
- 更简单的方案:Redis 计数器(固定窗口)
- 签名验证 + 时间戳(防重放攻击)
- 行为验证:图形验证码 / 滑块验证
- 设备指纹(高级方案)
- 黑白名单 + 自动封禁
- 方案组合建议
- 补充注意事项
针对 PHP 项目的接口防刷,核心目标是防止恶意高频请求、脚本重复调用和资源滥用,以下是多层次的实现方案,建议根据项目规模和精度组合使用。
基础层:Nginx 层面限流(最高效)
在进入 PHP 应用之前拦截,性能最好,适合全局防护。
限制单 IP 并发连接数:
limit_conn_zone $binary_remote_addr zone=addr:10m;
server {
location /api/ {
limit_conn addr 5; # 每个IP最多5个并发连接
limit_conn_status 429;
}
}
限制请求速率:
limit_req_zone $binary_remote_addr zone=one:10m rate=10r/s;
server {
location /api/ {
limit_req zone=one burst=5 nodelay; # 每秒10个请求,突发允许5个
limit_req_status 429;
}
}
应用层:Token + 时间窗口(最常用)
在 PHP 内部实现,可按用户、IP、接口精细化控制。
方案:滑动窗口计数器(推荐)
使用 Redis 的 zset(有序集合)记录请求时间戳,时间窗口滑动淘汰过期数据。
class RateLimiter {
private $redis;
private $key;
public function __construct($key, $maxRequests = 60, $window = 60) {
$this->redis = new Redis();
$this->redis->connect('127.0.0.1', 6379);
$this->key = $key;
$this->maxRequests = $maxRequests;
$this->window = $window;
}
public function allow() {
$now = microtime(true);
$start = $now - $this->window;
// 移除窗口外的旧记录
$this->redis->zRemRangeByScore($this->key, 0, $start);
$count = $this->redis->zCard($this->key);
if ($count >= $this->maxRequests) {
return false; // 拒绝
}
// 添加当前请求记录
$this->redis->zAdd($this->key, $now, $now . rand(1000, 9999));
// 设置过期时间,避免内存泄漏
$this->redis->expire($this->key, $this->window + 10);
return true;
}
}
// 使用示例:
$limiter = new RateLimiter("user:123:api:login", 5, 60); // 60秒内5次
if (!$limiter->allow()) {
http_response_code(429);
die(json_encode(['error' => '请求太频繁']));
}
注意:zAdd 时要确保唯一性(后面加随机数),避免重复覆盖。
更简单的方案:Redis 计数器(固定窗口)
适合轻量级场景,但会有“临界点”问题(窗口最后一秒突发)。
$key = "rate:{userId}:api:login";
$limit = 5;
$window = 60; // 秒
$current = $redis->get($key);
if ($current && $current >= $limit) {
die("请求过于频繁");
}
$redis->multi();
$redis->incr($key);
$redis->expire($key, $window);
$redis->exec();
签名验证 + 时间戳(防重放攻击)
适合敏感接口(支付、登录),防止请求被截获后重复发送。
// 服务端验证
function validateRequest($params, $signature, $timestamp, $timeout = 300) {
// 1. 检查时间戳是否过期
if (abs(time() - $timestamp) > $timeout) {
return false;
}
// 2. 检查签名是否被使用过(防重放)
$nonceKey = "nonce:{$params['nonce']}";
if ($redis->exists($nonceKey)) {
return false;
}
$redis->setex($nonceKey, $timeout, 1);
// 3. 计算签名并比对
$expected = md5(ksort($params) . $secretKey . $timestamp);
return $expected === $signature;
}
请求方逻辑:
$params = ['action' => 'pay', 'amount' => 100];
$params['nonce'] = uniqid(); // 随机数,保证唯一
$params['timestamp'] = time();
ksort($params);
$sign = md5(implode('', $params) . $secretKey);
行为验证:图形验证码 / 滑块验证
在 N 次失败后强制要求验证码,可以有效阻止脚本攻击。
if ($redis->get("fail:user:{$uid}") > 3) {
// 前端需要传入验证码token
if (!verifyCaptcha($_POST['captcha_token'])) {
die(json_encode(['code' => 1001, 'msg' => '需要人机验证']));
}
$redis->del("fail:user:{$uid}");
}
设备指纹(高级方案)
通过收集客户端 UA、屏幕分辨率、Canvas、WebGL 等信息生成指纹,识别异常访问。
可在前端生成指纹传给后端,后端判断该指纹是否异常。
黑白名单 + 自动封禁
结合日志监控,对异常 IP/UserID 自动动态封禁。
function checkBanned($userId, $ip) {
$banKey = "ban:user:{$userId}";
$banIp = "ban:ip:{$ip}";
if ($redis->exists($banKey) || $redis->exists($banIp)) {
die("访问已被封禁");
}
}
function banUser($userId, $duration = 3600) {
$redis->setex("ban:user:{$userId}", $duration, 1);
}
可以结合 fail2ban 或 openresty 做更灵活的封禁。
方案组合建议
| 场景 | 推荐方案 |
|---|---|
| 全局限流 | Nginx limit_req + Redis 计数器 |
| 登录/注册 | 滑动窗口 + 验证码 + 失败次数封禁 |
| 支付/订单 | 签名验证 + nonce + 防重放 |
| 高并发抢购 | 滑动窗口 + 队列削峰 + 令牌桶 |
| API 对外开放 | 签名 + AppKey + 调用额度控制 |
补充注意事项
- 响应码统一: 所有拒绝请求应返回
HTTP 429 Too Many Requests,方便前端处理。 - 错误信息提示: 不要暴露具体原因(如“你被限流了”),统一返回“请求过于频繁,请稍后重试”。
- 合理设置阈值: 根据业务正常用户的峰值 + 20%~30% 余量设置。
- 日志记录: 拦截的请求应该记录日志,便于复盘和优化。
- 降级策略: 当 Redis 或其他依赖不可用时,应该自动降级(如临时去掉限流),而不是导致整个系统不可用。