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

19 KiB
Raw Permalink Blame History

阿里云SMS短信服务集成文档

目录

  1. 概述
  2. Maven依赖
  3. 配置文件
  4. 代码实现
  5. 使用示例
  6. 常见问题

概述

本文档提供阿里云短信服务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. 进入"短信服务"产品页面
  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() 方法中的循环次数:

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. 生产环境安全建议

  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

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时可以直接参考使用。


相关链接