注册、登录、找回密码、支付确认,这些环节几乎都要用到短信验证码。发一条短信看起来简单,真正麻烦的是三件事:验证码能不能稳定到达,会不会被人拿去刷,接入后的成本能不能算清楚。阿里云短信服务的验证码接口就是为这几件事准备的。这篇文章以一个注册登录场景为线,讲接入思路、关键步骤,以及开通后容易踩的坑。

一、为什么用阿里云短信验证码接口
选短信服务商,通常看三点:到达率、接入难度和风控能力。阿里云短信服务在国内运营商通道上用得比较多,三大运营商都能覆盖,面向国内用户的产品用它比较省心。验证码属于高频、强时效的场景,有几点要提前了解:
- 模板要先报备:验证码内容需要先提交模板,审核通过才能发送。前期多一道流程,但内容有据可查,后面也不容易因为随意改文案出问题。
- SDK 和 HTTP 接口都能用:官方提供 Java、Python、Node.js、Go 等 SDK。不想引入依赖的话,直接调用 HTTP 接口也可以。
- 发送记录可以查:控制台能看到每条短信的发送状态。用户反馈“没收到”时,先从这里查起。
- 支持频率限制:可以按手机号、IP 设置发送频率,挡住一部分短信轰炸。
计费标准、模板审核时长和通道质量都可能随政策调整。正式选型前,以官方文档和价格页的最新内容为准,别拿旧文章里的数字做预算。
二、接入前要先准备的几件事
代码还没写,有几件事要先定下来,否则联调时很容易卡在权限或模板上。
1. 开通服务,给子账号单独授权
在阿里云控制台开通短信服务,然后在 RAM 访问控制里创建一个子账号,只授予短信相关权限,不要直接用主账号的 AccessKey。AccessKey 一旦泄露,子账号的权限决定了损失范围,这一步越早做越省事。
2. 申请签名和模板
发短信需要两样东西:签名和模板。签名一般是公司或产品名称,需要提供营业执照等资料审核。模板是带变量的正文,比如:
您的验证码是 ${code},5 分钟内有效,请勿泄露给他人。
${code} 是占位符,发送时传入真实验证码即可。模板写得越直白越好,用途一眼能看出来,不要夹带营销内容,否则很可能被驳回。
3. 先把业务参数定下来
动手前,先回答几个问题:验证码几分钟过期?同一手机号每天最多发几次?同一 IP 每分钟允许多少请求?输错几次后锁定?这些数字决定后端的校验逻辑,也直接影响体验和安全。如果一开始定得含糊,后面改起来会很痛。
三、核心实现:一个注册登录的例子
下面用一个电商平台的注册登录场景,说明验证码从生成、发送到校验的完整链路。假设这个平台日活不低,注册高峰时短信请求明显变多,也担心有人恶意刷短信,所以对风控和稳定性都有要求。
1. 生成并存储验证码
验证码由服务端生成,使用安全随机数,存入 Redis,过期时间与有效期保持一致。键名按“业务类型 + 手机号”组织,比如 sms:login:13800000000。
常见错误是把验证码返回给前端,或者打印到日志里。前端只需要知道发送成功与否,验证码应该只留在服务端。
2. 调用阿里云接口发送
以 Java 为例,使用官方 SDK 发送短信的核心代码如下:
import com.aliyun.dysmsapi20170525.Client;
import com.aliyun.dysmsapi20170525.models.SendSmsRequest;
import com.aliyun.dysmsapi20170525.models.SendSmsResponse;
import com.aliyun.teaopenapi.models.Config;
import java.security.SecureRandom;
import java.util.concurrent.TimeUnit;
public class SmsCodeService {
private static final int CODE_LENGTH = 6;
private static final long EXPIRE_SECONDS = 300;
private final Client smsClient;
private final RedisTemplate redisTemplate;
private final SecureRandom random = new SecureRandom();
public SmsCodeService(Client smsClient, RedisTemplate redisTemplate) {
this.smsClient = smsClient;
this.redisTemplate = redisTemplate;
}
/**
* 发送登录验证码
* @param phone 手机号(已做格式校验)
* @return 是否发送成功
*/
public boolean sendLoginCode(String phone) throws Exception {
// 1. 生成验证码
String code = generateCode();
// 2. 调用阿里云发送短信
SendSmsRequest request = new SendSmsRequest()
.setPhoneNumbers(phone)
.setSignName("你的签名")
.setTemplateCode("SMS_XXXXXXX")
.setTemplateParam("{\"code\":\"" + code + "\"}");
SendSmsResponse response = smsClient.sendSms(request);
if (!"OK".equals(response.getBody().getCode())) {
return false;
}
// 3. 发送成功后再写入缓存,避免发送失败却残留一个用户收不到的验证码
String key = "sms:login:" + phone;
redisTemplate.opsForValue().set(key, code, EXPIRE_SECONDS, TimeUnit.SECONDS);
return true;
}
private String generateCode() {
int bound = (int) Math.pow(10, CODE_LENGTH);
int value = random.nextInt(bound);
return String.format("%0" + CODE_LENGTH + "d", value);
}
}
这段代码有两处值得留意。第一,验证码用 SecureRandom 生成,java.util.Random 的随机性不够,不适合用在验证场景。第二,先调用发送接口,成功后再写缓存。如果顺序反过来,发送失败时缓存里就会留下一个用户根本收不到的验证码。
3. 校验验证码
校验要处理三种情况:正确、过期、错误次数过多。下面是一个简化版本:
// 需要 import java.security.MessageDigest 和 java.nio.charset.StandardCharsets
private static final int MAX_FAILS = 5;
public boolean verifyLoginCode(String phone, String inputCode) {
String key = "sms:login:" + phone;
String failKey = "sms:fail:" + phone;
// Redis 的计数值可能是 Long 或 String,统一按 Number 处理
Object failRaw = redisTemplate.opsForValue().get(failKey);
int fails = failRaw == null ? 0 : ((Number) failRaw).intValue();
if (fails >= MAX_FAILS) {
return false;
}
String cached = (String) redisTemplate.opsForValue().get(key);
if (cached == null) {
return false; // 已过期或从未发送
}
// 常量时间比较,避免通过响应耗时猜测验证码
if (MessageDigest.isEqual(cached.getBytes(StandardCharsets.UTF_8),
inputCode.getBytes(StandardCharsets.UTF_8))) {
// 校验成功后立即删除,防止重复使用
redisTemplate.delete(key);
redisTemplate.delete(failKey);
return true;
}
Long n = redisTemplate.opsForValue().increment(failKey);
if (n != null && n == 1L) {
// 只在第一次失败时设置过期,避免每次失败都把锁定窗口往后推
redisTemplate.expire(failKey, 300, TimeUnit.SECONDS);
}
return false;
}
校验成功后立即删除验证码,是为了防止同一个验证码被重复使用。错误计数用来防止暴力枚举。失败上限和锁定时长最好做成配置项,别写死在代码里,方便后面根据业务调整。
四、风控和成本控制
验证码接口最常见的风险是被人刷。短信费用涨得很快,正常用户的到达率也可能受影响。风控最好分层做。
第一层是频率限制,按手机号限制每分钟一次、每天十次,按 IP 限制单位时间内的总请求量。第二层是人机验证,比如图形验证码或滑块,在发送前把机器请求拦下来。第三层是监控,发送量突然拉高,或者某些号段的失败率明显上升,都应该触发告警。
有个很常见的走向:只限制手机号时,攻击者会换着号码刷,单日费用很快超出预算。补上 IP 维度和滑块之后,大部分异常请求会被拦下。所以单一维度的限制通常不够,几种办法要组合使用。
五、常见问题怎么查
用户收不到验证码
先看阿里云控制台的发送记录,确认请求是否成功,回执状态是什么。如果显示发送成功但用户没收到,多半是运营商拦截、内容被判为垃圾短信,或者用户手机把陌生号码拦截了。这时可以检查模板里有没有敏感词,并准备语音验证码作为备用通道。
模板审核被驳回
常见原因有三类:模板带营销词、用途描述不清、变量格式不规范。签名和备案主体不一致也会卡住流程。提交前对照官方审核规范逐条检查,比来回修改省时间。
接口返回限流错误
这说明发送频率超过了平台或账号的限额。前端加一个倒计时,避免用户反复点击。服务端的重试要设置上限,不要对同一请求无限重试,否则只会让限流越来越严。
六、上线前检查
- 确认子账号权限最小化,AccessKey 从环境变量或配置中心读取,不要写死在代码里。
- 有效期、发送频率、错误次数上限都写入配置,上线前逐项核对实际数值。
- 日志中不记录完整验证码,手机号做脱敏,比如只保留前三位和后四位。
- 在测试环境用白名单号码跑通全流程,正常、过期、输错、超限四种情况都要覆盖。
- 在云账单相关功能里设置费用提醒,异常调用出现时能及时收到通知。
接入本身不复杂,差别通常出在细节上:验证码的生成和存储是否安全,校验逻辑是否严谨,风控是否分层,上线前测试是否充分。这几处做扎实,验证码既好用,也扛得住恶意请求。后面如果要扩展语音验证码或多通道容灾,可以在这个基础上继续做。