SpringBoot整合阿里云实现短信验证码功能

  Java   43分钟   134浏览   0评论

引言

你好呀,我是小邹。

在现代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 安全机制设计

  1. 频率控制:防止恶意频繁发送

    • 通过Redis记录上次发送时间
    • 强制等待间隔(默认60秒)
  2. 有效期控制

    • 验证码5分钟有效
    • 过期自动清理
  3. 防暴力破解

    • 错误次数限制(5次)
    • 错误次数过多时强制失效验证码
  4. 防重复使用

    • 验证成功后立即删除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测试

  1. 发送验证码请求

    • Method: POST
    • URL: http://localhost:8080/api/verify/send
    • Params:
      • phone: 13800138000
      • businessType: LOGIN
  2. 验证验证码请求

    • 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 性能优化建议

  1. 连接池优化

    spring:
      redis:
        lettuce:
          pool:
            max-active: 20      # 根据并发量调整
            max-idle: 10
            min-idle: 5
    
  2. 批量操作:对于大量验证需求,考虑批量验证

  3. 异步发送:短信发送可以异步处理

    @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 监控指标

建议监控以下指标:

  1. 短信发送成功率
  2. 验证码验证成功率
  3. Redis连接状态
  4. 发送频率异常情况

八、部署注意事项

  1. 环境配置

    • 生产环境和开发环境使用不同的阿里云密钥
    • Redis生产环境建议使用集群模式
  2. 安全加固

    • 密钥通过环境变量或配置中心管理
    • 启用Redis SSL连接
    • 限制API访问IP
  3. 备份策略

    • 定期备份验证码发送记录
    • 监控短信费用使用情况

总结

本文详细介绍了基于阿里云短信服务和Redis的验证码系统的完整实现方案。该系统具有以下特点:

  1. 安全性高:多重防护机制,有效防止恶意攻击
  2. 可靠性强:基于Redis的持久化存储,确保数据不丢失
  3. 扩展性好:模块化设计,便于功能扩展
  4. 易于维护:完善的日志和监控体系

通过本文的实现,您可以快速构建一个生产级别的短信验证码系统,满足各种业务场景的需求。

如果你觉得文章对你有帮助,那就请作者喝杯咖啡吧☕
微信
支付宝
  0 条评论