解释一波逻辑:
- 前端将手机号作为入参,请求获取短信验证码接口;
- 后端拿到手机号,根据手机号构建 Redis Key , 如 verification_code:18012349108;
- 查询 Redis , 判断该 Key 值是否存在;
- 若已经存在,说明验证码还在有效期内(设置了3分钟有效期),并提示用户请求验证码太过频繁;
- 若不存在,则生成 6 位随机数字作为验证码;
- 调用第三方短信发送服务,比如阿里云的,将验证码发送到用户手机上;
- 同时,将该验证码存储到 Redis 中,过期时间为 3 分钟,用于后续用户点击登录时,判断填写的验证码和缓存中的是否一致,以及判断用户获取验证码是否太过频繁。
接口定义
接口地址:POST /verification/code/sent
入参:
{
"phone":"18019939108" // 手机号
}
出参:
{
"success": false,
"message": "请求太频繁,请3分钟后再试",
"errorCode": "AUTH-20000",
"data": null
}
入参VO:
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class SendVerificationCodeReqVO {
@NotBlank(message = "手机号不能为空")
@PhoneNumber
private String phone;
}
这里的 @PhoneNumber
是自定义注解。
代码
用到了一些常用的工具类包:
首先:
[[202502222143 Redis 创建 RedisKeyConstants 常量类,用于统一管理 Redis Key|创建 RedisKeyConstants
常量类,用于统一管理 Redis Key]]
之后增加验证码请求次数过多的业务异常:
VERIFICATION_CODE_SEND_FREQUENTLY("USER-20000", "请求太频繁,请3分钟后再试"),
定义 verificationCodeService
:
public interface VerificationCodeService {
/**
* 接受手机号并发送验证码
*
* @param sendVerificationCodeReqVO the send verification code req vo
* @return the response
*/ Response<Object> send(SendVerificationCodeReqVO sendVerificationCodeReqVO);
}
以及实现类:这里用到了 使用进程池优化网络IO
@Service
@Slf4j
public class VerificationCodeServiceImpl implements VerificationCodeService {
@Resource
RedissonClient redissonClient;
@Override
public Response<Object> send(SendVerificationCodeReqVO sendVerificationCodeReqVO) {
// 获取手机号
String phone = sendVerificationCodeReqVO.getPhone();
// 构建手机号
RedisKey String key = RedisKeyConstants.buildVerificationCodeKey(phone);
// 判断是否已经发送过,即 redis 中还存留着
RBucket<String> bucket = redissonClient.getBucket(key);
if(bucket.isExists())
throw new BizException(ResponseCodeEnum.VERIFICATION_CODE_SEND_FREQUENTLY);
// 生成六位随机数字
String verificationCode = RandomUtil.randomNumbers(6);
// TODO 调用发码平台
log.info("已发送手机号 {} 验证码:{}", phone,verificationCode);
// 存储到 redis 并设置 3 分钟后过期
bucket.set(verificationCode, 3, TimeUnit.MINUTES);
return Response.success();
}
}
接入阿里api
短信服务注册资格后申请一个测试,模版选用阿里云专用模板即可(免审核)。
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>dysmsapi20170525</artifactId>
<version>3.1.1</version>
</dependency>
记得获取阿里云的 Access Key
,它是接入凭证,放在application.yml 即可:
############ AliCloud SMS 配置 ############
aliyun:
accessKeyId: LTAI5tGQNtUFU8mT8pnr6avb
accessKeySecret: CsjjeEgG8bTeoJczWDxbl66OzcoAZm
接下来创建一些相关类,放在 sms 文件夹下:
AliyunAccessKeyProperties
用来接受配置文件中填写的AccessKey
信息
@ConfigurationProperties(prefix = "aliyun")
@Component
@Data
public class AliyunAccessKeyProperties {
private String accessKeyId;
private String accessKeySecret;
}
AliyunSmsClientConfig
配置类,用于初始化一个短信发送客户端,注入到 Spring 容器中
import com.aliyun.dysmsapi20170525.Client;
import com.aliyun.teaopenapi.models.Config;
/**
* @author Edwin
* @date 2/22/2025 11:25 PM
* @description: 短信发送客户端
*/
@Configuration
@Slf4j
public class AliyunSmsClientConfig {
@Resource
private AliyunAccessKeyProperties aliyunAccessKeyProperties;
@Bean
public Client smsClient() {
try {
Config config = new Config()
// 必填
.setAccessKeyId(aliyunAccessKeyProperties.getAccessKeyId())
// 必填
.setAccessKeySecret(aliyunAccessKeyProperties.getAccessKeySecret());
// Endpoint 请参考 https://api.aliyun.com/product/Dysmsapi config.endpoint = "dysmsapi.aliyuncs.com";
return new Client(config);
} catch (Exception e) {
log.error("初始化阿里云短信发送客户端错误: ", e);
return null;
}
}
}
AliyunSmsHelper
短信发送工具类
import com.aliyun.dysmsapi20170525.Client;
/**
* @author Edwin
* @date 2/22/2025 11:28 PM
* @description: TODO
*/
@Component
@Slf4j
public class AliyunSmsHelper {
@Resource
private Client client;
/**
* 发送短信
* @param signName
* @param templateCode
* @param phone
* @param templateParam
* @return
*/
public boolean sendMessage(String signName, String templateCode, String phone, String templateParam) {
SendSmsRequest sendSmsRequest = new SendSmsRequest()
.setSignName(signName)
.setTemplateCode(templateCode)
.setPhoneNumbers(phone)
.setTemplateParam(templateParam);
RuntimeOptions runtime = new RuntimeOptions();
try {
log.info("==> 开始短信发送, phone: {}, signName: {}, templateCode: {}, templateParam: {}", phone, signName, templateCode, templateParam);
// 发送短信
SendSmsResponse response = client.sendSmsWithOptions(sendSmsRequest, runtime);
log.info("==> 短信发送成功, response: {}", JsonUtil.toJsonString(response)); // JsonUtil 为自定义的
return true;
} catch (Exception error) {
log.error("==> 短信发送错误: ", error);
return false;
}
}
}
之后再使用线程池调用即可:
// 调用第三方短信发送服务
threadPoolTaskExecutor.submit(() -> {
String signName = "阿里云短信测试";
String templateCode = "SMS_154950909";
String templateParam = String.format("{\"code\":\"%s\"}", verificationCode);
aliyunSmsHelper.sendMessage(signName, templateCode, phone, templateParam);
});