短信验证码已经成为现代互联网服务的标配功能,从用户注册到支付确认,几乎每个关键操作环节都需要它的参与。我经历过三个需要短信验证码系统的项目,踩过不少坑之后,总结出一套稳定可靠的实现方案。
这个功能看似简单,但实际开发中会遇到各种意想不到的问题:短信延迟、验证码被刷、通道不稳定等等。本文将分享从零开始构建短信验证码功能的完整方案,包括服务选型、代码实现和防刷策略,这些都是我在实际项目中验证过的可靠方案。
国内常见的短信服务平台主要有阿里云短信、腾讯云短信和七牛云短信等。经过实际测试,我整理了一个关键参数对比表:
| 服务商 | 到达率 | 价格(条) | 到达速度 | 接口稳定性 | 特色功能 |
|---|---|---|---|---|---|
| 阿里云 | 99.5% | 0.045元 | 3-5秒 | 高 | 支持模板变量 |
| 腾讯云 | 99.3% | 0.042元 | 2-4秒 | 高 | 与微信生态整合好 |
| 七牛云 | 98.8% | 0.038元 | 4-6秒 | 中 | 价格优势明显 |
提示:选择服务商时不仅要看价格,更要关注到达率和稳定性。有些低价服务商在高峰时段会出现明显延迟。
以阿里云为例,申请短信服务需要以下步骤:
python复制# 示例:阿里云Python SDK初始化
from aliyunsdkcore.client import AcsClient
from aliyunsdkcore.request import CommonRequest
client = AcsClient(
'<your-access-key-id>',
'<your-access-key-secret>',
'default'
)
验证码数据需要单独设计表结构,我推荐以下字段:
sql复制CREATE TABLE sms_verification (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
phone VARCHAR(20) NOT NULL,
code VARCHAR(10) NOT NULL,
scene VARCHAR(50) NOT NULL COMMENT '使用场景',
ip VARCHAR(50) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expired_at TIMESTAMP NOT NULL,
is_used TINYINT(1) DEFAULT 0,
INDEX idx_phone_scene (phone, scene)
);
关键设计考虑:
发送验证码的API需要包含以下安全措施:
java复制// Java示例:发送短信验证码
@RestController
@RequestMapping("/api/sms")
public class SmsController {
@PostMapping("/send")
public ResponseEntity<?> sendVerificationCode(
@RequestParam String phone,
@RequestParam String scene,
HttpServletRequest request) {
// 1. 频率检查
if(rateLimitService.isLimited(phone, scene)) {
return ResponseEntity.badRequest().body("发送频率过高");
}
// 2. 生成验证码
String code = generateRandomCode(6);
// 3. 保存到数据库
smsService.saveCode(phone, code, scene, request.getRemoteAddr());
// 4. 调用短信平台
boolean success = smsPlatform.send(phone, scene, code);
return success ?
ResponseEntity.ok().build() :
ResponseEntity.status(500).body("短信发送失败");
}
}
重要场景(如注册、支付)应该先通过图形验证码验证:
javascript复制// 前端实现示例
async function sendSms() {
// 先获取图形验证码
const captcha = await getCaptcha();
// 验证通过后再请求短信
const res = await fetch('/api/sms/send', {
method: 'POST',
body: JSON.stringify({
phone: '13800138000',
scene: 'register',
captcha: captcha
})
});
}
建立简单的风控规则可以有效防止刷短信:
python复制# Python风控示例
def check_risk(phone, ip):
# 检查1小时内该IP的发送次数
hour_count = SmsLog.objects.filter(
ip=ip,
created_at__gte=timezone.now()-timedelta(hours=1)
).count()
# 检查该手机号今日发送次数
day_count = SmsLog.objects.filter(
phone=phone,
created_at__gte=timezone.now()-timedelta(days=1)
).count()
if hour_count > 30 or day_count > 10:
raise RiskException("操作过于频繁")
验证码校验需要处理多种边界情况:
go复制// Go示例:验证码校验
func VerifyCode(phone, scene, code string) bool {
// 查询最新未使用的验证码
record, err := db.GetLatestCode(phone, scene)
if err != nil {
return false
}
// 检查是否已使用
if record.IsUsed {
return false
}
// 检查是否过期
if time.Now().After(record.ExpiredAt) {
return false
}
// 检查验证码是否匹配
if record.Code != code {
return false
}
// 标记为已使用
db.MarkAsUsed(record.ID)
return true
}
在微服务架构中,需要考虑:
java复制// Redis实现示例
public boolean verifyCode(String phone, String scene, String code) {
String key = "sms:" + scene + ":" + phone;
String storedCode = redis.get(key);
if(storedCode == null || !storedCode.equals(code)) {
return false;
}
// 验证成功后删除key
redis.del(key);
return true;
}
高峰期可以采用消息队列缓解压力:
python复制# Celery异步任务示例
@app.task
def async_send_sms(phone, scene, code):
try:
sms_client.send(phone, scene, code)
except Exception as e:
logger.error(f"短信发送失败: {e}")
# 加入重试队列
self.retry(exc=e)
重要业务应该配置备用短信通道:
java复制// 多通道切换示例
public void sendSms(String phone, String content) {
for (SmsProvider provider : providers) {
try {
provider.send(phone, content);
monitor.recordSuccess(provider);
break;
} catch (Exception e) {
monitor.recordFailure(provider);
}
}
}
完善的监控应该包括:
javascript复制// 监控示例
const stats = {
total: 0,
success: 0,
failures: 0,
lastError: null
};
async function sendSms(phone, code) {
stats.total++;
try {
await smsProvider.send(phone, code);
stats.success++;
} catch (err) {
stats.failures++;
stats.lastError = err.message;
// 触发报警
if(stats.failures > 10) {
alertAdmin('短信发送连续失败');
}
}
}
重要提示:千万不要在响应中返回生成的验证码,这是常见的安全漏洞。测试环境可以通过日志查看,但生产环境必须严格保密。
我在实际项目中遇到过这些问题:
这些经验教训让我明白,短信验证码功能需要持续优化和监控,不能一劳永逸。