Vue+Vant+Koa2 實現發送短信驗證碼

今天咱們來繼續完善上一篇的 註冊教程,在現實註冊過程當中,手機短信驗證碼是必不可少的。那麼怎麼實現呢?前端

首先咱們須要在短信平臺開通短信服務功能,大的平臺主要有阿里雲、騰訊雲、聚合數據等( 通常須要拿到短信模板ID、APPID、發送連接便可 ),通常費用在每條0.04元左右,比較大的平臺天天須要發送幾千、甚至上萬條,可見天天總的短信驗證碼也是有必定的成本的,這還排除了存在惡意刷驗證碼狀況。所以在後臺編寫驗證碼部分代碼時,必定要加上發送的數量限制,好比:每一個手機號天天最多發送條數(好比最多6條)、每一個IP地址天天最多發送條數等,這樣即便是遇到惡意刷驗證碼的狀況也能夠保證短信數量不至於過多。vue

 

這篇文章是按照生產環境來寫的,代碼部分包含了真實的驗證碼發送部分,只要配置好相應的短信平臺參數就能夠實現發送驗證碼。這篇文章做爲學習研究用,特地將驗證碼 alert() 處來。ios

 

效果圖:ajax

   
 彈出六位數驗證碼,而後發送驗證碼按鈕顯示1分鐘倒計時,控制檯打印後端發來的返回信息。正則表達式

 

 

 主要部分:數據庫

1> 添加發送驗證碼的條數限制,每一個IP天天最多發送10條、每一個手機號天天最多發送6條,axios

2> 驗證碼用 Math.random() 、Math.floor()  生成隨機六位數,後端

3> 用到 koa-session 將生成的驗證碼存入session,等到用戶點擊註冊按鈕時,將用戶輸入的驗證碼與生成的驗證碼進行比對,api

 

1、前端部分服務器

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、後端部分

 

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}).sort({_id: -1}).limit(1);
      // 同一天,同一個 ip 文檔條數
      let clientIpCount = await MobilePhoneModel.find({clientIp, curDate}).countDocuments();
      
      if(mobilePhoneDoc) {
        // 說明次數未到到限制,可繼續發送
        if(mobilePhoneDoc.sendCount < smsSendMax && clientIpCount < ipCountMax) {
          let sendCount = mobilePhoneDoc.sendCount + 1;
          //將發送記錄添加到數據庫中
          let newmobilePhone = new MobilePhoneModel({mobilePhone: mobilePhone, clientIp: clientIp, sendCount: sendCount, curDate: curDate});
          let res = await newmobilePhone.save();
          // 執行發送短信驗證碼
          // 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文檔,能夠找到每一種返回狀態碼對應的說明,並根據狀態碼設定不一樣的返回狀態解釋。

 

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

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

相關文章
相關標籤/搜索