短信服务(SMS,Short Message Service)是一种允许应用程序通过API发送短信的云服务。在企业应用中,短信服务常用于:
短信服务的基本工作流程如下:
在中国市场,主流的短信服务商包括:
服务商 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
阿里云 | 稳定性高,集成简单,有完整SDK | 价格相对较高 | 大中型企业应用 |
腾讯云 | 与微信生态结合好,有免费体验 | 某些地区到达率不稳定 | 与腾讯生态结合的应用 |
华为云 | 安全性高,企业信誉好 | API相对复杂 | 政企客户 |
容联云 | 价格实惠,到达率尚可 | SDK更新不够及时 | 创业公司,成本敏感场景 |
云片网 | 接口简单,支持国际短信 | 大客户支持不足 | 小型应用,需要国际短信 |
短信服务通常按照以下方式计费:
在中国,由于监管要求,发送短信需要预先审核的短信模板和短信签名:
短信签名:显示在短信内容前面的标识,一般是企业名称或产品名称,如【淘宝网】。
短信模板:预设的短信内容格式,包含变量占位符,如:
【XX公司】您的验证码是${code},5分钟内有效,请勿泄露给他人。
首先,我们需要创建一个Spring Boot项目。可以通过Spring Initializr或IDE工具创建。
基本依赖:
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-configuration-processorartifactId>
<optional>trueoptional>
dependency>
dependencies>
建议采用以下项目结构:
src/main/java/com/example/sms
│
├── config
│ └── SmsConfig.java
│
├── service
│ ├── SmsService.java (接口)
│ └── impl
│ ├── AliyunSmsServiceImpl.java
│ ├── TencentSmsServiceImpl.java
│ └── ...
│
├── model
│ ├── SmsRequest.java
│ └── SmsResponse.java
│
├── controller
│ └── SmsController.java
│
├── util
│ └── SmsUtil.java
│
└── SmsApplication.java
阿里云短信是国内使用较广泛的短信服务,下面详细介绍如何集成。
在pom.xml
中添加:
<dependency>
<groupId>com.aliyungroupId>
<artifactId>aliyun-java-sdk-coreartifactId>
<version>4.6.0version>
dependency>
<dependency>
<groupId>com.aliyungroupId>
<artifactId>dysmsapi20170525artifactId>
<version>2.0.23version>
dependency>
创建application.yml
配置文件:
sms:
aliyun:
access-key-id: 您的AccessKeyID
access-key-secret: 您的AccessKeySecret
sign-name: 您的短信签名
templates:
verification-code: SMS_1234567890 # 验证码模板ID
order-notification: SMS_0987654321 # 订单通知模板ID
# 短信发送的一些通用配置
common:
max-retry-count: 3 # 最大重试次数
retry-interval: 1000 # 重试间隔(毫秒)
code-expire: 300 # 验证码有效期(秒)
创建配置类读取这些配置:
package com.example.sms.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import lombok.Data;
import java.util.Map;
@Data
@Configuration
@ConfigurationProperties(prefix = "sms")
public class SmsConfig {
private AliyunConfig aliyun;
private CommonConfig common;
@Data
public static class AliyunConfig {
private String accessKeyId;
private String accessKeySecret;
private String signName;
private Map<String, String> templates;
}
@Data
public static class CommonConfig {
private int maxRetryCount;
private long retryInterval;
private int codeExpire;
}
}
SmsRequest.java:
package com.example.sms.model;
import lombok.Builder;
import lombok.Data;
import java.util.Map;
@Data
@Builder
public class SmsRequest {
private String templateId; // 短信模板ID
private String phoneNumber; // 手机号码
private Map<String, String> templateParams; // 模板参数
}
SmsResponse.java:
package com.example.sms.model;
import lombok.Builder;
import lombok.Data;
@Data
@Builder
public class SmsResponse {
private boolean success; // 是否发送成功
private String code; // 状态码
private String message; // 消息
private String requestId; // 请求ID
private String bizId; // 业务ID
}
package com.example.sms.service;
import com.example.sms.model.SmsRequest;
import com.example.sms.model.SmsResponse;
public interface SmsService {
/**
* 发送短信
*
* @param request 短信请求
* @return 短信响应
*/
SmsResponse sendSms(SmsRequest request);
/**
* 发送验证码
*
* @param phoneNumber 手机号码
* @return 验证码
*/
String sendVerificationCode(String phoneNumber);
/**
* 验证验证码
*
* @param phoneNumber 手机号码
* @param code 验证码
* @return 是否验证通过
*/
boolean verifyCode(String phoneNumber, String code);
}
package com.example.sms.service.impl;
import com.aliyun.dysmsapi20170525.Client;
import com.aliyun.dysmsapi20170525.models.SendSmsRequest;
import com.aliyun.dysmsapi20170525.models.SendSmsResponse;
import com.aliyun.teaopenapi.models.Config;
import com.example.sms.config.SmsConfig;
import com.example.sms.model.SmsRequest;
import com.example.sms.model.SmsResponse;
import com.example.sms.service.SmsService;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import javax.annotation.PostConstruct;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.TimeUnit;
@Slf4j
@Service
public class AliyunSmsServiceImpl implements SmsService {
private static final String VERIFICATION_CODE_PREFIX = "sms:vcode:";
private static final String VERIFICATION_CODE_TEMPLATE_KEY = "verification-code";
@Autowired
private SmsConfig smsConfig;
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private ObjectMapper objectMapper;
private Client client;
@PostConstruct
public void init() throws Exception {
Config config = new Config()
.setAccessKeyId(smsConfig.getAliyun().getAccessKeyId())
.setAccessKeySecret(smsConfig.getAliyun().getAccessKeySecret())
.setEndpoint("dysmsapi.aliyuncs.com");
client = new Client(config);
}
@Override
public SmsResponse sendSms(SmsRequest request) {
try {
// 构建请求对象
SendSmsRequest sendSmsRequest = new SendSmsRequest()
.setPhoneNumbers(request.getPhoneNumber())
.setSignName(smsConfig.getAliyun().getSignName())
.setTemplateCode(request.getTemplateId())
.setTemplateParam(objectMapper.writeValueAsString(request.getTemplateParams()));
log.info("发送短信请求: {}", sendSmsRequest);
// 发送短信
SendSmsResponse response = client.sendSms(sendSmsRequest);
log.info("短信发送结果: {}", response);
// 处理响应
boolean success = "OK".equals(response.getBody().getCode());
return SmsResponse.builder()
.success(success)
.code(response.getBody().getCode())
.message(response.getBody().getMessage())
.requestId(response.getBody().getRequestId())
.bizId(response.getBody().getBizId())
.build();
} catch (Exception e) {
log.error("短信发送异常", e);
return SmsResponse.builder()
.success(false)
.code("UNKNOWN_ERROR")
.message(e.getMessage())
.build();
}
}
@Override
public String sendVerificationCode(String phoneNumber) {
// 生成6位随机验证码
String code = generateVerificationCode(6);
// 构建短信请求
Map<String, String> templateParams = new HashMap<>();
templateParams.put("code", code);
SmsRequest request = SmsRequest.builder()
.phoneNumber(phoneNumber)
.templateId(smsConfig.getAliyun().getTemplates().get(VERIFICATION_CODE_TEMPLATE_KEY))
.templateParams(templateParams)
.build();
// 发送短信
SmsResponse response = sendSms(request);
if (response.isSuccess()) {
// 将验证码存入Redis,设置过期时间
String key = VERIFICATION_CODE_PREFIX + phoneNumber;
redisTemplate.opsForValue().set(key, code, smsConfig.getCommon().getCodeExpire(), TimeUnit.SECONDS);
return code;
} else {
log.error("验证码发送失败: {}", response);
throw new RuntimeException("验证码发送失败: " + response.getMessage());
}
}
@Override
public boolean verifyCode(String phoneNumber, String code) {
String key = VERIFICATION_CODE_PREFIX + phoneNumber;
String savedCode = redisTemplate.opsForValue().get(key);
if (StringUtils.hasText(savedCode) && savedCode.equals(code)) {
// 验证成功后删除验证码
redisTemplate.delete(key);
return true;
}
return false;
}
/**
* 生成随机验证码
*/
private String generateVerificationCode(int length) {
Random random = new Random();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < length; i++) {
sb.append(random.nextInt(10));
}
return sb.toString();
}
}
package com.example.sms.controller