1. 短信验证码接口防刷设计概述
短信验证码作为现代互联网服务中最基础的安全验证手段之一,几乎渗透到我们日常使用的每一个应用中。从注册登录到密码重置,从支付确认到敏感操作授权,短信验证码都扮演着至关重要的角色。然而,正是这种普遍性,使得短信接口成为黑产重点攻击的目标。
在实际工作中,我见过太多因为短信接口防护不足导致的惨痛案例:某电商平台一夜之间被刷掉数十万短信费用;某社交应用因验证码爆破导致大量账号被盗;更有甚者,竞争对手通过短信轰炸恶意攻击用户,导致平台口碑直线下降。这些教训告诉我们,短信接口的安全防护绝不是可有可无的"加分项",而是必须严格设计的核心安全环节。
短信防刷的核心矛盾在于:既要有效拦截机器人和恶意流量,又要确保正常用户流畅体验。过于宽松的策略会让黑产有机可乘,而过于严格的限制又会误伤真实用户。这就需要我们建立一套分层、智能的防御体系,针对不同类型的攻击采取差异化的应对策略。
2. 短信接口面临的威胁模型
2.1 批量触发验证码攻击
这是最常见也最直接的攻击方式。攻击者通过脚本自动化批量生成手机号,然后高频调用短信发送接口。我曾处理过一个案例,攻击者利用代理IP池和手机号生成器,在短短2小时内发送了超过5万条验证码短信,直接导致当月短信预算超标。
这类攻击的特点是:
- 使用真实或虚拟手机号
- 请求分布在不同IP上
- 通常针对注册/登录场景
- 目的是消耗短信配额或干扰正常服务
防御关键在于建立多维度的限流机制,不能仅依赖IP限制,因为现代攻击者很容易获取大量代理IP。
2.2 短信轰炸攻击
与批量攻击不同,短信轰炸是集中针对特定手机号进行高频验证码发送。去年我们平台就遭遇过一次有组织的骚扰攻击,攻击者锁定几个目标用户,每分钟发送数十条验证码,导致用户手机完全无法正常使用。
这类攻击的特征包括:
- 固定手机号高频请求
- 可能来自不同IP和设备
- 通常没有验证意图
- 目的是骚扰特定用户
应对策略需要重点保护单个手机号的发送频率,同时结合行为分析识别异常模式。
2.3 SMS Pumping/AIT欺诈
这是一种更为隐蔽且危害巨大的攻击方式。黑产会诱导用户在某些页面输入手机号(通常伪装成正常服务),然后利用这些手机号向特定高溢价号码段发送验证码。由于某些国家的短信资费较高,攻击者可以从中套利。
这类攻击的典型模式:
- 针对特定国家/运营商号段
- 验证码几乎不会被验证
- 短时间内爆发性增长
- 目的是利用短信资费差套利
防御需要结合号码归属分析、发送行为分析和成本熔断机制。
2.4 验证码爆破攻击
攻击者不直接攻击发送接口,而是针对验证接口进行暴力破解。常见手段包括:
- 高并发尝试常见验证码组合(如000000,123456)
- 利用系统漏洞重置失败计数
- 分布式尝试降低被封风险
我曾审计过一个系统,由于其验证接口没有失败计数限制,导致6位验证码在15分钟内被完全爆破。
2.5 分布式绕过攻击
高级攻击者会采用多种手段规避传统防御:
- 使用僵尸网络分散请求源
- 轮换设备指纹和UA
- 模拟正常用户行为轨迹
- 低频率持续攻击避免触发阈值
这类攻击最难防御,需要结合多维度行为分析和机器学习模型。
3. 防御体系设计原则
3.1 分层防御理念
有效的短信防刷系统应该像洋葱一样层层防护:
- 边缘层:基础校验和粗粒度限流
- 业务层:精细规则和风险决策
- 数据层:行为分析和模型判断
- 供应商层:运营商级防护
每层都有自己的防御重点和处置手段,即使某一层被绕过,其他层仍能提供保护。
3.2 渐进式挑战机制
不是所有请求都需要同等强度的验证。我们的策略是:
- 低风险:直接放行
- 中风险:简单人机验证
- 高风险:强身份验证或拒绝
这种渐进式方法可以在安全性和用户体验间取得平衡。实践中,我们使用风险评分来决定挑战等级,评分因素包括:
- 设备可信度
- IP信誉
- 行为模式
- 历史记录
3.3 成本控制优先
必须将短信视为付费资源而非普通API。我们的防护策略始终围绕成本控制展开:
- 严格预算和配额管理
- 实时成本计算和告警
- 自动熔断机制
- 高风险号段特殊处理
4. 发送接口详细实现
4.1 请求设计与验证
良好的接口设计是防护的第一道防线。我们的发送接口规范如下:
json复制POST /api/v1/sms/send
{
"phone": "+8613812345678", // E.164格式
"scene": "login", // 场景标识
"device_id": "a1b2c3d4", // 设备指纹
"captcha_token": "xxx", // 人机验证令牌
"client_meta": { // 客户端元数据
"ua": "Mozilla/5.0...",
"app_version": "3.2.1",
"os": "iOS 15.4",
"lang": "zh-CN"
}
}
关键验证点:
- 手机号格式和号段校验
- 场景白名单检查
- 请求签名防篡改
- 时间戳和nonce防重放
经验分享:我们曾因为未校验场景参数导致攻击者通过修改scene值绕过部分限制。建议对所有输入参数进行严格校验。
4.2 多维限流实现
限流是短信防刷的核心。我们采用分层限流策略:
手机号维度
python复制# Redis Lua脚本实现原子化限流
local key = "sms:rl:phone:"..KEYS[1]..":"..KEYS[2]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local current = redis.call("LLEN", key)
if current >= limit then
return 0
else
redis.call("RPUSH", key, ARGV[3])
redis.call("EXPIRE", key, window)
return 1
end
典型配置:
- 1次/60秒(防轰炸)
- 5次/10分钟(防密集尝试)
- 10次/24小时(防长期滥用)
IP维度
python复制# 使用滑动窗口算法
local key = "sms:rl:ip:"..KEYS[1]..":"..KEYS[2]
local now = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local limit = tonumber(ARGV[3])
redis.call("ZREMRANGEBYSCORE", key, 0, now-window)
local count = redis.call("ZCARD", key)
if count >= limit then
return 0
else
redis.call("ZADD", key, now, now)
redis.call("EXPIRE", key, window)
return 1
end
典型配置:
- 20次/10分钟(按业务调整)
- 对/24网段聚合限流(防分布式攻击)
设备维度
实现方式与IP类似,关键点:
- 使用可靠的设备指纹生成算法
- 结合多种设备特征提高稳定性
- 新设备特殊处理
国家/号段维度
python复制# 高风险国家特殊限制
local country = get_country_code(phone)
if country in HIGH_RISK_COUNTRIES:
apply_stricter_limits()
4.3 风控引擎集成
我们的风控系统会实时计算请求风险分数:
python复制def calculate_risk_score(request):
score = 0
# 设备风险
if request.device_id not in known_devices:
score += 20
# IP风险
if ip_is_proxy(request.ip):
score += 30
# 行为异常
if request.interval < NORMAL_THRESHOLD:
score += 15
# 号段风险
if request.phone in RISKY_PREFIXES:
score += 25
return score
根据分数采取不同措施:
- 0-30: 直接放行
- 31-60: 要求人机验证
- 61-100: 拒绝并记录
4.4 OTP生成与存储
验证码的安全处理至关重要:
python复制def generate_otp_record(scene, phone):
otp = random.randint(100000, 999999)
salt = os.urandom(16)
otp_hash = hashlib.sha256(f"{otp}{salt}".encode()).hexdigest()
record = {
"otp_hash": otp_hash,
"salt": salt.hex(),
"expire_at": time.time() + 600,
"used": False
}
redis_key = f"sms:otp:{scene}:{phone}"
redis.setex(redis_key, 600, json.dumps(record))
return otp
关键安全措施:
- 不存储明文OTP
- 每个验证码单独salt
- 严格TTL控制
- 标记使用状态
5. 验证接口防护设计
5.1 验证流程安全实现
验证接口同样需要严格防护:
python复制def verify_otp(scene, phone, code):
# 限流检查
if is_rate_limited(f"verify:{phone}"):
raise RateLimitExceeded()
# 获取OTP记录
record = get_otp_record(scene, phone)
if not record:
raise OtpInvalid()
# 状态检查
if record["used"]:
raise OtpUsed()
if record["expire_at"] < time.time():
raise OtpExpired()
# 验证哈希
input_hash = hashlib.sha256(f"{code}{record['salt']}".encode()).hexdigest()
if not secure_compare(input_hash, record["otp_hash"]):
# 失败计数
increment_fail_count(phone)
raise OtpInvalid()
# 标记已使用
mark_otp_used(scene, phone)
return True
5.2 失败计数设计
防爆破的关键是合理的失败计数:
python复制def increment_fail_count(phone):
key = f"sms:otp:fail:{phone}"
fails = redis.incr(key)
if fails == 1:
redis.expire(key, 600) # 10分钟TTL
if fails >= MAX_FAILS:
lock_key = f"sms:otp:lock:{phone}"
redis.setex(lock_key, LOCK_TIME, 1)
raise OtpLocked()
重要细节:
- 失败计数独立于OTP有效期
- 锁定期间禁止所有验证尝试
- 不透露具体剩余尝试次数
6. 高级防护策略
6.1 成本熔断机制
实时监控短信成本:
python复制class SmsBudgetMonitor:
def __init__(self):
self.budgets = load_budget_config()
def check_budget(self, country, provider):
key = f"sms:budget:{country}:{provider}"
cost = redis.incrbyfloat(key, get_message_cost(country))
if cost > self.budgets[country]["alert"]:
trigger_alert()
if cost > self.budgets[country]["limit"]:
enable_circuit_breaker(country)
熔断后处理:
- 通知相关人员
- 只允许白名单号码发送
- 自动恢复机制
6.2 号码预校验服务
集成第三方号码验证:
python复制def precheck_phone(phone):
# 缓存检查
cache_key = f"phone:precheck:{phone}"
if cached := redis.get(cache_key):
return json.loads(cached)
# 调用第三方API
result = third_party_api.check_number(phone)
# 缓存结果
redis.setex(cache_key, 604800, json.dumps(result)) # 7天
return result
验证内容包括:
- 号码有效性
- 运营商信息
- 号码类型(虚拟/实体)
- 风险评分
7. 监控与告警体系
7.1 关键监控指标
我们使用Prometheus监控以下指标:
python复制# 发送量统计
SMS_SEND_TOTAL = Counter(
"sms_send_total",
"Total SMS sent",
["scene", "country", "provider", "result"]
)
# 验证成功率
SMS_VERIFY_SUCCESS_RATE = Gauge(
"sms_verify_success_rate",
"OTP verification success rate",
["scene"]
)
# 限流触发
RATE_LIMIT_TRIGGERS = Counter(
"sms_rate_limit_triggers",
"Rate limit triggers by dimension",
["dimension", "rule"]
)
7.2 告警规则配置
典型告警规则示例:
yaml复制groups:
- name: sms-alerts
rules:
- alert: HighSMSCost
expr: sum(rate(sms_cost_estimated_total[5m])) by (country) > 100
for: 10m
labels:
severity: critical
annotations:
summary: "High SMS cost in {{ $labels.country }}"
- alert: LowOTPConversion
expr: sms_verify_success_rate < 0.2
for: 30m
labels:
severity: warning
annotations:
summary: "Low OTP conversion rate ({{ $value }})"
8. 实战经验与优化建议
8.1 性能优化技巧
在高并发场景下,我们总结了以下优化经验:
- Redis管道化:将多个相关操作打包执行
python复制with redis.pipeline() as pipe:
pipe.incr("counter")
pipe.expire("counter", 60)
pipe.execute()
- 本地缓存:对静态数据(如黑名单)使用本地缓存
python复制@lru_cache(maxsize=1024)
def is_blacklisted(phone_prefix):
return db.query("SELECT 1 FROM blacklist WHERE prefix = ?", phone_prefix)
- 异步写日志:不影响主流程的关键日志记录
python复制async def audit_log_async(action, data):
await log_queue.put({"action": action, "data": data, "time": time.time()})
8.2 常见陷阱与解决方案
问题1:限流被并发请求绕过
解决方案:使用Redis Lua脚本保证原子性
问题2:验证码爆破攻击
解决方案:失败计数不随验证码更新而重置
问题3:虚拟号码消耗预算
解决方案:集成号码预校验服务
问题4:跨国攻击成本激增
解决方案:按国家设置差异化限制和熔断
问题5:设备指纹被篡改
解决方案:多因素设备识别+行为分析
9. 演进路线与高级方案
对于需要更高安全级别的场景,可以考虑以下进阶方案:
9.1 机器学习风控模型
构建用户行为基线:
python复制class BehaviorModel:
def __init__(self):
self.model = load_pretrained_model()
def evaluate(self, request):
features = extract_features(request)
return self.model.predict(features)
特征工程包括:
- 请求时间分布
- 设备使用模式
- 地理位置轨迹
- 历史行为对比
9.2 多因素验证增强
对于高风险操作,实施阶梯验证:
- 短信验证码
- 生物识别(指纹/面容)
- 硬件安全密钥
- 人工审核
9.3 供应商协同防护
与短信服务商深度合作:
- 启用供应商端欺诈检测
- 共享黑名单数据
- 实时流量协同分析
- 定制路由策略
10. 总结与最佳实践
经过多年实战,我们总结了短信防刷的"黄金法则":
- 限流是基础:必须实现多维度、分层级的限流策略
- 状态机是关键:OTP的生命周期管理必须严谨
- 成本控制是底线:实时监控和熔断机制必不可少
- 防御要纵深:单一防护措施很容易被绕过
- 可观测性是保障:没有监控的风控就是"盲人摸象"
对于刚起步的系统,建议优先实现:
- 手机号+IP+设备多维限流
- 严格的OTP状态管理
- 基本的风险评分
- 关键指标监控
随着业务发展,再逐步加入:
- 号码预校验
- 机器学习风控
- 供应商协同防护
- 多因素验证
最后要记住,安全是一个持续的过程。黑产的技术也在不断进化,我们的防御策略需要定期评估和更新。每次大促或活动前,都应该对短信系统进行压力测试和攻防演练,确保防护措施始终有效。