19 KiB
19 KiB
阿里云SMS短信服务集成文档
目录
概述
本文档提供阿里云短信服务(SMS)的完整集成方案,包括验证码发送、验证码校验、发送频率限制等功能。
功能特性
- ✅ 发送6位数字验证码
- ✅ 验证码5分钟有效期
- ✅ 每个手机号每天最多发送10次
- ✅ 使用Redis存储验证码和发送次数
- ✅ 完整的错误处理和日志记录
Maven依赖
pom.xml
<dependencies>
<!-- 阿里云短信SDK -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>dysmsapi20170525</artifactId>
<version>2.0.24</version>
</dependency>
<!-- 阿里云核心SDK -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>tea-openapi</artifactId>
<version>0.2.8</version>
</dependency>
<!-- 阿里云Tea工具 -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>tea-util</artifactId>
<version>0.2.21</version>
</dependency>
<!-- Spring Boot Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
配置文件
application.yml
spring:
data:
redis:
host: localhost
port: 6379
database: 0
password: # 如果有密码则填写
# 阿里云短信服务配置
aliyun:
sms:
# 短信签名(需要在阿里云控制台申请)
sign-name: 你的短信签名
# 短信模板CODE(需要在阿里云控制台申请)
template-code: SMS_xxxxxxxx
# 阿里云AccessKey ID
access-key-id: ${ALIYUN_ACCESS_KEY_ID:your-access-key-id}
# 阿里云AccessKey Secret
access-key-secret: ${ALIYUN_ACCESS_KEY_SECRET:your-access-key-secret}
环境变量配置(推荐)
Linux/Mac:
export ALIYUN_ACCESS_KEY_ID=your-access-key-id
export ALIYUN_ACCESS_KEY_SECRET=your-access-key-secret
Windows:
set ALIYUN_ACCESS_KEY_ID=your-access-key-id
set ALIYUN_ACCESS_KEY_SECRET=your-access-key-secret
代码实现
1. Service接口
SmsService.java
package com.example.service;
/**
* 短信服务接口
*/
public interface SmsService {
/**
* 发送短信验证码
*
* @param phoneNumber 手机号码
* @return 是否发送成功
* @throws Exception 发送异常
*/
boolean sendVerificationCode(String phoneNumber) throws Exception;
/**
* 验证短信验证码
*
* @param phoneNumber 手机号码
* @param code 验证码
* @return 是否验证通过
*/
boolean verifyCode(String phoneNumber, String code);
}
2. Service实现类
SmsServiceImpl.java
package com.example.service.impl;
import com.aliyun.dysmsapi20170525.Client;
import com.aliyun.dysmsapi20170525.models.SendSmsRequest;
import com.aliyun.tea.TeaException;
import com.aliyun.teautil.models.RuntimeOptions;
import com.example.service.SmsService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Random;
import java.util.concurrent.TimeUnit;
/**
* 阿里云短信服务实现类
*/
@Service
public class SmsServiceImpl implements SmsService {
private static final Logger logger = LoggerFactory.getLogger(SmsServiceImpl.class);
@Value("${aliyun.sms.sign-name}")
private String signName;
@Value("${aliyun.sms.template-code}")
private String templateCode;
@Value("${aliyun.sms.access-key-id}")
private String accessKeyId;
@Value("${aliyun.sms.access-key-secret}")
private String accessKeySecret;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 短信验证码Redis前缀
private static final String SMS_CODE_PREFIX = "sms:code:";
// 短信发送次数Redis前缀
private static final String SMS_COUNT_PREFIX = "sms:count:";
// 短信验证码有效期(分钟)
private static final long SMS_CODE_EXPIRE = 5;
// 每天最大发送次数
private static final int MAX_SMS_COUNT_PER_DAY = 10;
/**
* 创建阿里云短信客户端
*/
private Client createSmsClient() throws Exception {
com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config();
config.accessKeyId = accessKeyId;
config.accessKeySecret = accessKeySecret;
config.endpoint = "dysmsapi.aliyuncs.com";
return new Client(config);
}
/**
* 生成6位随机数字验证码
*/
private String generateVerificationCode() {
Random random = new Random();
StringBuilder code = new StringBuilder();
for (int i = 0; i < 6; i++) {
code.append(random.nextInt(10));
}
return code.toString();
}
/**
* 获取当天结束时间的剩余秒数
*/
private long getSecondsUntilEndOfDay() {
LocalDateTime now = LocalDateTime.now();
LocalDateTime endOfDay = now.toLocalDate().atTime(23, 59, 59);
Duration duration = Duration.between(now, endOfDay);
return duration.getSeconds();
}
@Override
public boolean sendVerificationCode(String phoneNumber) throws Exception {
// 1. 检查当天发送次数是否达到上限
String countKey = SMS_COUNT_PREFIX + phoneNumber + ":" +
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
Integer count = (Integer) redisTemplate.opsForValue().get(countKey);
if (count != null && count >= MAX_SMS_COUNT_PER_DAY) {
logger.warn("手机号{}今日短信发送次数已达上限: {}", phoneNumber, MAX_SMS_COUNT_PER_DAY);
return false;
}
// 2. 生成6位随机验证码
String verificationCode = generateVerificationCode();
// 3. 构建短信请求
Client client = createSmsClient();
SendSmsRequest sendSmsRequest = new SendSmsRequest()
.setSignName(signName)
.setTemplateCode(templateCode)
.setPhoneNumbers(phoneNumber)
.setTemplateParam("{\"code\":\"" + verificationCode + "\"}");
RuntimeOptions runtime = new RuntimeOptions();
try {
// 4. 发送短信
client.sendSmsWithOptions(sendSmsRequest, runtime);
logger.info("短信验证码发送成功,手机号: {}", phoneNumber);
// 5. 将验证码保存到Redis,设置过期时间
String codeKey = SMS_CODE_PREFIX + phoneNumber;
redisTemplate.opsForValue().set(codeKey, verificationCode, SMS_CODE_EXPIRE, TimeUnit.MINUTES);
// 6. 增加当天发送次数,并设置过期时间为当天结束
if (count == null) {
count = 0;
}
redisTemplate.opsForValue().set(countKey, count + 1, getSecondsUntilEndOfDay(), TimeUnit.SECONDS);
return true;
} catch (TeaException error) {
logger.error("短信发送失败, 手机号: {}, 错误信息: {}, 诊断信息: {}",
phoneNumber, error.getMessage(), error.getData().get("Recommend"));
return false;
} catch (Exception error) {
logger.error("短信发送异常, 手机号: {}", phoneNumber, error);
return false;
}
}
@Override
public boolean verifyCode(String phoneNumber, String code) {
if (phoneNumber == null || code == null) {
return false;
}
String codeKey = SMS_CODE_PREFIX + phoneNumber;
String savedCode = (String) redisTemplate.opsForValue().get(codeKey);
if (savedCode != null && savedCode.equals(code)) {
// 验证成功后删除验证码
redisTemplate.delete(codeKey);
return true;
}
return false;
}
}
3. Controller控制器
SmsController.java
package com.example.controller;
import com.example.common.ApiResponse;
import com.example.common.ResultUtils;
import com.example.service.SmsService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* 短信控制器
*/
@RestController
@RequestMapping("/sms")
@Tag(name = "短信接口", description = "提供短信验证码相关功能")
public class SmsController {
@Autowired
private SmsService smsService;
/**
* 发送短信验证码
*
* @param phoneNumber 手机号
* @return 发送结果
*/
@PostMapping("/sendCode")
@Operation(summary = "发送短信验证码", description = "向指定手机号发送验证码,每个手机号每天最多发送10次")
public ApiResponse<Boolean> sendVerificationCode(
@Parameter(description = "手机号码", required = true)
@RequestParam String phoneNumber) {
try {
boolean success = smsService.sendVerificationCode(phoneNumber);
if (success) {
return ResultUtils.success(true, "验证码发送成功");
} else {
return ResultUtils.error(40001, "发送验证码失败,请稍后重试");
}
} catch (Exception e) {
return ResultUtils.error(50000, "发送验证码异常:" + e.getMessage());
}
}
/**
* 验证短信验证码
*
* @param phoneNumber 手机号
* @param code 验证码
* @return 验证结果
*/
@PostMapping("/verifyCode")
@Operation(summary = "验证短信验证码", description = "验证手机号和验证码是否匹配")
public ApiResponse<Boolean> verifyCode(
@Parameter(description = "手机号码", required = true)
@RequestParam String phoneNumber,
@Parameter(description = "验证码", required = true)
@RequestParam String code) {
boolean valid = smsService.verifyCode(phoneNumber, code);
if (valid) {
return ResultUtils.success(true, "验证成功");
} else {
return ResultUtils.error(40002, "验证码错误或已过期");
}
}
}
4. 通用响应类(可选)
ApiResponse.java
package com.example.common;
import lombok.Data;
/**
* 通用API响应类
*/
@Data
public class ApiResponse<T> {
private int code;
private String message;
private T data;
public ApiResponse(int code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
}
}
ResultUtils.java
package com.example.common;
/**
* 响应工具类
*/
public class ResultUtils {
public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(200, "success", data);
}
public static <T> ApiResponse<T> success(T data, String message) {
return new ApiResponse<>(200, message, data);
}
public static <T> ApiResponse<T> error(int code, String message) {
return new ApiResponse<>(code, message, null);
}
}
使用示例
1. 发送验证码
请求:
curl -X POST http://localhost:8080/sms/sendCode \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "phoneNumber=13800138000"
响应:
{
"code": 200,
"message": "验证码发送成功",
"data": true
}
2. 验证验证码
请求:
curl -X POST http://localhost:8080/sms/verifyCode \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "phoneNumber=13800138000&code=123456"
响应:
{
"code": 200,
"message": "验证成功",
"data": true
}
3. 在业务代码中使用
@Service
public class UserService {
@Autowired
private SmsService smsService;
/**
* 用户注册
*/
public void register(String phoneNumber, String code, String password) {
// 1. 验证验证码
if (!smsService.verifyCode(phoneNumber, code)) {
throw new BusinessException("验证码错误或已过期");
}
// 2. 执行注册逻辑
// ...
}
/**
* 找回密码
*/
public void resetPassword(String phoneNumber, String code, String newPassword) {
// 1. 验证验证码
if (!smsService.verifyCode(phoneNumber, code)) {
throw new BusinessException("验证码错误或已过期");
}
// 2. 重置密码
// ...
}
}
常见问题
1. 如何申请阿里云短信服务?
- 登录阿里云控制台
- 进入"短信服务"产品页面
- 申请短信签名(需要企业资质或个人认证)
- 申请短信模板(需要审核,一般1-2个工作日)
- 获取AccessKey ID和AccessKey Secret
2. 短信模板格式
模板示例:
您的验证码是${code},请在5分钟内完成验证。
模板变量:
- 使用
${变量名}格式 - 代码中传递JSON格式:
{"code":"123456"}
3. 常见错误码
| 错误码 | 说明 | 解决方案 |
|---|---|---|
| isv.BUSINESS_LIMIT_CONTROL | 触发业务流控 | 降低发送频率 |
| isv.MOBILE_NUMBER_ILLEGAL | 手机号码格式错误 | 检查手机号格式 |
| isv.TEMPLATE_MISSING_PARAMETERS | 模板参数缺失 | 检查模板参数 |
| isv.INVALID_PARAMETERS | 参数异常 | 检查所有参数 |
| isv.AMOUNT_NOT_ENOUGH | 账户余额不足 | 充值 |
| isv.TEMPLATE_PARAMS_ILLEGAL | 模板变量值非法 | 检查变量值格式 |
4. 如何修改验证码位数?
修改 generateVerificationCode() 方法中的循环次数:
private String generateVerificationCode() {
Random random = new Random();
StringBuilder code = new StringBuilder();
// 修改这里的数字,比如改为4位验证码
for (int i = 0; i < 4; i++) {
code.append(random.nextInt(10));
}
return code.toString();
}
5. 如何修改验证码有效期?
修改常量 SMS_CODE_EXPIRE:
// 修改为10分钟
private static final long SMS_CODE_EXPIRE = 10;
6. 如何修改每日发送次数限制?
修改常量 MAX_SMS_COUNT_PER_DAY:
// 修改为5次
private static final int MAX_SMS_COUNT_PER_DAY = 5;
7. Redis连接失败怎么办?
确保Redis服务已启动:
# Linux/Mac
redis-server
# 检查Redis是否运行
redis-cli ping
# 应该返回 PONG
8. 如何测试短信发送?
阿里云提供测试环境,可以使用测试手机号:
// 测试环境配置
config.endpoint = "dysmsapi.aliyuncs.com"; // 正式环境
// config.endpoint = "dysmsapi-test.aliyuncs.com"; // 测试环境(如果有)
9. 生产环境安全建议
- 不要硬编码密钥:使用环境变量或配置中心
- 启用HTTPS:保护API通信安全
- 添加图形验证码:防止恶意刷短信
- IP限流:防止单个IP频繁请求
- 手机号验证:验证手机号格式和归属地
- 监控告警:设置短信发送量告警
10. 性能优化建议
- Redis连接池:配置合理的连接池参数
- 异步发送:使用
@Async异步发送短信 - 批量发送:如果需要群发,使用批量接口
- 缓存优化:合理设置Redis过期时间
完整项目结构
src/
├── main/
│ ├── java/
│ │ └── com/
│ │ └── example/
│ │ ├── controller/
│ │ │ └── SmsController.java
│ │ ├── service/
│ │ │ ├── SmsService.java
│ │ │ └── impl/
│ │ │ └── SmsServiceImpl.java
│ │ ├── common/
│ │ │ ├── ApiResponse.java
│ │ │ └── ResultUtils.java
│ │ └── Application.java
│ └── resources/
│ └── application.yml
└── test/
└── java/
└── com/
└── example/
└── service/
└── SmsServiceTest.java
单元测试示例
SmsServiceTest.java
package com.example.service;
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
class SmsServiceTest {
@Autowired
private SmsService smsService;
@Test
void testSendVerificationCode() throws Exception {
// 使用测试手机号
String phoneNumber = "13800138000";
boolean result = smsService.sendVerificationCode(phoneNumber);
assertTrue(result, "验证码发送应该成功");
}
@Test
void testVerifyCode() throws Exception {
String phoneNumber = "13800138000";
// 发送验证码
smsService.sendVerificationCode(phoneNumber);
// 验证错误的验证码
boolean result1 = smsService.verifyCode(phoneNumber, "000000");
assertFalse(result1, "错误的验证码应该验证失败");
// 注意:无法测试正确的验证码,因为验证码是随机生成的
}
}
总结
本文档提供了阿里云SMS短信服务的完整集成方案,包括:
✅ Maven依赖配置 ✅ 完整的代码实现 ✅ 配置文件示例 ✅ 使用示例和测试方法 ✅ 常见问题解答 ✅ 安全和性能优化建议
将此文档保存到你的项目中,以后需要集成阿里云SMS时可以直接参考使用。