1. 项目概述
短信功能在现代Java项目中几乎成为标配需求,无论是用户注册验证、订单状态通知还是安全提醒,短信API集成都是开发者必须掌握的技能。作为在金融和电商领域深耕多年的Java工程师,我参与过数十个短信网关对接项目,从最初的HTTP裸请求到现在的SDK封装,踩过各种坑也积累了大量实战经验。
这个教程将带你用30分钟完成阿里云短信服务的完整集成,包含签名认证、模板配置、异常处理和性能优化等关键环节。不同于官方文档的碎片化说明,我会重点分享企业级项目中那些"文档上不会写但实际开发必须知道"的细节,比如如何避免触发风控、怎样设计重试机制、以及处理不同运营商的特殊限制。
2. 环境准备与SDK选型
2.1 开发环境要求
基础环境需要JDK 8+和Maven 3.6+,我推荐使用IntelliJ IDEA作为IDE(社区版即可)。对于网络环境有个特别提醒:短信API调用需要服务器能够访问公网,但很多企业的测试环境会有白名单限制。曾经有个项目团队花了三天排查"超时问题",最后发现是运维没给测试服务器开通短信服务的出口IP权限。
关键检查点:
- ping dysmsapi.aliyuncs.com 测试连通性
- telnet dysmsapi.aliyuncs.com 80 检查端口
- 企业网络需放行100.100.0.0/16和10.0.0.0/8网段
2.2 阿里云SDK vs 原生HTTP请求
官方提供了aliyun-java-sdk-core和aliyun-java-sdk-dysmsapi两个核心依赖。虽然直接发HTTP请求也能实现功能,但在企业项目中我强烈建议使用SDK:
<dependency> <groupId>com.aliyun</groupId> <artifactId>aliyun-java-sdk-core</artifactId> <version>4.5.28</version> </dependency> <dependency> <groupId>com.aliyun</groupId> <artifactId>aliyun-java-sdk-dysmsapi</artifactId> <version>2.1.0</version> </dependency>SDK的优势在于:
- 自动处理签名算法(SHA-1和HMAC-SHA1)
- 内置重试机制应对网络抖动
- 封装了响应结果解析
- 符合阿里云最新的API变更
3. 核心代码实现
3.1 配置管理最佳实践
不要硬编码AccessKey!我在代码审计时见过太多把AK/SK直接写在类文件里的案例。正确的做法是使用Spring的@Value注入或专门的配置类:
@Configuration public class SmsConfig { @Value("${sms.aliyun.accessKeyId}") private String accessKeyId; @Value("${sms.aliyun.accessKeySecret}") private String accessKeySecret; @Bean public IAcsClient acsClient() { IClientProfile profile = DefaultProfile.getProfile( "cn-hangzhou", accessKeyId, accessKeySecret); return new DefaultAcsClient(profile); } }在application.yml中配置:
sms: aliyun: accessKeyId: yourAccessKey accessKeySecret: yourSecret signName: 企业签名 templateCode: SMS_123456783.2 短信发送服务层实现
基础发送功能的核心代码:
@Service @Slf4j public class AliyunSmsService { @Autowired private IAcsClient acsClient; @Value("${sms.aliyun.signName}") private String signName; public SendSmsResponse sendVerificationCode(String phone, String code) { CommonRequest request = new CommonRequest(); request.setSysDomain("dysmsapi.aliyuncs.com"); request.setSysVersion("2017-05-25"); request.setSysAction("SendSms"); request.putQueryParameter("PhoneNumbers", phone); request.putQueryParameter("SignName", signName); request.putQueryParameter("TemplateCode", "SMS_12345678"); request.putQueryParameter("TemplateParam", "{\"code\":\"" + code + "\"}"); try { CommonResponse response = acsClient.getCommonResponse(request); log.info("短信发送响应: {}", response.getData()); return objectMapper.readValue(response.getData(), SendSmsResponse.class); } catch (Exception e) { throw new SmsException("短信发送失败", e); } } }3.3 模板参数的特殊处理
当模板中包含多个变量时,JSON字符串的构建要特别注意转义问题。建议使用Jackson的ObjectMapper来确保格式正确:
Map<String, String> params = new HashMap<>(); params.put("code", "1234"); params.put("product", "智能门锁"); String templateParam = objectMapper.writeValueAsString(params); request.putQueryParameter("TemplateParam", templateParam);4. 企业级功能增强
4.1 发送频率控制
为防止恶意刷短信,必须实现发送间隔限制。我推荐使用Redis的过期键特性:
public boolean checkSendFrequency(String phone) { String key = "sms:limit:" + phone; // 同一手机号60秒内只能发一次 return redisTemplate.opsForValue().setIfAbsent(key, "1", 60, TimeUnit.SECONDS); }4.2 异步发送与重试机制
对于批量发送场景,应该采用异步处理。以下是基于Spring @Async的实现:
@Async("smsTaskExecutor") @Retryable(value = SmsException.class, maxAttempts = 3) public void asyncSendSms(String phone, String templateCode, Map<String, String> params) { try { SendSmsResponse response = sendSms(phone, templateCode, params); if (!"OK".equalsIgnoreCase(response.getCode())) { throw new SmsException(response.getMessage()); } } catch (ClientException e) { log.error("短信发送客户端异常", e); throw new SmsException(e.getMessage()); } }需要在配置类添加:
@Configuration @EnableAsync @EnableRetry public class AsyncConfig { @Bean(name = "smsTaskExecutor") public Executor taskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(5); executor.setMaxPoolSize(10); executor.setQueueCapacity(100); executor.setThreadNamePrefix("SmsSender-"); executor.initialize(); return executor; } }5. 异常处理与监控
5.1 错误码分类处理
阿里云短信API返回的错误码需要针对性处理:
| 错误码 | 含义 | 处理建议 |
|---|---|---|
| isv.BUSINESS_LIMIT_CONTROL | 流量控制 | 检查是否触发频控,等待后重试 |
| isv.INVALID_PARAMETERS | 参数错误 | 检查模板参数格式 |
| isv.MOBILE_NUMBER_ILLEGAL | 非法手机号 | 校验手机号格式 |
| isv.AMOUNT_NOT_ENOUGH | 账户余额不足 | 及时充值 |
实现建议:
public void handleErrorCode(String code) { switch (code) { case "isv.BUSINESS_LIMIT_CONTROL": throw new FrequencyLimitException("短信发送太频繁"); case "isv.AMOUNT_NOT_ENOUGH": alertService.notifyAdmin("短信余额不足"); throw new BusinessException("服务暂不可用"); // 其他错误码处理... } }5.2 Prometheus监控集成
在生产环境中应该监控短信发送的成功率:
@Bean public MeterRegistryCustomizer<PrometheusMeterRegistry> smsMetrics() { return registry -> { Counter.builder("sms.requests") .description("Total SMS requests") .tag("type", "verification") .register(registry); Counter.builder("sms.failures") .description("Failed SMS requests") .tag("type", "verification") .register(registry); }; } // 在发送方法中添加计数 smsRequests.increment(); try { // 发送逻辑... } catch (Exception e) { smsFailures.increment(); throw e; }6. 安全防护方案
6.1 敏感信息脱敏
日志中的手机号需要脱敏处理:
public String maskPhoneNumber(String phone) { if (phone == null || phone.length() < 7) return phone; return phone.substring(0, 3) + "****" + phone.substring(7); } // 使用示例 log.info("发送短信至: {}", maskPhoneNumber("13800138000"));6.2 请求签名加固
虽然SDK已经处理了基础签名,但在高安全要求场景可以增加额外验证:
public boolean verifyRequest(String phone, String timestamp, String nonce, String signature) { String content = phone + timestamp + nonce + smsConfig.getSecretSalt(); String actualSign = DigestUtils.sha256Hex(content); return actualSign.equals(signature); }7. 性能优化技巧
7.1 连接池配置
修改SDK默认的HTTP连接参数:
DefaultProfile profile = DefaultProfile.getProfile(regionId, accessKeyId, accessKeySecret); HttpClientConfig clientConfig = HttpClientConfig.getDefault(); clientConfig.setMaxRequestsPerHost(50); clientConfig.setConnectionTimeoutMillis(3000); profile.setHttpClientConfig(clientConfig);7.2 本地缓存优化
对模板内容和签名信息进行缓存:
@Cacheable(value = "smsTemplate", key = "#templateCode") public String getTemplateContent(String templateCode) { // 从数据库或API获取模板内容 return templateRepository.findByCode(templateCode).getContent(); }8. 测试验证方案
8.1 Junit测试用例
@Test public void testSendVerificationCode() { String testPhone = "13800138000"; String testCode = "123456"; when(acsClient.getCommonResponse(any())) .thenReturn(mockResponse(200, "{\"Code\":\"OK\"}")); SendSmsResponse response = smsService.sendVerificationCode(testPhone, testCode); assertEquals("OK", response.getCode()); } private CommonResponse mockResponse(int status, String data) { CommonResponse response = new CommonResponse(); response.setHttpStatus(status); response.setHttpContent(data.getBytes(), "UTF-8", null); return response; }8.2 集成测试要点
- 准备测试专用签名和模板
- 使用真实的测试手机号接收
- 验证不同运营商号码(移动/联通/电信/虚拟运营商)
- 测试边界情况:空参数、超长内容、特殊字符
- 模拟网络异常测试重试机制
9. 部署上线检查清单
- [ ] 确认AccessKey具有最小必要权限
- [ ] 检查短信签名和模板已审核通过
- [ ] 配置生产环境的endpoint地址
- [ ] 设置合理的线程池和连接池参数
- [ ] 接入监控和告警系统
- [ ] 准备应急预案(如切换备用通道)
10. 扩展应用场景
10.1 国际短信支持
// 设置国际区号 request.putQueryParameter("PhoneNumbers", "+852" + phone); // 使用国际短信模板 request.putQueryParameter("TemplateCode", "SMS_12345678_OVERSEA");10.2 语音验证码集成
public SingleCallByTtsResponse sendVoiceCode(String phone, String code) { SingleCallByTtsRequest request = new SingleCallByTtsRequest(); request.setCalledNumber(phone); request.setTtsCode("TTS_12345678"); request.setTtsParam("{\"code\":\""+code+"\"}"); return acsClient.getAcsResponse(request); }在电商项目中,我们通常将短信和语音验证码结合使用:第一次发送短信,如果用户5分钟内未收到,再触发语音验证码作为备用方案。这种组合方案可以将送达率提升至99.9%以上。