Eggjs筆記:用戶註冊之短信相關功能

短信相關服務

相關廠商

  • 聚合數據: https://www.juhe.cn/service
  • 雲片: https://www.yunpian.com/
  • 阿里雲,騰訊,百度等都有提供短信接口選擇適合自己的一款

一般性接入流程

  • 這裏用雲片的服務來做說明, https://www.yunpian.com/
  • 註冊賬戶並實名認證
  • 後臺管理系統的控制檯中創建簽名, 簽名顯示在短信內容的最前面,顯示這條短信來自哪家公司/產品/網站。因運營商要求,簽名需經過審覈。
  • 後臺管理系統的控制檯中創建模板
  • 找sdk以及官方文檔實現發送短信,找到相關apikey在配置文件中填入

用戶註冊流程

  • 填入手機號,圖形驗證碼,進行前端校驗程序
  • 發送手機驗證碼,服務器生成驗證碼,並調用短信服務傳入驗證碼,驗證碼發送到用戶手機
  • 用戶手機驗證碼和服務器驗證碼做比較,成功則進入下一步,否則提示錯誤信息
  • 設置賬戶密碼,服務器端生成一條用戶數據
  • 備註:一般發送短信,發送郵件等功能會分解到RabbitMQ等異步框架來實現,要看業務需求來做具體設計了

相關代碼實現(只摘取關鍵代碼展示)

首先進行全局配置

// 發送短信的 apiKey
config.sendMsg = {
    yunApiKey: '', // 填入運營商提供的key (項目使用的是雲片,當然後期可以切換多個, 在後面配置多個即可)
    enable: false, // 是否開啓發送短信,默認否,調試模式,開啓後將走發送短信的流程
}

封裝相關service服務 app/service/*.js

sendmsg.js

'use strict';

const https = require('https');
const qs = require('querystring');
const Service = require('egg').Service;

class SendMsgService extends Service {
    async yunpianSend(mobile, code) {
        let apikey = this.config.sendMsg.yunApiKey;
        // 修改爲您要發送的短信內容
        let text = '【這裏填寫您的業務品牌】您的驗證碼是: ' + code + ', 爲了您的賬戶安全,請不要泄露給其他人,有效時間爲10分鐘, 請儘快驗證';
        // 智能匹配模板發送https地址
        let sms_host = 'sms.yunpian.com';
        let send_sms_uri = '/v2/sms/single_send.json';
        // 指定模板發送接口https地址
        send_sms(send_sms_uri, apikey, mobile, text);

        function send_sms(uri, apikey, mobile, text) {
            let post_data = {
                'apikey': apikey,
                'mobile': mobile,
                'text': text,
            }; //這是需要提交的數據  
            let content = qs.stringify(post_data);
            post(uri, content, sms_host);
        }

        function post(uri, content, host) {
            let options = {
                hostname: host,
                port: 443,
                path: uri,
                method: 'POST',
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
                }
            };
            let req = https.request(options, function(res) {
                // console.log('STATUS: ' + res.statusCode);  
                // console.log('HEADERS: ' + JSON.stringify(res.headers));  
                res.setEncoding('utf8');
                res.on('data', function(chunk) {
                    console.log('BODY: ' + chunk); // 如果出現錯誤,可以嘗試自己把它寫入一個日誌或者記錄至數據庫
                });
            });
            //console.log(content);
            req.write(content);
            req.end();
        }
    }
}

module.exports = SendMsgService;

tools.js

'use strict';

const svgCaptcha = require('svg-captcha'); // 引入驗證
const md5 = require('md5');
const sd = require('silly-datetime');
const Service = require('egg').Service;

class ToolsService extends Service {

    // 生成驗證碼
    async captcha(w, h) {
        let width = w ? w : 100;
        let height = h ? h : 32;
        const captcha = svgCaptcha.create({
            size: 4,
            fontSize: 50,
            width: width,
            height: height,
            background: '#cc9966',
        });
        return captcha;
    }

    // md5 加密 三次
    md5(str) {
        return md5(md5(md5(str)));
    }

    // 獲取當前時間戳
    getTime() {
        let d = new Date();
        return d.getTime();
    }

    // 獲取當前日期
    getDay() {
        let day = sd.format(new Date(), 'YYYYMMDD');
        return day;
    }

    // 獲取隨機數字 建議傳遞4或6 用於短信驗證碼
    getRandomNum(num) {
        if (typeof num !== 'number') {
            return -1;
        }
        let random_str = '';
        for (let i = 0; i < num; i++) {
            random_str += Math.floor(Math.random() * 10);
        }
        return random_str;
    }
}

module.exports = ToolsService;

你可以看到這樣配置的原因是,如果後期更換服務商,直接在config中配置和在這個service中添加新的方法即可,達到開箱即用的效果

相關路由配置

router.get('/user/registerStep1', webMiddleware, controller.web.user.registerStep1);
router.get('/user/sendCode', webMiddleware, controller.web.user.sendCode);
router.get('/user/registerStep2', webMiddleware, controller.web.user.registerStep2);
router.get('/user/validatePhoneCode', webMiddleware, controller.web.user.validatePhoneCode);
router.get('/user/registerStep3', webMiddleware, controller.web.user.registerStep3);
router.post('/user/doRegister', webMiddleware, controller.web.user.doRegister);

相關model app/model/*.js

user_temp.js

'use strict';

/* 臨時用戶表主要保存 用戶的手機, 當天發送了幾次驗證碼, 也就是當前的簽名(手機+日期),是否存在,
存在要判斷當天發送了幾次驗證碼, 不存在,保存新的 
還有,是否有必要保存當前驗證碼, 這個可以根據需求來做,理論上最好保存一下的 TODO
*/
module.exports = app => {
    const mongoose = app.mongoose;
    const Schema = mongoose.Schema;

    var d = new Date();
    const UserTemp = new Schema({
        phone: { type: Number },
        send_count: { type: Number },
        sign: { type: String },
        add_day: {
            type: Number
        },
        ip: { type: String },
        add_time: {
            type: Number,
            default: d.getTime()
        }
    });

    return mongoose.model('UserTemp', UserTemp, 'user_temp');
}

