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等表来记录,这样后期可以更安全的监控和编程,同样的,这也是要看业务需求了

猜你喜欢

转载自blog.csdn.net/Tyro_java/article/details/106534651