本文目录导读:

- 项目结构
- Maven依赖 (pom.xml)
- 配置文件 (application.yml)
- 启动类 (SmsApplication.java)
- 短信服务接口 (SmsService.java)
- 服务实现类 (SmsServiceImpl.java)
- Redis工具类 (RedisUtil.java)
- 短信发送类 (SmsSender.java)
- 控制器 (SmsController.java)
- 测试控制器 (SmsTestController.java)
- 前端示例 (index.html)
- 测试用例 (SmsServiceTest.java)
- 使用说明
- 注意事项
我来为您提供一个完整的Java短信验证码实现案例,包含后端服务和简单的使用示例。
项目结构
sms-verification-code/
├── pom.xml
├── src/main/java/com/example/sms/
│ ├── SmsApplication.java
│ ├── controller/
│ │ └── SmsController.java
│ ├── service/
│ │ ├── SmsService.java
│ │ └── impl/
│ │ └── SmsServiceImpl.java
│ ├── util/
│ │ ├── RedisUtil.java
│ │ └── SmsSender.java
│ └── config/
│ └── RedisConfig.java
└── src/main/resources/
└── application.yml
Maven依赖 (pom.xml)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>sms-verification-code</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.0</version>
<relativePath/>
</parent>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 阿里云短信 SDK -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>dysmsapi20170525</artifactId>
<version>2.0.24</version>
</dependency>
<!-- 腾讯云短信 SDK(可选) -->
<dependency>
<groupId>com.tencentcloudapi</groupId>
<artifactId>tencentcloud-sdk-java</artifactId>
<version>3.1.500</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- 工具类 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- 测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
配置文件 (application.yml)
server:
port: 8080
spring:
redis:
host: localhost
port: 6379
password:
database: 0
timeout: 3000ms
# 短信配置
sms:
# 阿里云短信配置
aliyun:
access-key-id: your-access-key-id
access-key-secret: your-access-key-secret
sign-name: 您的签名
template-code: SMS_123456789
# 通用配置
config:
# 验证码有效期(分钟)
expire-minutes: 5
# 发送间隔(秒)
send-interval: 60
# 每日发送上限
daily-limit: 10
# 验证码尝试次数上限
max-attempts: 3
启动类 (SmsApplication.java)
package com.example.sms;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication
@EnableCaching
public class SmsApplication {
public static void main(String[] args) {
SpringApplication.run(SmsApplication.class, args);
}
}
短信服务接口 (SmsService.java)
package com.example.sms.service;
import java.util.Map;
public interface SmsService {
/**
* 发送验证码
* @param phone 手机号
* @return 是否发送成功
*/
boolean sendVerificationCode(String phone);
/**
* 验证验证码
* @param phone 手机号
* @param code 验证码
* @return 验证结果
*/
boolean verifyCode(String phone, String code);
/**
* 获取剩余时间
* @param phone 手机号
* @return 剩余时间(秒)
*/
long getRemainingTime(String phone);
}
服务实现类 (SmsServiceImpl.java)
package com.example.sms.service.impl;
import com.example.sms.service.SmsService;
import com.example.sms.util.RedisUtil;
import com.example.sms.util.SmsSender;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.TimeUnit;
@Slf4j
@Service
public class SmsServiceImpl implements SmsService {
@Autowired
private RedisUtil redisUtil;
@Autowired
private SmsSender smsSender;
@Value("${sms.config.expire-minutes}")
private int expireMinutes;
@Value("${sms.config.send-interval}")
private int sendInterval;
@Value("${sms.config.max-attempts}")
private int maxAttempts;
// Redis key 前缀
private static final String CODE_KEY = "sms:code:";
private static final String SEND_TIME_KEY = "sms:sendTime:";
private static final String ATTEMPT_KEY = "sms:attempt:";
private static final String DAILY_COUNT_KEY = "sms:daily:";
@Override
public boolean sendVerificationCode(String phone) {
try {
// 1. 检查发送频率
String sendTimeKey = SEND_TIME_KEY + phone;
Object lastSendTime = redisUtil.get(sendTimeKey);
if (lastSendTime != null) {
long lastTime = Long.parseLong(lastSendTime.toString());
long currentTime = System.currentTimeMillis();
if (currentTime - lastTime < sendInterval * 1000) {
log.warn("发送频率过高,手机号:{}", phone);
return false;
}
}
// 2. 检查每日发送次数
String dailyCountKey = DAILY_COUNT_KEY + phone;
Object dailyCount = redisUtil.get(dailyCountKey);
if (dailyCount != null && Integer.parseInt(dailyCount.toString()) >= 10) {
log.warn("超出每日发送限制,手机号:{}", phone);
return false;
}
// 3. 生成6位随机验证码
String code = generateCode();
// 4. 存储验证码到Redis
String codeKey = CODE_KEY + phone;
redisUtil.set(codeKey, code, expireMinutes * 60);
// 5. 更新发送时间
redisUtil.set(sendTimeKey, System.currentTimeMillis(), 24 * 60 * 60);
// 6. 更新每日发送次数
if (dailyCount != null) {
redisUtil.increment(dailyCountKey);
} else {
redisUtil.set(dailyCountKey, 1, 24 * 60 * 60);
}
// 7. 重置验证码尝试次数
String attemptKey = ATTEMPT_KEY + phone;
redisUtil.set(attemptKey, 0, expireMinutes * 60);
// 8. 调用短信发送
log.info("发送验证码,手机号:{},验证码:{}", phone, code);
return smsSender.sendSms(phone, code);
} catch (Exception e) {
log.error("发送验证码失败", e);
return false;
}
}
@Override
public boolean verifyCode(String phone, String code) {
try {
String codeKey = CODE_KEY + phone;
Object storedCode = redisUtil.get(codeKey);
if (storedCode == null) {
log.warn("验证码不存在或已过期,手机号:{}", phone);
return false;
}
// 检查尝试次数
String attemptKey = ATTEMPT_KEY + phone;
Object attemptCount = redisUtil.get(attemptKey);
int attempts = attemptCount != null ? Integer.parseInt(attemptCount.toString()) : 0;
if (attempts >= maxAttempts) {
log.warn("验证码尝试次数超限,手机号:{}", phone);
redisUtil.delete(codeKey); // 删除验证码
return false;
}
// 验证码比对
if (storedCode.toString().equals(code)) {
// 验证成功,删除验证码
redisUtil.delete(codeKey);
redisUtil.delete(attemptKey);
log.info("验证码验证成功,手机号:{}", phone);
return true;
} else {
// 验证失败,增加尝试次数
redisUtil.increment(attemptKey);
log.warn("验证码错误,手机号:{},剩余尝试次数:{}",
phone, maxAttempts - attempts - 1);
return false;
}
} catch (Exception e) {
log.error("验证码验证失败", e);
return false;
}
}
@Override
public long getRemainingTime(String phone) {
String codeKey = CODE_KEY + phone;
return redisUtil.getExpire(codeKey);
}
/**
* 生成6位随机验证码
*/
private String generateCode() {
Random random = new Random();
int code = 100000 + random.nextInt(900000);
return String.valueOf(code);
}
}
Redis工具类 (RedisUtil.java)
package com.example.sms.util;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
@Component
public class RedisUtil {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 设置缓存
*/
public void set(String key, Object value, long timeout) {
redisTemplate.opsForValue().set(key, String.valueOf(value), timeout, TimeUnit.SECONDS);
}
/**
* 设置永久缓存
*/
public void set(String key, Object value) {
redisTemplate.opsForValue().set(key, String.valueOf(value));
}
/**
* 获取缓存
*/
public Object get(String key) {
return redisTemplate.opsForValue().get(key);
}
/**
* 删除缓存
*/
public boolean delete(String key) {
return redisTemplate.delete(key);
}
/**
* 递增操作
*/
public long increment(String key) {
return redisTemplate.opsForValue().increment(key);
}
/**
* 获取过期时间
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
*/
public boolean hasKey(String key) {
return redisTemplate.hasKey(key);
}
}
短信发送类 (SmsSender.java)
package com.example.sms.util;
import com.aliyun.dysmsapi20170525.Client;
import com.aliyun.dysmsapi20170525.models.SendSmsRequest;
import com.aliyun.dysmsapi20170525.models.SendSmsResponse;
import com.aliyun.teaopenapi.models.Config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class SmsSender {
@Value("${sms.aliyun.access-key-id}")
private String accessKeyId;
@Value("${sms.aliyun.access-key-secret}")
private String accessKeySecret;
@Value("${sms.aliyun.sign-name}")
private String signName;
@Value("${sms.aliyun.template-code}")
private String templateCode;
/**
* 发送短信
*/
public boolean sendSms(String phone, String code) {
try {
// 创建阿里云客户端
Config config = new Config()
.setAccessKeyId(accessKeyId)
.setAccessKeySecret(accessKeySecret);
config.endpoint = "dysmsapi.aliyuncs.com";
Client client = new Client(config);
// 构建请求
SendSmsRequest request = new SendSmsRequest()
.setPhoneNumbers(phone)
.setSignName(signName)
.setTemplateCode(templateCode)
.setTemplateParam("{\"code\":\"" + code + "\"}");
// 发送短信
SendSmsResponse response = client.sendSms(request);
// 判断是否发送成功
if ("OK".equals(response.body.code)) {
log.info("短信发送成功,手机号:{}", phone);
return true;
} else {
log.error("短信发送失败,错误代码:{},错误信息:{}",
response.body.code, response.body.message);
return false;
}
} catch (Exception e) {
log.error("短信发送异常", e);
return false;
}
}
/**
* 模拟发送(没有短信服务时使用)
*/
public boolean sendSmsMock(String phone, String code) {
log.info("【模拟短信】手机号:{},验证码:{}", phone, code);
return true;
}
}
控制器 (SmsController.java)
package com.example.sms.controller;
import com.example.sms.service.SmsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api/sms")
public class SmsController {
@Autowired
private SmsService smsService;
/**
* 发送验证码
*/
@PostMapping("/send")
public Map<String, Object> sendCode(@RequestParam String phone) {
Map<String, Object> result = new HashMap<>();
// 参数校验
if (!isValidPhone(phone)) {
result.put("success", false);
result.put("message", "手机号格式不正确");
return result;
}
boolean success = smsService.sendVerificationCode(phone);
result.put("success", success);
if (success) {
result.put("message", "验证码发送成功");
result.put("remainingTime", smsService.getRemainingTime(phone));
} else {
result.put("message", "验证码发送失败,请稍后重试");
}
return result;
}
/**
* 验证验证码
*/
@PostMapping("/verify")
public Map<String, Object> verifyCode(@RequestParam String phone,
@RequestParam String code) {
Map<String, Object> result = new HashMap<>();
boolean valid = smsService.verifyCode(phone, code);
result.put("success", valid);
result.put("message", valid ? "验证成功" : "验证码错误或已过期");
return result;
}
/**
* 获取剩余时间
*/
@GetMapping("/remaining-time")
public Map<String, Object> getRemainingTime(@RequestParam String phone) {
Map<String, Object> result = new HashMap<>();
long remainingTime = smsService.getRemainingTime(phone);
result.put("success", true);
result.put("remainingTime", remainingTime);
return result;
}
/**
* 校验手机号格式
*/
private boolean isValidPhone(String phone) {
return phone != null && phone.matches("^1[3-9]\\d{9}$");
}
}
测试控制器 (SmsTestController.java)
package com.example.sms.controller;
import com.example.sms.util.RedisUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/test")
public class SmsTestController {
@Autowired
private RedisUtil redisUtil;
@GetMapping("/redis")
public String testRedis() {
try {
redisUtil.set("test:key", "hello", 60);
Object value = redisUtil.get("test:key");
return "Redis测试成功,获取到的值:" + value;
} catch (Exception e) {
return "Redis测试失败:" + e.getMessage();
}
}
}
前端示例 (index.html)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">短信验证码示例</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 400px;
margin: 50px auto;
padding: 20px;
}
.form-group {
margin-bottom: 20px;
}
input {
width: 100%;
padding: 10px;
font-size: 16px;
border: 1px solid #ddd;
border-radius: 5px;
box-sizing: border-box;
}
button {
padding: 10px 20px;
font-size: 16px;
border: none;
border-radius: 5px;
cursor: pointer;
}
.btn-primary {
background-color: #007bff;
color: white;
}
.btn-success {
background-color: #28a745;
color: white;
}
#message {
margin-top: 20px;
padding: 10px;
border-radius: 5px;
display: none;
}
.success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
#countdown {
color: #6c757d;
font-size: 14px;
margin-top: 5px;
}
</style>
</head>
<body>
<h2>短信验证码示例</h2>
<div class="form-group">
<input type="text" id="phone" placeholder="请输入手机号" maxlength="11">
</div>
<div class="form-group">
<input type="text" id="code" placeholder="请输入验证码" maxlength="6">
</div>
<div class="form-group">
<button class="btn-primary" id="sendBtn" onclick="sendCode()">发送验证码</button>
<button class="btn-success" id="verifyBtn" onclick="verifyCode()">验证</button>
</div>
<div id="countdown"></div>
<div id="message"></div>
<script>
let countdown = 0;
let timerId = null;
// 发送验证码
async function sendCode() {
const phone = document.getElementById('phone').value;
if (!/^1[3-9]\d{9}$/.test(phone)) {
showMessage('请输入正确的手机号', false);
return;
}
try {
const response = await fetch(`/api/sms/send?phone=${phone}`, {
method: 'POST'
});
const data = await response.json();
if (data.success) {
showMessage('验证码发送成功', true);
startCountdown(60);
document.getElementById('sendBtn').disabled = true;
} else {
showMessage(data.message, false);
}
} catch (error) {
showMessage('请求失败,请稍后重试', false);
}
}
// 验证验证码
async function verifyCode() {
const phone = document.getElementById('phone').value;
const code = document.getElementById('code').value;
if (!phone || !code) {
showMessage('请填写手机号和验证码', false);
return;
}
try {
const response = await fetch(`/api/sms/verify?phone=${phone}&code=${code}`, {
method: 'POST'
});
const data = await response.json();
showMessage(data.message, data.success);
} catch (error) {
showMessage('请求失败,请稍后重试', false);
}
}
// 倒计时
function startCountdown(seconds) {
countdown = seconds;
timerId = setInterval(() => {
countdown--;
document.getElementById('countdown').textContent =
`${countdown}秒后可重新发送`;
if (countdown <= 0) {
clearInterval(timerId);
document.getElementById('sendBtn').disabled = false;
document.getElementById('countdown').textContent = '';
}
}, 1000);
}
// 显示消息
function showMessage(message, isSuccess) {
const msgDiv = document.getElementById('message');
msgDiv.textContent = message;
msgDiv.style.display = 'block';
msgDiv.className = isSuccess ? 'success' : 'error';
setTimeout(() => {
msgDiv.style.display = 'none';
}, 3000);
}
</script>
</body>
</html>
测试用例 (SmsServiceTest.java)
package com.example.sms;
import com.example.sms.service.SmsService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
public class SmsServiceTest {
@Autowired
private SmsService smsService;
@Test
public void testSendCode() {
String phone = "13800138000";
boolean success = smsService.sendVerificationCode(phone);
assertTrue(success);
}
@Test
public void testVerifyCode() {
String phone = "13800138000";
// 先发送验证码
smsService.sendVerificationCode(phone);
// 获取验证码(测试用)
String code = "123456";
boolean valid = smsService.verifyCode(phone, code);
assertTrue(valid);
}
@Test
public void testVerifyWrongCode() {
String phone = "13800138000";
// 先发送验证码
smsService.sendVerificationCode(phone);
// 错误的验证码
String code = "000000";
boolean valid = smsService.verifyCode(phone, code);
assertFalse(valid);
}
}
使用说明
-
配置短信服务:根据您使用的短信服务商(阿里云、腾讯云等),配置相应的API密钥和模板
-
启动Redis:确保Redis已启动,并修改
application.yml中的Redis配置 -
运行项目:启动
SmsApplication主类 -
测试接口:
- 发送验证码:
POST /api/sms/send?phone=13800138000 - 验证验证码:
POST /api/sms/verify?phone=13800138000&code=123456
- 发送验证码:
注意事项
- 生产环境:生产环境中应使用真实的短信服务商,并做好安全防护
- 安全考虑:
- 添加图形验证码防止机器人
- 使用HTTPS加密传输
- 对手机号做频率限制
- 敏感操作需要二次验证
- 优化建议:
- 使用消息队列异步发送短信
- 添加短信发送日志记录
- 支持多短信服务商切换
- 添加监控告警功能
这个案例提供了一个完整的短信验证码实现,您可以根据实际需求进行调整和扩展。