user.js

'use strict';

module.exports = app => {
    const mongoose = app.mongoose;
    const Schema = mongoose.Schema;

    var d = new Date();
    const User = new Schema({
        password: { type: String },
        phone: { type: Number },
        last_ip: { type: String },
        add_time: {
            type: Number,
            default: d.getTime()
        },
        email: { type: String },
        status: {
            type: Number,
            default: d.getTime()
        }
    });

    return mongoose.model('User', User, 'user');
}

可以看到這裏用戶相關設計了2張表,一個是臨時表,就是還沒有註冊成功的,一個是真正的用戶表,這個需要根據具體的業務來進行設計

相關控制器的實現 app/controller/user.js

// 註冊第一步
async registerStep1() {
    await this.ctx.render('web/user/register_step1.html');
}

// 第一步發送短信驗證碼
async sendCode() {
    let phone = this.ctx.request.query.phone;
    let web_code = this.ctx.request.query.web_code; //用戶輸入的驗證碼
    if (web_code !== this.ctx.session.web_code) {
        this.ctx.body = {
            success: false,
            msg: '輸入的圖形驗證碼不正確'
        }
    } else {
        //判斷手機格式是否合法
        let reg = /^[\d]{11}$/;
        if (!reg.test(phone)) {
            this.ctx.body = {
                success: false,
                msg: '手機號不合法'
            }
        } else {
            let add_day = this.service.tools.getDay(); //年月日    
            let sign = this.service.tools.md5(phone + '_' + add_day); //簽名
            // 根據調試功能是否開啓來具體是否發送短信驗證
            let isSendMsgEnable = this.config.sendMsg.enable;
            if (!isSendMsgEnable) {
                this.ctx.session.phone_code = '1234';
                return this.ctx.body = {
                    success: true,
                    msg: '調試環境-默認手機驗證碼爲:1234',
                    sign: sign
                };
            }

            let ip = this.ctx.request.ip.replace(/::ffff:/, ''); //獲取客戶端ip         
            let phone_code = this.service.tools.getRandomNum(4); // 發送短信的隨機碼    
            let userTempResult = await this.ctx.model.UserTemp.find({ "sign": sign, add_day: add_day });
            //1個ip 一天只能發10個手機號
            let ipCount = await this.ctx.model.UserTemp.find({ "ip": ip, add_day: add_day }).count();
            if (userTempResult.length) {
                const userTemper = userTempResult[0];
                if (userTemper.send_count < 6 && ipCount < 10) { // 執行發送
                    let send_count = userTemper.send_count + 1;
                    await this.ctx.model.UserTemp.updateOne({ "_id": userTemper._id }, { "send_count": send_count, "add_time": this.service.tools.getTime() });

                    //發送短信
                    // this.service.sendCode.send(phone,'隨機驗證碼')
                    this.ctx.session.phone_code = phone_code;
                    // console.log('---------------------------------')
                    // console.log(phone_code, ipCount);

                    this.ctx.body = {
                        success: true,
                        msg: '短信發送成功',
                        sign: sign
                    }
                } else {
                    this.ctx.body = { "success": false, msg: '當前手機號碼發送次數達到上限,明天重試' };
                }
            } else {
                let userTmep = new this.ctx.model.UserTemp({
                    phone,
                    add_day,
                    sign,
                    ip,
                    send_count: 1
                });
                userTmep.save();

                //發送短信
                // this.service.sendCode.send(phone,'隨機驗證碼')  
                this.ctx.session.phone_code = phone_code;
                this.ctx.body = {
                    success: true,
                    msg: '短信發送成功',
                    sign: sign
                }
            }
        }
    }
}

