手机短信验证码技术
1.流程图
- 前端点击发送手机验证码
后端判断恶意请求拦截【手机号码限制次数 - redis设置过期时间,自增 - 大于10次直接抛异常 - 没有做】 - 验证图形验证码是否正确,不正确直接抛业务异常
前端用户输入的图形验证码和之前在redis保存的图形验证码对比【5分钟有效】,本地存储中 key - 如果正确,判断当前手机验证码是否有效
直接判断redis有没有:有就是没有过期,没有就是过期了或第一次发送
4.1. 在redis中没有找到手机验证码:第一次发送或手机验证码过期
a.重新生成新的手机验证码
4.2. 在redis中找到手机验证码:验证码还没过期,点击重新获取
a.判断重新获取验证码是否过了重发时间:防止违规操作 - 刷新页面又可以重新获取
为什么会有重发时间:由于网络问题,需要重发时间 和 防止重复点击重复请求
a1.如果没有过重发时间,又在重新点击重新验证码,调用接口 - 违规操作 - 抛业务异常
a2.如果过了重发时间,可以点击重新获取
b?.没有过期的验证码还在 <=> 重新获取要生成新的验证码【新的验证码如何选择:重新生成?还是用以前的没有过期的那一个】
5.记录验证码信息 = 保存到redis,设置过期时间
6.调用短信接口发送短信:手机code,手机号码
2.手机验证码发送细节
- redis记录/保存验证码
key:业务键:手机号 - 防止业务不兼容,如果同时手机号注册或登录,相同的key会覆盖
value:验证码:时间戳 - 可以获取时间戳判断重发时间
例如:register:188866666666 - 9527:14688777777711
register:188866666666 - 业务键:电话号码 - 因为以后其他业务还需要发送验证码,例如登录
9527:14688777777711 - 验证码:时间戳 - 时间戳用于计算是否重发时间 - 第一次发或验证码过期了:直接走主流程
- 后续再发:
要验证是否过期 - redis中获取是否为null
要判断是否过重发时间 - 时间戳
要使用redis存储验证码
要重置过期时间
3.短信发送接口
依赖
<!-- https://mvnrepository.com/artifact/commons-httpclient/commons-httpclient -->
<dependency>
<groupId>commons-httpclient</groupId>
<artifactId>commons-httpclient</artifactId>
<version>3.1</version>
</dependency>
接口,具体看对接的发送平台,查看官网API,如下列中国网建
package com.lzc.basic.utils; import org.apache.commons.httpclient.Header; import org.apache.commons.httpclient.HttpClient; import org.apache.commons.httpclient.NameValuePair; import org.apache.commons.httpclient.methods.PostMethod; import java.io.IOException; public class SendMsgUtils { public static void sendMsg(String phone,String code) { try { HttpClient client = new HttpClient(); PostMethod post = new PostMethod("https://utf8api.smschinese.cn/"); post.addRequestHeader("Content-Type","application/x-www-form-urlencoded;charset=utf-8");//在头文件中设置转码 NameValuePair[] data ={ new NameValuePair("Uid", "lzcxx"),new NameValuePair("Key", "7702FE8575F11493D0EA14149F57E457"),new NameValuePair("smsMob",phone),new NameValuePair("smsText",code)}; post.setRequestBody(data); client.executeMethod(post); Header[] headers = post.getResponseHeaders(); int statusCode = post.getStatusCode(); System.out.println("statusCode:"+statusCode); //HTTP状态码 for(Header h : headers){ System.out.println(h.toString()); } String result = new String(post.getResponseBodyAsString().getBytes("utf-8")); System.out.println(result); //打印返回消息状态 post.releaseConnection(); } catch (IOException e) { throw new RuntimeException(e); } } }
4.发送短信验证码前端,前端页面按钮设置倒计时60s
- 手机注册的div上添加id属性值,并且数据双向绑定
<input type="tel" name="" v-model="phoneUserForm.phone" id="phone" placeholder="请输入手机号">
- 获取的a标签换成button
<button type="button" @click="sendMobileCode();">获取
<script type="text/javascript">
new Vue({
//挂载点:当前Vue实例中的所有数据【模型数据data,方法methods】只能在id="app"的元素中使用
el: "#app",
//模型数据:可以有多个,所以也是json对象
data: {
phoneUserForm: {
imageCode: '', //用户输入的图形验证码
phone: '18706553265' //注册手机号
},
base64ImageCode: ''
},
//方法:可以有多个,所以也是json对象
methods: {
//获取手机验证码
sendMobileCode() {
//1.判断手机号不为空
if (!this.phoneUserForm.phone) {
alert("手机号不能为空");
return;
}
//2.判断图片验证码不为空
if (!this.phoneUserForm.imageCode) {
alert("图片验证码不能为空");
return;
}
//3.获取按钮,禁用按钮 发送时灰化不能使用,发送成功倒计时60才能使用,如果发送失败立即可以发送
var sendBtn = $(event.target);//获取到按钮
sendBtn.attr("disabled", true);//设置属性:disabled="true"
//组装请求参数
var param = {
phone: this.phoneUserForm.phone, //手机号码
imageCode: this.phoneUserForm.imageCode, //图形验证码
imageCodeKey: localStorage.getItem("phoneCodeKey") //获取redis的图形验证码的key
};
//4.发送ajax请求
this.$http.post("/verifyCode/smsCode", param).then(res => {
var ajaxResult = res.data;
if (ajaxResult.success) {
alert("手机验证码已经发送到您的手机,请在3分钟内使用");
//4.1.发送成:倒计时
var time = 60;
//周期性定时器
var interval = window.setInterval(function () {
//每一条倒计时减一
time = time - 1;
//把倒计时时间搞到按钮上
sendBtn.html(time + "s");
//4.2.倒计时完成恢复按钮
if (time <= 0) {
sendBtn.html("重新发送");
sendBtn.attr("disabled", false); //解除禁用
//清除定时器
window.clearInterval(interval);
}
}, 1000);
} else {
//4.3.发送失败:提示,恢复按钮
sendBtn.attr("disabled", false);
alert(ajaxResult.message);
}
})
}
},
//页面一加载就会执行
mounted() {
this.getImageCode();
}
})
</script>
5.总结
获取对接官网的key授权码,导入依赖,前端发送请求携带手机号,后端Dto接收参数,调用接口1.参数校验(JSR303);2.业务校验;3.业务实现
@Override public void sendPhoneCode(PhoneCodeDto phoneCodeDto) { //1.参数校验 //2.业务校验 //2.1校验图形验证码是否存在 String uuid = phoneCodeDto.getUuid(); Object objImageCode = redisTemplate.opsForValue().get(uuid); if (objImageCode == null) { throw new GlobalException("1021", "验证码不存在"); } //2.2校验图形验证码是否正确 String imageCode = phoneCodeDto.getImageCode(); if (!StrUtil.equalsAnyIgnoreCase(objImageCode.toString(), imageCode)) { throw new GlobalException("1022", "验证码不正确!"); } //2.3手机号是否已被注册 String phone = phoneCodeDto.getPhone(); User user = userMapper.findOneByPhone(phone); if (user != null) { throw new GlobalException("1023", "手机号已注册!"); } //字符串的format方法会把%s替换了的值返回给你 String key = String.format(VerifyCodeConstant.SEND_PHONE_CODE_KEY, phone); //业务实现 Long expire = redisTemplate.getExpire(key, TimeUnit.MILLISECONDS); String code = ""; // 判断expire返回值是否为空-2或者超过60秒过期了 if (expire == null || expire < 240000) { code = RandomUtil.randomString(6); } else { throw new GlobalException("1024", "验证码已过期!"); } //将新的code存到redis中 redisTemplate.opsForValue().set(key, code, 5, TimeUnit.MINUTES); //发送短信 // SendMsgUtils.sendMsg(phone,code); System.out.println(code); }