Authored by 李奇

发送短信接口添加频率和次数限制

/* eslint no-unused-vars: ["error", { "args": "none" }] */
/**
* 登录
* @author: Bi Kai<kai.bi@yoho.cn>
* @date: 2016/05/09
*/
'use strict';
const _ = require('lodash');
const passport = require('passport');
// const md5 = require('yoho-md5');
const uuid = require('uuid');
const co = Promise.coroutine;
const cookie = global.yoho.cookie;
const helpers = global.yoho.helpers;
const log = global.yoho.logger;
const config = global.yoho.config;
const cache = global.yoho.cache;
const utils = require(global.utils);
const RegService = require('../models/reg-service');
const AuthHelper = require('../models/auth-helper');
const loginPage = `${config.siteUrl}/signin.html`;
function doPassportCallback(openId, nickname, sourceType, req, res) {
let shoppingKey = cookie.getShoppingKey(req);
let refer = req.cookies.refer;
if (refer) {
refer = decodeURI(req.cookies.refer);
} else {
refer = `${config.siteUrl}/home`;
}
if (/signin|login/.test(refer)) {
refer = `${config.siteUrl}/home`;
}
refer = utils.refererLimit(refer);
if (openId && nickname) {
return AuthHelper.signinByOpenID(nickname, openId, sourceType, shoppingKey).then((result) => {
if (result.code !== 200) {
return Promise.reject(result);
}
if (result.data['is_bind'] && result.data['is_bind'] === 'N') { //eslint-disable-line
return helpers.urlFormat('/passport/bind/index', {
openId: openId,
sourceType: sourceType,
refer: refer
});
} else if (result.code === 200 && result.data.uid) {
return AuthHelper.syncUserSession(result.data.uid, req, res, result.data.session_key).then(() => {
return refer;
});
}
}).then((redirectTo) => {
return res.redirect(redirectTo);
});
} else {
return Promise.reject('missing third party login openId or nickname');
}
}
const common = {
beforeLogin: (req, res, next) => {
if (req.session.passwordWeak) {
return res.redirect('/passport/password/resetpage');
}
let refer = req.query.refer;
if (!refer) {
refer = req.get('Referer') || req.cookies.refer;
}
refer = utils.refererLimit(refer);
refer && !/signin|login|passport/.test(refer) && res.cookie('refer', encodeURI(refer), {
domain: 'yohobuy.com'
});
if (req.yoho.isApp) {
return next({
code: 401,
message: 'weblogin',
refer
});
}
next();
},
weixinCheck: (req, res, next) => {
let passLogin = _.get(req, 'cookies._WX_PASS_LOGIN', false);
if (req.yoho.isWechat && !passLogin) {
return res.redirect(helpers.urlFormat('/passport/login/wechat', {
refer: req.query.refer || req.get('Referer') || '/'
}));
}
next();
},
clearCookie: (req, res, next) => {
res.clearCookie('_SESSION_KEY', {
domain: 'yohobuy.com'
});
res.clearCookie('_UID', {
domain: 'yohobuy.com'
});
res.clearCookie('_TOKEN', {
domain: 'yohobuy.com'
});
if (req.session2 && req.session2.reset) {
req.session2.reset();
}
if (req.session && req.session.regenerate) {
return req.session.regenerate(() => {
return next();
});
}
},
isLoginUser: (req, res, next) => {
// 微信里边已经登录的时候,不再跳转登录
if (req.user.uid) {
AuthHelper.profile(req.user.uid).then(function(result) {
if (result.code !== 200) {
return next();
}
let refer = req.query.refer || decodeURI(req.cookies.refer) || config.siteUrl;
if (/sign|login/.test(refer)) {
refer = `${config.siteUrl}/home`;
}
refer = utils.refererLimit(refer);
return res.redirect(refer);
}).catch(() => {
return next();
});
} else {
return next();
}
},
check: (req, res, next) => {
let refer = req.query.refer;
// 短信推广的链接强制检查登录
if (req.user.uid) {
AuthHelper.profile(req.user.uid).then(function(result) {
if (result && result.code === 200) {
return res.redirect(refer);
}
return res.redirect(helpers.urlFormat('/signin.html', {
refer: refer
}));
}).catch(() => {
return res.redirect(helpers.urlFormat('/signin.html', {
refer: refer
}));
});
} else {
return res.redirect(helpers.urlFormat('/signin.html', {
refer: refer
}));
}
}
};
const local = {
loginPage: (req, res) => {
// 是否关闭账号登录
let closePassword = _.get(req.app.locals.wap, 'close.passwordLogin', false);
if (closePassword) {
return res.redirect(`/signin.html?refer=${req.query.refer || ''}`);
}
if (req.session.captchaValidCount == null) { // eslint-disable-line
req.session.captchaValidCount = 5;
}
res.render('login', {
width750: true,
loginIndex: true, // 模板中使用JS的标识
captchaShow: req.yoho.captchaShow,
backUrl: 'javascript:history.go(-1)', // eslint-disable-line
showHeaderImg: true, // 控制显示头部图片
isPassportPage: true, // 模板中模块标识
smsLoginUrl: '/passport/sms_login',
registerUrl: '/passport/reg/index', // 注册的URL链接
aliLoginUrl: '/passport/login/alipay', // 支付宝快捷登录的URL链接
weiboLoginUrl: '/passport/login/sina', // 微博登录的URL链接
qqLoginUrl: '/passport/login/qq', // 腾讯QQ登录的URL链接
wechatLoginUrl: '/passport/login/wechat', // 微信登录的URL链接
internationalUrl: '/passport/international', // 国际号登录的URL链接
phoneRetriveUrl: '/passport/back/mobile', // 通过手机号找回密码的URL链接
emailRetriveUrl: '/passport/back/email', // 通过邮箱找回密码的URL链接
module: 'passport',
page: 'login',
title: '登录',
reg: true
});
},
international: (req, res) => {
// 是否关闭账号登录
let closePassword = _.get(req.app.locals.wap, 'close.passwordLogin', false);
if (closePassword) {
return res.redirect(`/signin.html?refer=${req.query.refer || ''}`);
}
if (req.session.captchaValidCount == null) { // eslint-disable-line
req.session.captchaValidCount = 5;
}
res.render('international', {
width750: true,
backUrl: 'javascript:history.go(-1)', // eslint-disable-line
loginInternational: true, // 模板中使用JS的标识
captchaShow: req.yoho.captchaShow,
isPassportPage: true, // 模板中模块标识
headerText: '登录',
areaCode: '+86', // 默认区号
countrys: RegService.getAreaData(), // 地区信息列表
module: 'passport',
page: 'international',
title: '国际账号登录'
});
},
login: (req, res, next) => {
// 是否关闭账号登录
let closePassword = _.get(req.app.locals.wap, 'close.passwordLogin', false);
if (closePassword) {
return res.json({
code: 403,
message: '请使用快速登录'
});
}
passport.authenticate('local', (err, user) => {
if (err || !user) {
if (err.code === 4189) {
let obj = {
code: 4189,
message: err || '登录出错请重试',
url: '//m.yohobuy.com/passport/sms_login'
};
return res.json(obj);
} else {
let obj = {
code: 400,
message: err || '登录出错请重试',
data: '',
captchaShow: true
};
cache.set(`loginErrorIp:${req.yoho.clientIp}`, true, 3600).catch(log.error);
return res.json(obj);
}
} else {
let refer = req.cookies.refer;
if (refer) {
refer = decodeURI(req.cookies.refer);
} else {
refer = `${config.siteUrl}/home`;
}
if (/sign|login/.test(refer)) {
refer = `${config.siteUrl}/home`;
}
refer = utils.refererLimit(refer);
user.session = refer;
user.href = refer;
res.cookie('_LOGIN_TYPE', 0, {
domain: 'm.yohobuy.com'
});
// 弱密码返回数据
let passwordWeakReturn;
if (user.weakPassword) {
req.session.passwordWeak = user;
passwordWeakReturn = {
code: 510,
url: '/passport/password/resetpage',
pwdTip: _.get(user, 'pwdTip', '密码应为6-20位字母、数字的组合'),
uid: _.get(user, 'uid', '')
};
}
// 不可以跳过,不登录用户
if (user.weakPassword && user.canSkip !== 'Y') {
return res.json(passwordWeakReturn);
}
AuthHelper.syncUserSession(user.uid, req, res, user.session_key).then(() => {
if (user.weakPassword) {
return res.json(passwordWeakReturn);
} else {
res.json({
code: 200,
data: user
});
}
}).catch(next);
}
})(req, res, next);
},
logout: (req, res) => {
res.clearCookie('_SPK');
res.cookie('_WX_PASS_LOGIN', true, {
domain: 'm.yohobuy.com'
});
let refer = req.get('Referer') || config.siteUrl;
refer = utils.refererLimit(refer);
res.redirect(refer);
}
};
const wechat = {
login: (req, res, next) => {
// 设置为原链接标识originalUrl
req.session.originalUrl = 'true';
req.session.authState = uuid.v4();
res.clearCookie('_WX_PASS_LOGIN', {
domain: 'm.yohobuy.com'
});
return passport.authenticate('weixin', {
state: req.session.authState
})(req, res, next);
},
callback: (req, res, next) => {
if (req.session && req.session.authState && req.session.authState === req.query.state) {
passport.authenticate('weixin', (err, user) => {
if (err || !user) {
log.error(`wechat authenticate error : ${JSON.stringify(err)}`);
return res.redirect(loginPage);
}
let nickname = user._json.nickname || user.displayName;
let openId = user._json.unionid || user.id;
res.cookie('_WX_OPENID', _.get(user, '_json.openid'), {
domain: 'm.yohobuy.com'
});
res.cookie('_WX_UNIONID', _.get(user, '_json.unionid'), {
domain: 'm.yohobuy.com'
});
res.cookie('_LOGIN_TYPE', 4, {
domain: 'm.yohobuy.com'
});
doPassportCallback(openId, nickname, 'wechat', req, res).catch(next);
})(req, res, next);
} else {
log.error('Auth State Mismatch:' + req.originalUrl);
return res.redirect(loginPage);
}
}
};
const sina = {
login: (req, res, next) => {
// 设置为原链接标识originalUrl
req.session.originalUrl = 'true';
req.session.authState = uuid.v4();
return passport.authenticate('sina', {
state: req.session.authState
})(req, res, next);
},
callback: (req, res, next) => {
if (req.session && req.session.authState && req.session.authState === req.query.state) {
passport.authenticate('sina', (err, user) => {
if (err || !user) {
log.error(`sina authenticate error : ${JSON.stringify(err)}`);
return res.redirect(loginPage);
}
let nickname = user.screen_name;
let openId = user.id;
res.cookie('_LOGIN_TYPE', 2, {
domain: 'm.yohobuy.com'
});
doPassportCallback(openId, nickname, 'sina', req, res).catch(next);
})(req, res, next);
} else {
log.error('Auth State Mismatch:' + req.originalUrl);
return res.redirect(loginPage);
}
}
};
const qq = {
login: (req, res, next) => {
// 设置为原链接标识originalUrl
req.session.originalUrl = 'true';
req.session.authState = uuid.v4();
return passport.authenticate('qq', {
state: req.session.authState
})(req, res, next);
},
callback: (req, res, next) => {
if (req.session && req.session.authState && req.session.authState === req.query.state) {
passport.authenticate('qq', (err, user) => {
if (err) {
log.error(`qq authenticate error : ${JSON.stringify(err)}`);
return res.redirect(loginPage);
}
let nickname = user.nickname;
let openId = user.id;
res.cookie('_LOGIN_TYPE', 1, {
domain: 'm.yohobuy.com'
});
doPassportCallback(openId, nickname, 'qq', req, res).catch(next);
})(req, res, next);
} else {
log.error('Auth State Mismatch:' + req.originalUrl);
return res.redirect(loginPage);
}
}
};
const alipay = {
login: (req, res, next) => {
// 设置为原链接标识originalUrl
req.session.originalUrl = 'true';
return passport.authenticate('alipay')(req, res, next);
},
callback: (req, res, next) => {
passport.authenticate('alipay', (err, user) => {
if (err || !user) {
log.error(`alipay authenticate error : ${JSON.stringify(err)}`);
return res.redirect(loginPage);
}
let nickname = user.realName;
let openId = user.userId;
res.cookie('_LOGIN_TYPE', 3, {
domain: 'm.yohobuy.com'
});
doPassportCallback(openId, nickname, 'alipay', req, res).catch(next);
})(req, res, next);
}
};
exports.user = function(req, res, next) {
let result = {
code: 403,
message: '未登录',
data: ''
};
if (req.user.uid) {
result.code = 200;
result.message = '已登录';
result.data = req.user.uid.toString();
}
res.jsonp(result);
};
/**
* 中间件
* 根据用户登录是否成功决定是否展示验证码
*/
exports.loginShowCaptchaByIp = function(req, res, next) {
// 总开关状态
req.yoho.captchaShow = !_.get(req.app.locals.wap, 'close.loginValidation', false);
// 开关打开,不走任何验证逻辑
if (!req.yoho.captchaShow) {
return next();
} else {
req.yoho.captchaShow = false;
}
co(function*() {
let hasErrorLog = yield cache.get(`loginErrorIp:${req.yoho.clientIp}`);
log.info(`Pagerender clientip ${req.yoho.clientIp} status is ` + hasErrorLog);
if (hasErrorLog) {
req.yoho.captchaShow = true;
}
next();
})().catch(function(e) {
req.yoho.captchaShow = true;
next();
});
};
exports.common = common;
exports.local = local;
exports.wechat = wechat;
exports.sina = sina;
exports.qq = qq;
exports.alipay = alipay;
... ... @@ -5,10 +5,43 @@
*/
'use strict';
const _ = require('lodash');
const moment = require('moment');
const EventEmitter = require('events');
const SmsModel = require('../models/sms');
const smsController = {
/**
* 发送短信前
* @param req
* @param res
* @param next
* @returns {*}
*/
beforeSend(req, res, next) {
const count = _.get(req.session, 'smsSend.count');
const interval = _.get(req.session, 'smsSend.interval', 0);
// 重发次数用完了, 冻结5min
// 过了冻结期, count重设为5次
// 没有用完, 判断是否请求太频繁
const now = Date.now();
const during = moment.duration(interval - now, 'ms').minutes();
const msg = {
code: 429,
message: `请${during || 1}分钟后再试`
};
if (interval > now) {
return res.json(msg);
}
// 重置可发送次数
!count && _.set(req.session, 'smsSend.count', 5);
next();
},
/**
* 发送短信验证码
* @param req
... ... @@ -17,17 +50,26 @@ const smsController = {
*/
sendCode(req, res) {
const em = new EventEmitter();
const area = (req.body.area || '').trim();
const area = (req.body.area || '86').trim();
const mobile = (req.body.mobile || '').trim();
const inValid = [area, mobile].some(v => v === '');
em.on('resolve', () => {
_.set(req.session, 'smsLogin.area', area);
_.set(req.session, 'smsLogin.mobile', mobile);
req.ctx(SmsModel).sendSMS(mobile, area, 1)
.then(result => {
if (result.code === 200) {
_.set(req.session, 'smsSend.area', area);
_.set(req.session, 'smsSend.mobile', mobile);
// 剩余次数
--req.session.smsSend.count;
if (!req.session.smsSend.count) {
_.set(req.session, 'smsSend.interval', Date.now() + 5 * 60 * 1000);
} else {
_.set(req.session, 'smsSend.interval', Date.now() + 60 * 1000);
}
return res.json({
code: 200,
message: '验证码发送成功'
... ... @@ -64,8 +106,8 @@ const smsController = {
*/
checkCode(req, res, next) {
const code = (req.body.code || '').trim();
const area = _.get(req.session, 'smsLogin.area', '');
const mobile = _.get(req.session, 'smsLogin.mobile', '');
const area = _.get(req.session, 'smsSend.area', '');
const mobile = _.get(req.session, 'smsSend.mobile', '');
req.ctx(SmsModel).verifySMS(mobile, area, code, 1)
.then(result => {
... ...
... ... @@ -3,16 +3,14 @@
* @author: leo <qi.li@yoho.cn>
* @date: 2017/06/26
*/
'use strict';
const express = require('express');
const sms = require('./controllers/sms');
const user = require('./controllers/user');
const router = express.Router(); // eslint-disable-line
// SMS 短信
router.post('/sms/sendCode', sms.sendCode);
router.post('/sms/sendCode', sms.beforeSend, sms.sendCode);
router.post('/sms/checkCode', sms.checkCode, user.userInfo);
module.exports = router;
... ...