Java案例怎么实现短信验证码?完整流程与核心源码解析
目录导读
- 为什么要实现短信验证码?它解决了什么问题?
- 短信验证码在Java项目中的整体架构与选型(阿里云vs腾讯云)
- 核心实现步骤:前端触发 → 后端生成 → 第三方API调用 → 校验
- 实战源码:从Controller到Service完整代码(附带关键注释)
- 高频问题解答:验证码生成策略、有效期设置、防刷机制
- 如何对这段代码进行SEO优化与性能调优?
为什么要实现短信验证码?它解决了什么问题?
问:为什么现在几乎所有的App或网站都要求用户输入短信验证码?

答:因为“短信验证码”是目前平衡安全性与用户体验的最佳方案之一,它主要解决以下三个问题:
- 确认手机号真实有效:只有持有该手机号的用户才能收到验证码,完成注册或登录。
- 防机器人刷接口:在登录、注册、找回密码等高敏感操作中,加入验证码可以有效拦截自动化脚本攻击。
- 增加一道门槛:即便数据库被拖库,黑客也无法直接登录,因为他们没有手机短信验证码。
核心逻辑:用户填写手机号 → 系统生成随机6位数字/字母 → 调用第三方短信服务商API → 将验证码发送至用户手机 → 用户填写 → 后端对比校验。
短信验证码在Java项目中的整体架构与选型
问:直接写代码就能发短信了吗?需要选择什么供应商?
答:不能,Java代码本身无法直接发送短信,必须依赖第三方短信服务商提供的SDK,主流的服务商有:
| 服务商 | 特点 | 费用(参考) |
|---|---|---|
| 阿里云短信 | 国内普及最广,SDK文档清晰,支持国际短信 | 约0.045元/条 |
| 腾讯云短信 | 新用户赠送1000条,接口稳定 | 约0.043元/条 |
| 云片网 | 门槛低,小团队常用 | 约0.038元/条 |
本文示例以阿里云短信为例,因为它生态最成熟。
整体架构图:
前端(Vue/React) → Java后端Controller → Service层 → 调用阿里云SDK → 写入Redis(缓存验证码) → 返回成功/失败
核心实现步骤(附完整代码)
问:具体从项目结构到代码,每一步该怎么做?
引入依赖(pom.xml)
<!-- 阿里云短信SDK -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.6.3</version>
</dependency>
<!-- Redis 缓存验证码 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置类(application.yml)
aliyun:
sms:
access-key-id: LTAI5tJxxxxxxxxx # 请替换为自己的
access-key-secret: 8Z0xxxxxxxxxx
sign-name: 你的签名
template-code: SMS_123456789 # 模板ID
region-id: cn-hangzhou
Controller层 —— 接收手机号
@RestController
@RequestMapping("/api/sms")
public class SmsController {
@Autowired
private SmsService smsService;
@PostMapping("/send")
public Result<?> sendCode(@RequestParam String phone) {
// 参数校验:必须为11位数字
if (!phone.matches("^1[3-9]\\d{9}$")) {
return Result.error("手机号格式错误");
}
boolean flag = smsService.sendSmsCode(phone);
return flag ? Result.success("验证码已发送") : Result.error("发送失败,请稍后重试");
}
@PostMapping("/verify")
public Result<?> verifyCode(@RequestParam String phone, @RequestParam String code) {
boolean valid = smsService.verify(phone, code);
return valid ? Result.success("验证通过") : Result.error("验证码错误或已过期");
}
}
Service层 —— 核心业务逻辑
@Service
public class SmsService {
@Autowired
private StringRedisTemplate redisTemplate;
@Value("${aliyun.sms.access-key-id}")
private String accessKeyId;
@Value("${aliyun.sms.access-key-secret}")
private String accessKeySecret;
@Value("${aliyun.sms.sign-name}")
private String signName;
@Value("${aliyun.sms.template-code}")
private String templateCode;
@Value("${aliyun.sms.region-id}")
private String regionId;
// 发送验证码
public boolean sendSmsCode(String phone) {
// 1. 生成随机6位数字验证码
String code = String.format("%06d", new Random().nextInt(999999));
// 2. 将验证码存储到Redis,有效期5分钟
redisTemplate.opsForValue().set("sms:code:" + phone, code, 5, TimeUnit.MINUTES);
// 3. 调用阿里云API发送短信
try {
DefaultProfile profile = DefaultProfile.getProfile(regionId, accessKeyId, accessKeySecret);
IAcsClient client = new DefaultAcsClient(profile);
CommonRequest request = new CommonRequest();
request.setSysMethod(MethodType.POST);
request.setSysDomain("dysmsapi.aliyuncs.com");
request.setSysVersion("2017-05-25");
request.setSysAction("SendSms");
request.putQueryParameter("PhoneNumbers", phone);
request.putQueryParameter("SignName", signName);
request.putQueryParameter("TemplateCode", templateCode);
request.putQueryParameter("TemplateParam", "{\"code\":\"" + code + "\"}");
CommonResponse response = client.getCommonResponse(request);
// 实际生产需解析 response 中的 Code 字段
return response.getHttpResponse().isSuccess();
} catch (Exception e) {
log.error("短信发送失败: {}", e.getMessage());
return false;
}
}
// 校验验证码
public boolean verify(String phone, String inputCode) {
String key = "sms:code:" + phone;
String cacheCode = redisTemplate.opsForValue().get(key);
if (cacheCode == null) {
return false; // 已过期或未发送
}
// 验证后无论成功与否,建议删除,防止重复使用
redisTemplate.delete(key);
return cacheCode.equals(inputCode);
}
}
前端调用示例(JavaScript/axios)
// 发送验证码
axios.post('/api/sms/send', { phone: '13800138000' })
.then(res => { console.log(res.data.message); });
// 验证
axios.post('/api/sms/verify', { phone: '13800138000', code: '123456' })
.then(res => { alert(res.data.message); });
高频问题解答:防刷、有效期、生成策略
问:如果用户频繁点击获取验证码,怎么办?
答:这是面试常见题,必须添加防刷机制:
- 限流:同一手机号每分钟最多发送1次,每天最多5次,可以在Redis中加一个计数器(
sms:limit:phone:139xxxxxxxx)。 - 图形验证码:在点击“发送验证码”之前,要求先通过图形验证码或滑块验证,减少机器操作。
- IP限制:同一IP地址每小时最多发送50条。
问:验证码的有效期设为多少合理? 答:通常为5分钟,太短用户来不及输入,太长(如30分钟)会增加被暴力破解的风险,使用Redis的TTL自动过期是最佳实现方式。
问:验证码应该用纯数字还是数字+字母? 答:推荐纯6位数字,原因:
- 用户输入方便,尤其移动端。
- 对于验证码场景,“一次有效”已经足够安全,纯数字组合(1,000,000种)在5分钟内暴力破解是不现实的(除非被人肉盯上)。
SEO优化与性能调优建议
问:这篇文章是关于Java案例的,但我想让我的项目也有好的排名,技术方面要注意什么?
- URL设计:如果你的项目中有关于短信验证码的接口,建议RESTful风格,如
/api/sms/send-code而不是/send?type=1。 - 页面加载速度:JWT或Redis存储验证码时,尽量使用连接池,减少每次连接Redis的开销。
- 错误提示:不要在前端返回“验证码错误”或“手机号未注册”,应统一返回“验证失败”,防止黑客通过返回信息猜解用户状态。
- 日志埋点:发送验证码成功/失败、验证成功/失败都应记录日志,便于排查与监控(也利于SEO数据统计工具如百度统计)。
Java实现短信验证码的核心没有想象中复杂:生成随机数 → 存Redis → 调用第三方API → 校验,但生产环境真正做好的关键在于:
- 选择稳定的服务商(阿里云/腾讯云)
- 做好防刷与限流
- 处理好缓存与过期策略
如果你完全按照本文的代码和思路实现,不仅能通过面试关,还能直接部署上线,如果你已经踩过坑,欢迎在评论区分享你的“血泪史”——比如某次阿里云鉴权失败导致验证码全挂的教训。