PHP 发送验证码频率限制:从入门到企业级防刷的完整实践指南
目录导读
- 为什么必须做频率限制? —— 安全与成本的生死线
- 基础频率限制方案 —— 会话、缓存与数据库的三层防线
- 进阶防刷策略 —— 设备指纹、行为分析与分布式限流
- 核心代码实战 —— 基于Redis的原子性限流算法
- 经典问答解析 —— 解决你开发中90%的疑问
- SEO友好技巧 —— 确保内容被搜索引擎高效收录
为什么必须做频率限制?
在PHP开发中,发送短信或邮件验证码是身份认证的核心环节,如果不加任何限制,攻击者可以用自动化脚本在短时间内向任意手机号发送海量请求,导致:

- 经济损失:每条短信都有成本,1秒100条请求就是烧钱。
- 短信轰炸:恶意攻击特定用户手机,造成骚扰。
- 资源耗尽:邮件服务器或第三方API接口被拖垮。
核心原则:任何一个面向公网的验证码接口,都必须默认开启频率限制,这不是可选项,而是安全基线。
基础频率限制方案
1 基于Session的限制(适用于单机调试)
session_start();
$key = 'sms_' . $phone;
$count = $_SESSION[$key]['count'] ?? 0;
if ($count >= 5) {
die('请求过于频繁,请1小时后再试');
}
// 发送逻辑...
$_SESSION[$key]['count'] = $count + 1;
$_SESSION[$key]['time'] = time();
缺点:Session存储于单机内存,分布式部署下完全失效。
2 基于文件/数据库的限制
使用MySQL记录手机号+最新发送时间,加唯一索引控制间隔:
INSERT INTO sms_log (phone, send_time) VALUES ('13800138000', NOW());
SELECT COUNT(*) FROM sms_log WHERE phone='13800138000' AND send_time > DATE_SUB(NOW(), INTERVAL 1 DAY);
3 基于Redis的最佳实践(推荐)
Redis的INCR和EXPIRE天然适合计数器场景。
进阶防刷策略
仅仅限制“次数”远远不够,成熟的系统需要叠加以下维度:
- 设备指纹:基于用户浏览器的UA、Canvas指纹、IP段生成唯一ID,限制同一设备最多发送N条。
- IP频率:同一IP地址每小时最多发送20条,此IP即使更换手机号也会受限。
- 行为验证:发送前强制要求完成滑块验证或点选验证,提高自动化脚本门槛。
- 时段控制:凌晨1点到6点,降低发送阈值,甚至完全禁止。
- 风控黑名单:维护一个包含异常号码、虚拟运营商号段的黑名单库。
核心代码实战:基于Redis的原子性限流
以下是企业级项目中常用的“滑动窗口+多级限流”方案,利用Lua脚本保证原子性,防止并发穿透。
<?php
/**
* 发送验证码 - 频率限制核心类
* 规则:
* 1. 同一手机号 60秒内仅可发1次
* 2. 同一手机号 24小时内最多发5次
* 3. 同一IP 1小时内最多发10次
*/
class SmsRateLimiter {
private $redis;
public function __construct(Redis $redis) {
$this->redis = $redis;
}
/**
* 执行限流检测
* @return bool|string 成功返回true,失败返回提示信息
*/
public function check($phone, $ip) {
// Lua脚本:一次原子操作完成所有判断和计数
$lua = <<<LUA
--- 参数说明
local phone = KEYS[1]
local ip = KEYS[2]
local phone_sec_key = KEYS[3] -- 手机号秒级key
local phone_day_key = KEYS[4] -- 手机号日级key
local ip_hour_key = KEYS[5] -- IP小时级key
local sec_limit = tonumber(ARGV[1]) -- 60秒限制1次
local day_limit = tonumber(ARGV[2]) -- 24小时限制5次
local hour_limit = tonumber(ARGV[3]) -- 1小时限制10次
-- 秒级检查
local sec_count = redis.call('INCR', phone_sec_key)
if sec_count == 1 then
redis.call('EXPIRE', phone_sec_key, 60)
end
if sec_count > sec_limit then
return -1
end
-- 日级检查
local day_count = redis.call('INCR', phone_day_key)
if day_count == 1 then
redis.call('EXPIRE', phone_day_key, 86400)
end
if day_count > day_limit then
return -2
end
-- IP小时级检查
local ip_count = redis.call('INCR', ip_hour_key)
if ip_count == 1 then
redis.call('EXPIRE', ip_hour_key, 3600)
end
if ip_count > hour_limit then
return -3
end
return 1
LUA;
$result = $this->redis->eval(
$lua,
[
$phone,
$ip,
"rate:phone:sec:{$phone}",
"rate:phone:day:{$phone}",
"rate:ip:hour:{$ip}",
1, 5, 10 // 参数
],
5 // KEY数量
);
switch ($result) {
case 1: return true;
case -1: return '发送过于频繁,请60秒后再试';
case -2: return '今日发送次数已达上限,请明天再试';
case -3: return '当前网络环境异常,请稍后再试';
default: return '系统繁忙,请重试';
}
}
}
// 使用示例
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$rate = new SmsRateLimiter($redis);
$msg = $rate->check('13800138000', '192.168.1.1');
if ($msg !== true) {
// 输出错误信息,终止发送
echo json_encode(['success' => false, 'msg' => $msg]);
exit;
}
// 此处写入发送验证码的代码逻辑...
为什么用Lua?
- 多个Redis命令在Lua脚本中执行是原子性的,无需担心高并发下的计数超卖。
- 网络开销从多次RTT降为一次,性能提升明显。
经典问答解析
Q1:基于IP限制时,容易误伤同一个NAT出口下的真实用户(如公司WiFi),如何优化?
A:最好的方案是“IP+UA”组合指纹,或者对IP限流放宽阈值(如1小时50次),同时结合手机号维度做严格限制,另一个思路:如果用户登录了,优先使用用户ID作为限流key,IP仅作为兜底。
Q2:短信轰炸者通过更换手机号来绕过限制怎么办?
A:必须引入设备指纹,先在前端采集canvas、webgl、屏幕分辨率等特征生成哈希传给后端,后端将设备ID与已发送的号码做关联表,如果同一设备在1小时内发送了超过3个不同手机号,触发人工审核或强制滑块验证。
Q3:Redis挂了如何保证服务不崩溃?
A:采用“降级策略”,捕获Redis连接异常后,退化为用数据库表做简单的SELECT COUNT(*) 限制(允许极端情况下每秒1次),虽然性能不如Redis,但能扛住更极端的流量,核心原则:宁可放松限制,不可停止服务。
Q4:如何防止验证码被撞库直接暴力破解?
A:验证码本身必须有独立限流,例如验证码输入错误超过5次,立即作废当前验证码并强制重新获取,验证码应绑定IP和User-Agent,若再次请求时指纹不匹配则直接失效。
Q5:分布式架构下,多个PHP实例如何共享限流数据?
A:百分之百依赖Redis等集中式缓存,千万不能使用本地的APCu或文件存储,如果Redis需要集群,应使用一致性哈希来固定特定手机号到固定Redis节点,避免不同节点上的键冲突。
SEO友好技巧
为了确保本文在必应和谷歌有出色排名,我遵循以下实践:清晰含主关键词**:PHP、发送验证码、频率限制,平铺直叙。
- H1/H2/H3标签层级分明:搜索引擎能快速抓取核心结构。
- 长尾关键词覆盖:如“PHP redis限流”、“防短信轰炸”、“验证码接口防刷”,深度与实用性**:提供可直接运行的Lua代码,降低跳出率,增加外部引用可能。
- 内链与结构化数据:适当推荐相关的PHP安全类文章,并使用
ArticleSchema标记。
本文通过多级防刷策略、原子性Lua脚本以及常见业务场景问答,系统化地解决了PHP验证码接口被滥用的问题,请根据自身业务规模,灵活调整Redis限流参数(阈值和时间窗口),切记,没有任何一种方案是绝对安全的,必须结合日志监控、实时告警,才能构建稳固的安全防线。