# 阿里云SMS短信服务集成文档 ## 目录 1. [概述](#概述) 2. [Maven依赖](#maven依赖) 3. [配置文件](#配置文件) 4. [代码实现](#代码实现) 5. [使用示例](#使用示例) 6. [常见问题](#常见问题) --- ## 概述 本文档提供阿里云短信服务(SMS)的完整集成方案,包括验证码发送、验证码校验、发送频率限制等功能。 ### 功能特性 - ✅ 发送6位数字验证码 - ✅ 验证码5分钟有效期 - ✅ 每个手机号每天最多发送10次 - ✅ 使用Redis存储验证码和发送次数 - ✅ 完整的错误处理和日志记录 --- ## Maven依赖 ### pom.xml ```xml com.aliyun dysmsapi20170525 2.0.24 com.aliyun tea-openapi 0.2.8 com.aliyun tea-util 0.2.21 org.springframework.boot spring-boot-starter-data-redis org.springframework.boot spring-boot-starter-web ``` --- ## 配置文件 ### 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 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 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 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 { 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 ApiResponse success(T data) { return new ApiResponse<>(200, "success", data); } public static ApiResponse success(T data, String message) { return new ApiResponse<>(200, message, data); } public static ApiResponse 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/)