Vue+Vant+Koa2 实现发送短信验证码
今天我们来继续完善上一篇的 注册教程,在现实注册过程中,手机短信验证码是必不可少的。那么怎么实现呢?
首先我们需要在短信平台开通短信服务功能,大的平台主要有阿里云、腾讯云、聚合数据等( 一般需要拿到短信模板 ID、APPID、发送链接即可 ),一般费用在每条 0.04 元左右,比较大的平台每天需要发送几千、甚至上万条,可见每天总的短信验证码也是有一定的成本的,这还排除了存在恶意刷验证码情况。因此在后台编写验证码部分代码时,一定要加上发送的数量限制,比如:每个手机号每天最多发送条数(比如最多 6 条)、每个 IP 地址每天最多发送条数等,这样即使是遇到恶意刷验证码的情况也可以保证短信数量不至于过多。
这篇文章是按照生产环境来写的,代码部分包含了真实的验证码发送部分,只要配置好相应的短信平台参数就可以实现发送验证码。这篇文章作为学习研究用,特意将验证码 alert() 出来。
效果图:
弹出 6 位数验证码,然后发送验证码按钮显示 1 分钟倒计时,控制台打印后端发来的返回信息。
主要部分:
1> 添加发送验证码的条数限制,每个 IP 每天最多发送 10 条、每个手机号每天最多发送6条,
2> 验证码用 Math.random() 、Math.floor() 生成随机六位数,
3> 用到 koa-session 将生成的验证码存入 session,等到用户点击注册按钮时,将用户输入的验证码与生成的验证码进行比对,
一、前端部分
1.1 向 Register.vue 中加入 验证码输入框 :
<van-field v-model="smscode" center clearable label="短信验证码" placeholder="请输入短信验证码" > <template #button> <van-button v-if="!cutDownTime" size="small" type="primary" @click="sendSMSCode">发送验证码</van-button> <van-button v-if="cutDownTime" size="small" type="primary">{{cutDownTime}}s后再试</van-button> </template> </van-field>
1.2 添加 sendSMSCode 方法:
async sendSMSCode() { let pattern = /^[1][3,4,5,7,8][0-9]{9}$/g; //正则表达式,验证手机号格式 if(this.telnumber.length != 11 | !pattern.test(this.telnumber)) return this.$toast('请输入正确的手机号') } let res = await ajax.sendSMSCode(this.telnumber);this.cutDownTime = 60; let timer = setInterval(() => { // this.cutDownTime--; if(this.cutDownTime <= 0) { this.cutDownTime = '' } }, 1000)
1.3 修改 src\api\index.js ,添加方法,将手机号发送到后端:
// 发送 短信验证码 sendSMSCode(telnumber) { return axios.post(Url.sendSMSCodeApi,{telnumber}) }
二、后端部分
2.1 首先,我们来安装几个需要用到的插件:
2.1.1 silly-datetime
很便捷的设置时间格式插件,安装好以后,我们在 mall-server\utils 下,新建 tools.js ,用于将用到的工具都封装在这里:
const sd = require('silly-datetime')
/** * 工具封装 */ class Tools { // 格式化当前日期 getCurDate(format = 'YYYYMMDD') { // 默认返回格式:20200529 return sd.format(new Date(),format); } } module.exports = new Tools()
2.1.2 koa-session
安装完毕后,需要在 app.js 中引入:
app.keys = [ 'session secret' ]; // 设置签名的 Cookie 密钥 const CONFIG = { key: 'sessionId', maxAge: 60000, // cookie 的过期时间 60000ms => 60s => 1min overwrite: true, // 是否可以 overwrite (默认 default true) httpOnly: true, // true 表示只有服务器端可以获取 cookie signed: true, // 默认 签名 rolling: false, // 在每次请求时强行设置 cookie,这将重置 cookie 过期时间(默认:false) renew: false, // 在每次请求时强行设置 session,这将重置 session 过期时间(默认:false) }; app.use(session(CONFIG, app));
2.1.3 request:用于简化请求写法
2.1.4 querystring: 用于生成序列化请求路径,用法举例:
let queryData = querystring.stringify({ "mobile": mobilePhone, // 接受短信的用户手机号码 "tpl_id": "187915", // 您申请的短信模板 ID,根据实际情况修改 "tpl_value": `#code#=${ randomNum }`, // 您设置的模板变量,根据实际情况修改 "key": "d52256474eb6d73350e47eb52adbca67", // 应用 APPKEY (应用详细页查询) }); let queryUrl = 'http://v.juhe.cn/sms/send?' + queryData;
2.2 建立手机短信验证码模型,用于验证码发送条数限制统计:
// 手机号数据模型 (用于发送验证码) const mobilePhoneSchema = new Schema({ mobilePhone: Number, // 手机号 clientIp: String, // 客户端 ip sendCount: Number, // 发送次数 curDate: String, // 当前日期 sendTimestamp: { type: String, default: +new Date() }, // 短信发送的时间戳 });
2.3 添加接收前端 post 路由,接收 telnumber:
/** * 发送短信验证码 */ router.post('/sendSMSCode', async function (ctx) { let { telnumber } = ctx.request.body; const clientIp = ctx.req.headers['x-forwarded-for'] || // 判断是否有反向代理 IP ctx.req.connection.remoteAddress || // 判断 connection 的远程 IP ctx.req.socket.remoteAddress || // 判断后端的 socket 的 IP ctx.req.connection.socket.remoteAddress || ''; const curDate = tools.getCurDate(); // 当前时间 // console.log('ip:', clientIp) // console.log('date:', curDate) let args = { telnumber, clientIp, curDate }; try { let smsCodeData = await userService.dispatchSMSCode(args); // 将验证码保存入 session 中 (smsCodeData.code === 200) && (ctx.session.smsCode = smsCodeData.smsCode);
ctx.body = smsCodeData; } catch (error) { console.log(error) } })
2.4 添加 dispatchSMSCode 方法,添加发送条数限制,并调用发送短信 API ,我们将发送 API 封装在 sendSMSCode() 方法里,在下一步介绍,研究用不调用 API ,只将验证码 return 给前端。
/** * 发送短信验证码 * 一个手机号每天最多发送 6 条验证码 * 同一个 ip,一天只能向手机号码发送 10 次 */ async dispatchSMSCode({ mobilePhone, clientIp, curDate }) { let smsSendMax = 6; // 设定每个手机号短信发送限制数 let ipCountMax = 10; // 设定 ip 数限制数 let smsCode = ''; // 随机短信验证码 let smsCodeLen = 6; // 随机短信验证码长度 for (let i = 0; i < smsCodeLen; i++) { smsCode += Math.floor(Math.random() * 10); } console.log('短信验证码:', smsCode) try { // 根据当前日期、手机号查到该手机号当天的发送次数 let mobilePhoneDoc = await MobilePhoneModel.find({mobilePhone, curDate}); // 同一天,同一个 ip 文档条数 let clientIpCount = await MobilePhoneModel.find({clientIp, curDate}).countDocuments(); if (mobilePhoneDoc) { // 说明次数未到到限制,可继续发送 if (mobilePhoneDoc.sendCount < smsSendMax && clientIpCount < ipCountMax) { let sendCount = mobilePhoneDoc.sendCount + 1; //更新单个文档 mobilePhoneDoc.updateOne({ _id: mobilePhoneDoc._id }, { sendCount, sendTimestamp: +new Data() });// 执行发送短信验证码 // let data = sendSMSCode(smsCode, mobilePhone); switch(data.error_code) { case 0: return {smsCode, code: 200, msg: '验证码发送成功'}; case 10012: return { smsCode, code: 5000, msg: '没有免费短信了' }; default: return { smsCode, code: 4000, msg: '未知错误' }; } } else { return {code: 4020, msg: '当前手机号码发送次数达到上限,明天重试'} } } else { return { smsCode, code: 200, msg: '验证码发送成功' }; // 执行发送短信验证码 // const data = sendSMSCode(mobilePhone, smsCode); switch (data.error_code) { case 0: // 创建新文档 | 新增数据 let mPdoc = await MobilePhoneModel.create({ mobilePhone, clientIp, curDate, sendCount: 1 }); console.log(mPdoc) return { smsCode, code: 200, msg: '验证码发送成功' }; case 10012: return { smsCode, code: 5000, msg: '没有免费短信了' }; default: return { smsCode, code: 4000, msg: '未知错误' }; } } } catch (error) { console.log(error) } }
2.5 我们将发送 API 封装在 sendSMSCode() 方法里,该方法位于 mall-server\utils\sms.js 里。
let request = require('request'); let querystring = require('querystring'); /** * 当前选用聚合数据 https://www.juhe.cn SMS API (有免费使用短信条数) * 当然也可以选择其他第三方云服务提供商: 阿里云 | 腾讯云 | 网易云 | ... * * 发送手机短信验证码 * @param {String} mobilePhone 接受短信的用户手机号码 * @param {Number} randomNum 随机验证码 */ function sendSMSCode(randomNum, mobilePhone) { let queryData = querystring.stringify({ "mobile": mobilePhone, // 接受短信的用户手机号码 "tpl_id": "187915", // 您申请的短信模板 ID,根据实际情况修改 "tpl_value": `#code#=${ randomNum }`, // 您设置的模板变量,根据实际情况修改 "key": "d52256474eb6d73350e47eb52adbca67", // 应用 APPKEY (应用详细页查询) }); let queryUrl = 'http://v.juhe.cn/sms/send?' + queryData; return new Promise((resolve, reject) => { request(queryUrl, function(error, response, body) { if (!error && response.statusCode == 200) { // 解析接口返回的JSON内容 let newBody = JSON.parse(body); resolve(newBody); } else { reject('请求异常'); } }); }); } module.exports = sendSMSCode;
其中根据你申请的 API 文档,可以找到每一种返回状态码对应的说明,并根据状态码设定不同的返回状态解释。
好了,到这里我们就完整的实现了发送短信验证码功能,有了它,你的注册页面看起来就会有一种高达上的感觉,并且在验证用户手机号的真实性也是很有意义的。
文章中若存在错误之处,欢迎大家留言指正。
【推荐】还在用 ECharts 开发大屏?试试这款永久免费的开源 BI 工具!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步