短信相關服務
相關廠商
- 聚合數據: 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等表來記錄,這樣後期可以更安全的監控和編程,同樣的,這也是要看業務需求了