//註冊第二步  驗證碼驗證碼是否正確
async registerStep2() {
    let sign = this.ctx.request.query.sign;
    let web_code = this.ctx.request.query.web_code;
    let phone = this.ctx.request.query.phone; // 這裏只用作調試模式下的參數
    let isSendMsgEnable = this.config.sendMsg.enable;
    if (!isSendMsgEnable) {
        return await this.ctx.render('web/user/register_step2.html', {
            sign,
            phone,
            web_code
        });
    }
    // 正常的查詢驗證流程
    let add_day = await this.service.tools.getDay(); //年月日   
    let userTempResult = await this.ctx.model.UserTemp.find({ "sign": sign, add_day: add_day });
    if (!userTempResult.length) {
        this.ctx.redirect('/user/registerStep1'); // 不存在則跳轉回去
    } else {
        await this.ctx.render('web/user/register_step2.html', {
            sign,
            phone: userTempResult[0].phone,
            web_code
        });
    }
}

//驗證驗證碼
async validatePhoneCode() {
    let phone_code = this.ctx.request.query.phone_code;
    if (this.ctx.session.phone_code != phone_code) {
        this.ctx.body = {
            success: false,
            msg: '您輸入的手機驗證碼錯誤'
        }
    } else {
        let sign = this.ctx.request.query.sign;
        let isSendMsgEnable = this.config.sendMsg.enable;
        if (!isSendMsgEnable) {
            return this.ctx.body = {
                success: true,
                msg: '驗證碼輸入正確',
                sign
            }
        }
        // 正常的查詢校驗流程
        let add_day = await this.service.tools.getDay(); //年月日   
        let userTempResult = await this.ctx.model.UserTemp.find({ "sign": sign, add_day: add_day });
        if (!userTempResult.length) {
            this.ctx.body = {
                success: false,
                msg: '參數錯誤'
            }
        } else {
            //判斷驗證碼是否超時
            let nowTime = await this.service.tools.getTime();
            // 超過30分鐘了,那麼驗證碼過期
            if ((userTempResult[0].add_time - nowTime) / 1000 / 60 > 30) {
                this.ctx.body = {
                    success: false,
                    msg: '驗證碼已經過期'
                }
            } else {
                // 用戶表有沒有當前這個手機號 手機號有沒有註冊
                let userResult = await this.ctx.model.User.find({ "phone": userTempResult[0].phone });
                if (userResult.length) {
                    this.ctx.body = {
                        success: false,
                        msg: '此用戶已經存在'
                    }
                } else {
                    this.ctx.body = {
                        success: true,
                        msg: '驗證碼輸入正確',
                        sign
                    }
                }
            }
        }
    }
}

