引言
你好呀,我是小邹。
在现代Web应用中,短信验证码已成为用户身份验证和安全操作的重要保障手段。本文将详细介绍如何在Spring Boot项目中集成阿里云短信服务,配合Redis实现高效、安全的短信验证码系统。
一、环境准备
1.1 依赖配置
在pom.xml中添加以下依赖:
<!-- 阿里云短信服务SDK -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>dysmsapi20170525</artifactId>
<version>3.0.0</version>
</dependency>
<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>
<!-- 工具类 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
1.2 配置文件
application.yml中的关键配置:
# 阿里云短信配置
aliyun:
sms:
access-key-id: xxxxx
access-key-secret: xxxxxxxxxx
template-code: SMS_329365006 # 审核通过的模板CODE
endpoint: dysmsapi.aliyuncs.com
region-id: cn-hangzhou
# Redis配置
spring:
redis:
host: 192.168.110.88
port: 6379
password: 123456
database: 0
timeout: 3000ms
lettuce:
pool:
max-active: 8
max-wait: -1ms
max-idle: 8
min-idle: 0
# 验证码配置
verification:
code:
expire-time: 300 # 验证码有效期(秒)
length: 6 # 验证码长度
resend-interval: 60 # 重发间隔(秒)
二、核心配置类实现
2.1 Redis配置类
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(
RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
// 使用String序列化key
StringRedisSerializer stringSerializer = new StringRedisSerializer();
template.setKeySerializer(stringSerializer);
template.setHashKeySerializer(stringSerializer);
// 使用Jackson序列化value
GenericJackson2JsonRedisSerializer jsonSerializer =
new GenericJackson2JsonRedisSerializer();
template.setValueSerializer(jsonSerializer);
template.setHashValueSerializer(jsonSerializer);
template.afterPropertiesSet();
return template;
}
}
2.2 阿里云短信客户端配置
@Configuration
public class SmsConfig {
@Value("${aliyun.sms.access-key-id}")
private String accessKeyId;
@Value("${aliyun.sms.access-key-secret}")
private String accessKeySecret;
@Value("${aliyun.sms.endpoint}")
private String endpoint;
@Bean
public Client smsClient() throws Exception {
Config config = new Config()
.setAccessKeyId(accessKeyId)
.setAccessKeySecret(accessKeySecret);
config.endpoint = endpoint;
return new Client(config);
}
}
三、业务逻辑实现
3.1 服务接口定义
public interface VerificationCodeService {
/**
* 发送验证码
* @param phone 手机号
* @param businessType 业务类型
* @return 是否发送成功
*/
boolean sendVerificationCode(String phone, String businessType);
/**
* 验证验证码
* @param phone 手机号
* @param businessType 业务类型
* @param inputCode 用户输入的验证码
* @return 是否验证成功
*/
boolean verifyCode(String phone, String businessType, String inputCode);
}
3.2 服务实现类
@Service
@Slf4j
public class VerificationCodeServiceImpl implements VerificationCodeService {
@Autowired
private Client smsClient;
@Autowired
private RedisTemplate<String, String> redisTemplate;
private final String SIGN_NAME = "阿里巴巴科技"; // 审核通过的签名
@Value("${aliyun.sms.template-code}")
private String templateCode;
@Value("${verification.code.expire-time}")
private Integer expireTime;
@Value("${verification.code.length}")
private Integer codeLength;
@Value("${verification.code.resend-interval}")
private Integer resendInterval;
private static final String REDIS_KEY_PREFIX = "VERIFY_CODE:";
private static final String REDIS_SEND_LIMIT_PREFIX = "SEND_LIMIT:";
/**
* 发送验证码主逻辑
*/
@Override
public boolean sendVerificationCode(String phone, String businessType) {
// 1. 参数校验
if (!isValidPhone(phone)) {
log.warn("手机号格式不正确: {}", phone);
throw new IllegalArgumentException("手机号格式不正确");
}
// 2. 发送频率控制
String limitKey = REDIS_SEND_LIMIT_PREFIX + businessType + ":" + phone;
String lastSendTime = redisTemplate.opsForValue().get(limitKey);
if (StringUtils.hasText(lastSendTime)) {
long lastTime = Long.parseLong(lastSendTime);
long currentTime = System.currentTimeMillis();
if (currentTime - lastTime < resendInterval * 1000) {
throw new IllegalArgumentException(String.format(
"操作过于频繁,请%d秒后再试", resendInterval));
}
}
// 3. 生成验证码
String code = generateVerificationCode();
// 4. 存储到Redis(设置过期时间)
String redisKey = REDIS_KEY_PREFIX + businessType + ":" + phone;
redisTemplate.opsForValue().set(redisKey, code, expireTime, TimeUnit.SECONDS);
// 5. 记录发送时间(控制发送频率)
redisTemplate.opsForValue().set(limitKey,
String.valueOf(System.currentTimeMillis()),
resendInterval, TimeUnit.SECONDS);
// 6. 发送短信
return sendSms(phone, code, businessType);
}
/**
* 验证验证码
*/
@Override
public boolean verifyCode(String phone, String businessType, String inputCode) {
// 1. 参数校验
if (!StringUtils.hasText(inputCode)) {
throw new IllegalArgumentException("验证码不能为空");
}
// 2. 从Redis获取验证码
String redisKey = REDIS_KEY_PREFIX + businessType + ":" + phone;
String storedCode = redisTemplate.opsForValue().get(redisKey);
// 3. 验证码检查
if (storedCode == null) {
throw new IllegalArgumentException("验证码已过期,请重新获取");
}
if (!storedCode.equals(inputCode)) {
// 记录错误次数(防止暴力破解)
String errorKey = redisKey + ":ERROR";
Long errorCount = redisTemplate.opsForValue().increment(errorKey);
if (errorCount == 1) {
redisTemplate.expire(errorKey, 300, TimeUnit.SECONDS);
}
if (errorCount >= 5) {
redisTemplate.delete(redisKey);
throw new IllegalArgumentException("验证错误次数过多,请重新获取验证码");
}
throw new IllegalArgumentException("验证码错误");
}
// 4. 验证成功,删除验证码(防止重复使用)
redisTemplate.delete(redisKey);
return true;
}
/**
* 发送短信方法
*/
private boolean sendSms(String phone, String code, String businessType) {
try {
// 模板参数(必须与阿里云模板中的变量名一致)
String templateParam = String.format("{\"code\":\"%s\"}", code);
SendSmsRequest request = new SendSmsRequest()
.setPhoneNumbers(phone)
.setSignName(SIGN_NAME)
.setTemplateCode(templateCode)
.setTemplateParam(templateParam);
RuntimeOptions runtime = new RuntimeOptions();
SendSmsResponse response = smsClient.sendSmsWithOptions(request, runtime);
if ("OK".equals(response.getBody().getCode())) {
log.info("短信发送成功,手机号:{},业务类型:{}", phone, businessType);
return true;
} else {
log.error("短信发送失败,手机号:{},返回:{}",
phone, response.getBody().getMessage());
return false;
}
} catch (Exception e) {
log.error("发送短信异常,手机号:{}", phone, e);
return false;
}
}
/**
* 生成随机验证码
*/
private String generateVerificationCode() {
return RandomStringUtils.randomNumeric(codeLength);
}
/**
* 验证手机号格式
*/
private boolean isValidPhone(String phone) {
return phone != null && phone.matches("^1[3-9]\\d{9}$");
}
}
3.3 控制器实现
@RestController
@RequestMapping("/api/verify")
@Slf4j
public class VerificationCodeController {
@Autowired
private VerificationCodeService verificationCodeService;
/**
* 发送验证码接口
*/
@PostMapping("/send")
public ResponseEntity<Result<?>> sendVerificationCode(
@RequestParam String phone,
@RequestParam String businessType) {
try {
// 业务类型验证
if (!isValidBusinessType(businessType)) {
return ResponseEntity.badRequest()
.body(Result.fail(400, "无效的业务类型"));
}
// 手机号格式验证
if (!phone.matches("^1[3-9]\\d{9}$")) {
return ResponseEntity.badRequest()
.body(Result.fail(400, "手机号格式不正确"));
}
// 发送验证码
boolean success = verificationCodeService.sendVerificationCode(phone, businessType);
if (success) {
return ResponseEntity.ok(Result.success("验证码发送成功"));
} else {
return ResponseEntity.badRequest()
.body(Result.fail(400, "验证码发送失败,请稍后重试"));
}
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().body(Result.fail(400, e.getMessage()));
} catch (Exception e) {
log.error("发送验证码异常: ", e);
return ResponseEntity.internalServerError()
.body(Result.fail(500, "系统错误"));
}
}
/**
* 验证验证码接口
*/
@PostMapping("/verify")
public ResponseEntity<Result<?>> verifyCode(
@RequestParam String phone,
@RequestParam String businessType,
@RequestParam String code) {
try {
boolean success = verificationCodeService.verifyCode(phone, businessType, code);
if (success) {
return ResponseEntity.ok(Result.success("验证成功"));
} else {
return ResponseEntity.badRequest()
.body(Result.fail(400, "验证失败"));
}
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().body(Result.fail(400, e.getMessage()));
} catch (Exception e) {
log.error("验证验证码异常: ", e);
return ResponseEntity.internalServerError()
.body(Result.fail(500, "系统错误"));
}
}
/**
* 验证业务类型是否有效
*/
private boolean isValidBusinessType(String businessType) {
return Arrays.asList("RESET_PASSWORD", "UPDATE_PHONE_OLD",
"UPDATE_PHONE_NEW", "LOGIN", "REGISTER").contains(businessType);
}
}
四、系统设计要点
4.1 安全机制设计
频率控制:防止恶意频繁发送
- 通过Redis记录上次发送时间
- 强制等待间隔(默认60秒)
有效期控制:
- 验证码5分钟有效
- 过期自动清理
防暴力破解:
- 错误次数限制(5次)
- 错误次数过多时强制失效验证码
防重复使用:
- 验证成功后立即删除Redis中的验证码
4.2 Redis键设计
// 验证码存储键
VERIFY_CODE:{businessType}:{phone}
// 发送频率控制键
SEND_LIMIT:{businessType}:{phone}
// 错误次数记录键
VERIFY_CODE:{businessType}:{phone}:ERROR
4.3 业务类型枚举
建议使用枚举类规范业务类型:
public enum BusinessType {
RESET_PASSWORD("重置密码"),
UPDATE_PHONE_OLD("更换手机号-旧手机"),
UPDATE_PHONE_NEW("更换手机号-新手机"),
LOGIN("登录验证"),
REGISTER("用户注册");
private final String description;
BusinessType(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}
五、测试方法
5.1 单元测试
@SpringBootTest
@Slf4j
class VerificationCodeServiceTest {
@Autowired
private VerificationCodeService verificationCodeService;
@Test
void testSendVerificationCode() {
String phone = "13800138000";
String businessType = "LOGIN";
try {
boolean result = verificationCodeService.sendVerificationCode(phone, businessType);
assertTrue(result);
log.info("验证码发送成功");
} catch (Exception e) {
log.error("发送失败: {}", e.getMessage());
}
}
@Test
void testVerifyCode() {
String phone = "13800138000";
String businessType = "LOGIN";
String code = "123456"; // 实际应从Redis获取或用户输入
try {
boolean result = verificationCodeService.verifyCode(phone, businessType, code);
log.info("验证结果: {}", result ? "成功" : "失败");
} catch (Exception e) {
log.error("验证异常: {}", e.getMessage());
}
}
}
5.2 API接口测试
5.2.1 使用curl测试
# 发送验证码
curl -X POST "http://localhost:8080/api/verify/send?phone=13800138000&businessType=LOGIN"
# 验证验证码
curl -X POST "http://localhost:8080/api/verify/verify?phone=13800138000&businessType=LOGIN&code=123456"
5.2.2 使用Postman测试
发送验证码请求:
- Method: POST
- URL:
http://localhost:8080/api/verify/send - Params:
- phone: 13800138000
- businessType: LOGIN
验证验证码请求:
- Method: POST
- URL:
http://localhost:8080/api/verify/verify - Params:
- phone: 13800138000
- businessType: LOGIN
- code: [收到的验证码]
5.3 Redis数据验证
# 连接Redis
redis-cli -h 192.168.110.80 -p 6379 -a 123456
# 查看验证码
keys VERIFY_CODE:*
# 查看具体验证码值
get "VERIFY_CODE:LOGIN:13800138000"
# 查看发送频率限制
keys SEND_LIMIT:*
# 清理测试数据(谨慎操作)
del "VERIFY_CODE:LOGIN:13800138000"
del "SEND_LIMIT:LOGIN:13800138000"
5.4 集成测试用例
@Test
void testCompleteVerificationFlow() {
String phone = "13800138000";
String businessType = "LOGIN";
// 1. 发送验证码
boolean sendResult = verificationCodeService.sendVerificationCode(phone, businessType);
assertTrue(sendResult);
// 2. 从Redis获取验证码(模拟实际场景)
String redisKey = "VERIFY_CODE:" + businessType + ":" + phone;
String actualCode = redisTemplate.opsForValue().get(redisKey);
assertNotNull(actualCode);
// 3. 验证正确验证码
boolean verifySuccess = verificationCodeService.verifyCode(phone, businessType, actualCode);
assertTrue(verifySuccess);
// 4. 验证码已被删除
String deletedCode = redisTemplate.opsForValue().get(redisKey);
assertNull(deletedCode);
// 5. 验证错误验证码
assertThrows(IllegalArgumentException.class, () -> {
verificationCodeService.verifyCode(phone, businessType, "999999");
});
}
六、常见问题与解决方案
6.1 阿里云短信发送失败
| 错误码 | 原因 | 解决方案 |
|---|---|---|
| isv.SMS_SIGNATURE_ILLEGAL | 签名未审核或不存在 | 1. 确保签名已审核通过 2. 签名名称与审核的一致 |
| isv.INVALID_PARAMETERS | 模板参数错误 | 1. 检查模板参数JSON格式 2. 确保变量名与模板一致 |
| isv.BUSINESS_LIMIT_CONTROL | 触发流控 | 1. 检查发送频率 2. 联系阿里云调整限制 |
| MissingSecurityToken | 密钥错误 | 1. 检查AccessKeyId和Secret 2. 确保有短信服务权限 |
6.2 Redis连接问题
// 检查Redis配置
@Configuration
public class RedisHealthCheck {
@Autowired
private RedisTemplate<String, String> redisTemplate;
public boolean isRedisAvailable() {
try {
redisTemplate.opsForValue().get("test");
return true;
} catch (Exception e) {
log.error("Redis连接失败: {}", e.getMessage());
return false;
}
}
}
6.3 性能优化建议
连接池优化:
spring: redis: lettuce: pool: max-active: 20 # 根据并发量调整 max-idle: 10 min-idle: 5批量操作:对于大量验证需求,考虑批量验证
异步发送:短信发送可以异步处理
@Async public CompletableFuture<Boolean> sendVerificationCodeAsync(String phone, String businessType) { // 异步发送逻辑 }
七、监控与日志
7.1 关键日志记录
// 在关键位置添加详细日志
log.info("验证码发送,手机号:{},业务类型:{},IP:{}",
phone, businessType, getClientIp());
log.warn("验证码发送频繁,手机号:{},间隔:{}秒",
phone, resendInterval);
log.error("短信发送失败,手机号:{},错误:{}",
phone, errorMessage);
7.2 监控指标
建议监控以下指标:
- 短信发送成功率
- 验证码验证成功率
- Redis连接状态
- 发送频率异常情况
八、部署注意事项
环境配置:
- 生产环境和开发环境使用不同的阿里云密钥
- Redis生产环境建议使用集群模式
安全加固:
- 密钥通过环境变量或配置中心管理
- 启用Redis SSL连接
- 限制API访问IP
备份策略:
- 定期备份验证码发送记录
- 监控短信费用使用情况
总结
本文详细介绍了基于阿里云短信服务和Redis的验证码系统的完整实现方案。该系统具有以下特点:
- 安全性高:多重防护机制,有效防止恶意攻击
- 可靠性强:基于Redis的持久化存储,确保数据不丢失
- 扩展性好:模块化设计,便于功能扩展
- 易于维护:完善的日志和监控体系
通过本文的实现,您可以快速构建一个生产级别的短信验证码系统,满足各种业务场景的需求。

评论区 0