短信相关服务
相关厂商
- 聚合数据: 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等表来记录,这样后期可以更安全的监控和编程,同样的,这也是要看业务需求了