//註冊第三步  輸入密碼
async registerStep3() {
    let isSendMsgEnable = this.config.sendMsg.enable;
    let sign = this.ctx.request.query.sign;
    let phone_code = this.ctx.request.query.phone_code;
    let phone = this.ctx.request.query.phone; // 用於調試
    let msg = this.ctx.request.query.msg || '';
    // 調試狀態下的處理
    if (!isSendMsgEnable) {
        return await this.ctx.render('web/user/register_step3.html', {
            sign,
            phone_code,
            phone,
            msg
        });
    }
    // 正常流程
    let add_day = await this.service.tools.getDay(); //年月日   
    let userTempResult = await this.ctx.model.UserTemp.find({ "sign": sign, add_day: add_day });
    if (!userTempResult.length) {
        this.ctx.redirect('/user/registerStep1');
    } else {
        await this.ctx.render('web/user/register_step3.html', {
            sign: sign,
            phone_code: phone_code,
            msg: msg
        });
    }
}

//完成註冊 post
async doRegister() {
    let isSendMsgEnable = this.config.sendMsg.enable;
    let sign = this.ctx.request.body.sign;
    let phone_code = this.ctx.request.body.phone_code;
    let phone = this.ctx.request.body.phone;
    let add_day = await this.service.tools.getDay(); //年月日       
    let password = this.ctx.request.body.password;
    let rpassword = this.ctx.request.body.rpassword;
    let ip = this.ctx.request.ip.replace(/::ffff:/, '');

    if (this.ctx.session.phone_code != phone_code) {
        //非法操作
        return this.ctx.redirect('/user/registerStep1');
    }

    let userTempResult = await this.ctx.model.UserTemp.find({ "sign": sign, add_day: add_day });

    if (isSendMsgEnable && !userTempResult.length) {
        //非法操作
        this.ctx.redirect('/user/registerStep1');
    } else {
        //傳入參數正確 執行增加操作
        if (password.length < 6 || password != rpassword) {
            let msg = '密碼不能小於6位並且密碼和確認密碼必須一致';
            this.ctx.redirect('/user/registerStep3?sign=' + sign + '&phone_code=' + phone_code + '&msg=' + msg);
        } else {
            // 做的更安全的一些做法是將用戶, 管理員登錄的信息保存在一個用戶表中, 登錄用戶名,時間,錯誤密碼等存入新的user_log, admin_log表中 TODO
            // 處理調試環境下的一些問題
            phone = isSendMsgEnable ? userTempResult[0].phone : phone;
            console.log('phone: ', phone);
            let userModel = new this.ctx.model.User({
                    phone,
                    password: this.service.tools.md5(password),
                    last_ip: ip
                })
                //保存用戶
            let userReuslt = await userModel.save();
            if (userReuslt) {
                //獲取用戶信息
                let userinfo = await this.ctx.model.User.find({ phone }, '_id phone last_ip add_time email status')
                    //用戶註冊成功以後默認登錄
                    //cookies 安全 加密
                this.service.cookies.set('userinfo', userinfo[0]);
                this.ctx.redirect('/');
            }
        }
    }
}

可以看到這裏使用了三個頁面來進行處理,理論上爲了用戶體驗,在一個頁面實現即可,可以通過不同的請求參數來代表第幾步,比如:/user/register?step=1 從而進行顯示和隱藏dom或者做一些動畫效果處理,這樣是一種更好的處理方式,上面的代碼只是用來展示了一些流程信息

注意:上面的web_code是圖形驗證碼,phone_code是手機驗證碼, 短信次數限制要具體參考服務商是一個什麼樣的情況,以及自己公司的具體業務了

註冊之後就是登錄了,關於登錄其實還有很多有意思的東西, 比如做一些前臺用戶和後臺管理員用戶的登錄日誌,建立user_log, admin_log等表來記錄,這樣後期可以更安全的監控和編程,同樣的,這也是要看業務需求了

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章