共计 3005 个字符,预计需要花费 8 分钟才能阅读完成。
背景与痛点
现代应用几乎都离不开手机验证码系统,但开发者常面临三大难题:

-
并发请求问题:当大量用户同时请求验证码时,可能导致系统资源耗尽或短信服务商接口被限流。
-
短信轰炸风险:恶意用户可能利用脚本频繁请求验证码,导致正常用户手机被垃圾短信淹没。
-
接口重放攻击:攻击者截获验证码请求后重复发送,消耗系统资源或绕过验证。
技术方案对比
常见的验证方案各有优劣:
- 短信验证:
- 优点:用户认知度高,实施简单
-
缺点:有成本,可能被滥用
-
语音验证:
- 优点:更难自动化攻击
-
缺点:用户体验较差,成本更高
-
TOTP(基于时间的一次性密码):
- 优点:无需网络,无额外成本
- 缺点:需要用户安装认证器应用
对于大多数场景,短信验证仍是平衡安全与体验的最佳选择,关键在于如何优化实现。
核心实现方案
1. JWT 生成时效令牌
使用 JWT 可以为每个验证请求生成有时效的令牌,避免重放攻击:
import jwt
from datetime import datetime, timedelta
SECRET_KEY = "your-secret-key"
def generate_verify_token(phone):
payload = {
'phone': phone,
'exp': datetime.utcnow() + timedelta(minutes=5)
}
return jwt.encode(payload, SECRET_KEY, algorithm='HS256')
2. Redis 分布式锁
防止同一手机号频繁请求验证码:
import redis
from redis.exceptions import LockError
redis_client = redis.Redis(host='localhost', port=6379)
def acquire_lock(phone, timeout=60):
lock_key = f"sms_lock:{phone}"
try:
# 尝试获取锁,设置 60 秒自动过期
return redis_client.set(lock_key, 1, nx=True, ex=timeout)
except LockError:
return False
3. 滑动窗口防御
采用滑动窗口算法限制单位时间内的请求次数:
def check_sms_rate_limit(phone):
window_size = 60 # 60 秒窗口
max_requests = 3 # 最大 3 次
now = int(time.time())
window_start = now - window_size
# 使用 Redis 的 ZSET 实现滑动窗口
key = f"sms_rate:{phone}"
redis_client.zremrangebyscore(key, 0, window_start) # 移除旧记录
current_count = redis_client.zcard(key)
if current_count < max_requests:
redis_client.zadd(key, {now: now})
redis_client.expire(key, window_size)
return True
return False
完整代码示例
以下是 Python 实现的完整验证码服务核心逻辑:
import random
import time
from functools import wraps
class SMSService:
def __init__(self):
self.redis = redis.Redis(host='localhost', port=6379)
def generate_code(self):
return str(random.randint(100000, 999999))
def send_sms(self, phone, code):
# 这里实现实际短信发送逻辑
print(f"发送验证码到{phone}: {code}")
return True
def verify_phone(self, phone):
# 检查手机号格式等基本验证
if not phone or len(phone) != 11:
raise ValueError("手机号格式不正确")
def rate_limit_check(self, phone):
if not check_sms_rate_limit(phone):
raise Exception("请求过于频繁,请稍后再试")
def send_verification_code(self, phone):
self.verify_phone(phone)
self.rate_limit_check(phone)
if not acquire_lock(phone):
raise Exception("操作过于频繁,请稍后再试")
code = self.generate_code()
self.redis.setex(f"sms_code:{phone}", 300, code) # 5 分钟有效期
if self.send_sms(phone, code):
return generate_verify_token(phone)
raise Exception("短信发送失败")
def verify_code(self, phone, code, token):
try:
# 验证 JWT 令牌
payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
if payload['phone'] != phone:
return False
# 验证验证码
stored_code = self.redis.get(f"sms_code:{phone}")
if not stored_code or stored_code.decode() != code:
return False
# 验证成功后删除验证码
self.redis.delete(f"sms_code:{phone}")
return True
except jwt.ExpiredSignatureError:
raise Exception("验证码已过期")
except jwt.InvalidTokenError:
raise Exception("无效的验证令牌")
生产环境建议
性能优化
- Redis 连接池:避免每次操作都新建连接
pool = redis.ConnectionPool(host='localhost', port=6379, max_connections=10)
redis_client = redis.Redis(connection_pool=pool)
- 批量操作:对于大量验证请求,考虑使用 Redis 管道(pipeline)
安全防护
- IP 限流:对同一 IP 的请求进行限制
- 设备指纹:收集设备信息识别异常设备
- 验证码复杂度:使用 6 位以上数字字母组合
监控指标
- 短信发送成功率
- 验证码验证失败率
- 平均响应时间
- 异常请求比例
常见问题与解决方案
-
问题:验证码发送延迟
解决方案:使用消息队列异步处理发送任务 -
问题:Redis 宕机导致服务不可用
解决方案:实现多级缓存或降级方案 -
问题:验证码被暴力破解
解决方案:增加错误尝试次数限制 -
问题:国际号码支持
解决方案:集成第三方验证服务如 Twilio
思考与演进
随着技术发展,手机验证系统也面临新的挑战:
- 如何在保证安全性的同时提升用户体验?
- 无密码认证 (Passwordless) 是否会取代短信验证?
- 生物识别技术如何与现有验证系统结合?
期待与你共同探讨这些问题的解决方案。
正文完
