Files
cpzs-backend/阿里云SMS接口集成文档.md
2026-02-14 12:15:01 +08:00

713 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 阿里云SMS短信服务集成文档
## 目录
1. [概述](#概述)
2. [Maven依赖](#maven依赖)
3. [配置文件](#配置文件)
4. [代码实现](#代码实现)
5. [使用示例](#使用示例)
6. [常见问题](#常见问题)
---
## 概述
本文档提供阿里云短信服务SMS的完整集成方案包括验证码发送、验证码校验、发送频率限制等功能。
### 功能特性
- ✅ 发送6位数字验证码
- ✅ 验证码5分钟有效期
- ✅ 每个手机号每天最多发送10次
- ✅ 使用Redis存储验证码和发送次数
- ✅ 完整的错误处理和日志记录
---
## Maven依赖
### pom.xml
```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
```yaml
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**:
```bash
export ALIYUN_ACCESS_KEY_ID=your-access-key-id
export ALIYUN_ACCESS_KEY_SECRET=your-access-key-secret
```
**Windows**:
```cmd
set ALIYUN_ACCESS_KEY_ID=your-access-key-id
set ALIYUN_ACCESS_KEY_SECRET=your-access-key-secret
```
---
## 代码实现
### 1. Service接口
**SmsService.java**
```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**
```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**
```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**
```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**
```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. 发送验证码
**请求**:
```bash
curl -X POST http://localhost:8080/sms/sendCode \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "phoneNumber=13800138000"
```
**响应**:
```json
{
"code": 200,
"message": "验证码发送成功",
"data": true
}
```
### 2. 验证验证码
**请求**:
```bash
curl -X POST http://localhost:8080/sms/verifyCode \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "phoneNumber=13800138000&code=123456"
```
**响应**:
```json
{
"code": 200,
"message": "验证成功",
"data": true
}
```
### 3. 在业务代码中使用
```java
@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. 登录[阿里云控制台](https://www.aliyun.com/)
2. 进入"短信服务"产品页面
3. 申请短信签名(需要企业资质或个人认证)
4. 申请短信模板需要审核一般1-2个工作日
5. 获取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()` 方法中的循环次数:
```java
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`
```java
// 修改为10分钟
private static final long SMS_CODE_EXPIRE = 10;
```
### 6. 如何修改每日发送次数限制?
修改常量 `MAX_SMS_COUNT_PER_DAY`
```java
// 修改为5次
private static final int MAX_SMS_COUNT_PER_DAY = 5;
```
### 7. Redis连接失败怎么办
确保Redis服务已启动
```bash
# Linux/Mac
redis-server
# 检查Redis是否运行
redis-cli ping
# 应该返回 PONG
```
### 8. 如何测试短信发送?
阿里云提供测试环境,可以使用测试手机号:
```java
// 测试环境配置
config.endpoint = "dysmsapi.aliyuncs.com"; // 正式环境
// config.endpoint = "dysmsapi-test.aliyuncs.com"; // 测试环境(如果有)
```
### 9. 生产环境安全建议
1. **不要硬编码密钥**:使用环境变量或配置中心
2. **启用HTTPS**保护API通信安全
3. **添加图形验证码**:防止恶意刷短信
4. **IP限流**防止单个IP频繁请求
5. **手机号验证**:验证手机号格式和归属地
6. **监控告警**:设置短信发送量告警
### 10. 性能优化建议
1. **Redis连接池**:配置合理的连接池参数
2. **异步发送**:使用 `@Async` 异步发送短信
3. **批量发送**:如果需要群发,使用批量接口
4. **缓存优化**合理设置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**
```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时可以直接参考使用。
---
## 相关链接
- [阿里云短信服务官网](https://www.aliyun.com/product/sms)
- [阿里云短信服务API文档](https://help.aliyun.com/document_detail/101414.html)
- [阿里云短信服务SDK](https://help.aliyun.com/document_detail/215759.html)
- [阿里云短信服务控制台](https://dysms.console.aliyun.com/)