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 文檔,可以找到每一種返回狀態碼對應的說明,並根據狀態碼設定不同的返回狀態解釋。

 

好了,到這里我們就完整的實現了發送短信驗證碼功能,有了它,你的注冊頁面看起來就會有一種高達上的感覺,並且在驗證用戶手機號的真實性也是很有意義的。

文章中若存在錯誤之處,歡迎大家留言指正。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM