Authored by 李奇

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

1 -/* eslint no-unused-vars: ["error", { "args": "none" }] */  
2 -/**  
3 - * 登录  
4 - * @author: Bi Kai<kai.bi@yoho.cn>  
5 - * @date: 2016/05/09  
6 - */  
7 -'use strict';  
8 -const _ = require('lodash');  
9 -const passport = require('passport');  
10 -  
11 -// const md5 = require('yoho-md5');  
12 -const uuid = require('uuid');  
13 -const co = Promise.coroutine;  
14 -const cookie = global.yoho.cookie;  
15 -const helpers = global.yoho.helpers;  
16 -const log = global.yoho.logger;  
17 -const config = global.yoho.config;  
18 -const cache = global.yoho.cache;  
19 -const utils = require(global.utils);  
20 -const RegService = require('../models/reg-service');  
21 -const AuthHelper = require('../models/auth-helper');  
22 -  
23 -const loginPage = `${config.siteUrl}/signin.html`;  
24 -  
25 -function doPassportCallback(openId, nickname, sourceType, req, res) {  
26 - let shoppingKey = cookie.getShoppingKey(req);  
27 - let refer = req.cookies.refer;  
28 -  
29 - if (refer) {  
30 - refer = decodeURI(req.cookies.refer);  
31 - } else {  
32 - refer = `${config.siteUrl}/home`;  
33 - }  
34 -  
35 - if (/signin|login/.test(refer)) {  
36 - refer = `${config.siteUrl}/home`;  
37 - }  
38 -  
39 - refer = utils.refererLimit(refer);  
40 -  
41 - if (openId && nickname) {  
42 - return AuthHelper.signinByOpenID(nickname, openId, sourceType, shoppingKey).then((result) => {  
43 - if (result.code !== 200) {  
44 - return Promise.reject(result);  
45 - }  
46 - if (result.data['is_bind'] && result.data['is_bind'] === 'N') { //eslint-disable-line  
47 - return helpers.urlFormat('/passport/bind/index', {  
48 - openId: openId,  
49 - sourceType: sourceType,  
50 - refer: refer  
51 - });  
52 - } else if (result.code === 200 && result.data.uid) {  
53 - return AuthHelper.syncUserSession(result.data.uid, req, res, result.data.session_key).then(() => {  
54 - return refer;  
55 - });  
56 - }  
57 - }).then((redirectTo) => {  
58 - return res.redirect(redirectTo);  
59 - });  
60 - } else {  
61 - return Promise.reject('missing third party login openId or nickname');  
62 - }  
63 -}  
64 -  
65 -const common = {  
66 - beforeLogin: (req, res, next) => {  
67 - if (req.session.passwordWeak) {  
68 - return res.redirect('/passport/password/resetpage');  
69 - }  
70 -  
71 - let refer = req.query.refer;  
72 -  
73 - if (!refer) {  
74 - refer = req.get('Referer') || req.cookies.refer;  
75 - }  
76 -  
77 - refer = utils.refererLimit(refer);  
78 -  
79 - refer && !/signin|login|passport/.test(refer) && res.cookie('refer', encodeURI(refer), {  
80 - domain: 'yohobuy.com'  
81 - });  
82 - if (req.yoho.isApp) {  
83 - return next({  
84 - code: 401,  
85 - message: 'weblogin',  
86 - refer  
87 - });  
88 - }  
89 - next();  
90 - },  
91 - weixinCheck: (req, res, next) => {  
92 - let passLogin = _.get(req, 'cookies._WX_PASS_LOGIN', false);  
93 -  
94 - if (req.yoho.isWechat && !passLogin) {  
95 - return res.redirect(helpers.urlFormat('/passport/login/wechat', {  
96 - refer: req.query.refer || req.get('Referer') || '/'  
97 - }));  
98 - }  
99 - next();  
100 - },  
101 - clearCookie: (req, res, next) => {  
102 - res.clearCookie('_SESSION_KEY', {  
103 - domain: 'yohobuy.com'  
104 - });  
105 - res.clearCookie('_UID', {  
106 - domain: 'yohobuy.com'  
107 - });  
108 - res.clearCookie('_TOKEN', {  
109 - domain: 'yohobuy.com'  
110 - });  
111 - if (req.session2 && req.session2.reset) {  
112 - req.session2.reset();  
113 - }  
114 - if (req.session && req.session.regenerate) {  
115 - return req.session.regenerate(() => {  
116 - return next();  
117 - });  
118 - }  
119 - },  
120 - isLoginUser: (req, res, next) => {  
121 - // 微信里边已经登录的时候,不再跳转登录  
122 - if (req.user.uid) {  
123 - AuthHelper.profile(req.user.uid).then(function(result) {  
124 - if (result.code !== 200) {  
125 - return next();  
126 - }  
127 - let refer = req.query.refer || decodeURI(req.cookies.refer) || config.siteUrl;  
128 -  
129 - if (/sign|login/.test(refer)) {  
130 - refer = `${config.siteUrl}/home`;  
131 - }  
132 -  
133 - refer = utils.refererLimit(refer);  
134 - return res.redirect(refer);  
135 - }).catch(() => {  
136 - return next();  
137 - });  
138 - } else {  
139 - return next();  
140 - }  
141 - },  
142 - check: (req, res, next) => {  
143 - let refer = req.query.refer;  
144 -  
145 - // 短信推广的链接强制检查登录  
146 - if (req.user.uid) {  
147 - AuthHelper.profile(req.user.uid).then(function(result) {  
148 - if (result && result.code === 200) {  
149 - return res.redirect(refer);  
150 - }  
151 -  
152 - return res.redirect(helpers.urlFormat('/signin.html', {  
153 - refer: refer  
154 - }));  
155 - }).catch(() => {  
156 - return res.redirect(helpers.urlFormat('/signin.html', {  
157 - refer: refer  
158 - }));  
159 - });  
160 - } else {  
161 - return res.redirect(helpers.urlFormat('/signin.html', {  
162 - refer: refer  
163 - }));  
164 - }  
165 - }  
166 -};  
167 -  
168 -  
169 -const local = {  
170 - loginPage: (req, res) => {  
171 - // 是否关闭账号登录  
172 - let closePassword = _.get(req.app.locals.wap, 'close.passwordLogin', false);  
173 -  
174 - if (closePassword) {  
175 - return res.redirect(`/signin.html?refer=${req.query.refer || ''}`);  
176 - }  
177 - if (req.session.captchaValidCount == null) { // eslint-disable-line  
178 - req.session.captchaValidCount = 5;  
179 - }  
180 -  
181 - res.render('login', {  
182 - width750: true,  
183 - loginIndex: true, // 模板中使用JS的标识  
184 - captchaShow: req.yoho.captchaShow,  
185 - backUrl: 'javascript:history.go(-1)', // eslint-disable-line  
186 - showHeaderImg: true, // 控制显示头部图片  
187 - isPassportPage: true, // 模板中模块标识  
188 - smsLoginUrl: '/passport/sms_login',  
189 - registerUrl: '/passport/reg/index', // 注册的URL链接  
190 - aliLoginUrl: '/passport/login/alipay', // 支付宝快捷登录的URL链接  
191 - weiboLoginUrl: '/passport/login/sina', // 微博登录的URL链接  
192 - qqLoginUrl: '/passport/login/qq', // 腾讯QQ登录的URL链接  
193 - wechatLoginUrl: '/passport/login/wechat', // 微信登录的URL链接  
194 - internationalUrl: '/passport/international', // 国际号登录的URL链接  
195 - phoneRetriveUrl: '/passport/back/mobile', // 通过手机号找回密码的URL链接  
196 - emailRetriveUrl: '/passport/back/email', // 通过邮箱找回密码的URL链接  
197 - module: 'passport',  
198 - page: 'login',  
199 - title: '登录',  
200 - reg: true  
201 - });  
202 - },  
203 - international: (req, res) => {  
204 - // 是否关闭账号登录  
205 - let closePassword = _.get(req.app.locals.wap, 'close.passwordLogin', false);  
206 -  
207 - if (closePassword) {  
208 - return res.redirect(`/signin.html?refer=${req.query.refer || ''}`);  
209 - }  
210 - if (req.session.captchaValidCount == null) { // eslint-disable-line  
211 - req.session.captchaValidCount = 5;  
212 - }  
213 -  
214 - res.render('international', {  
215 - width750: true,  
216 - backUrl: 'javascript:history.go(-1)', // eslint-disable-line  
217 - loginInternational: true, // 模板中使用JS的标识  
218 - captchaShow: req.yoho.captchaShow,  
219 - isPassportPage: true, // 模板中模块标识  
220 - headerText: '登录',  
221 - areaCode: '+86', // 默认区号  
222 - countrys: RegService.getAreaData(), // 地区信息列表  
223 - module: 'passport',  
224 - page: 'international',  
225 - title: '国际账号登录'  
226 - });  
227 - },  
228 - login: (req, res, next) => {  
229 - // 是否关闭账号登录  
230 - let closePassword = _.get(req.app.locals.wap, 'close.passwordLogin', false);  
231 -  
232 - if (closePassword) {  
233 - return res.json({  
234 - code: 403,  
235 - message: '请使用快速登录'  
236 - });  
237 - }  
238 -  
239 - passport.authenticate('local', (err, user) => {  
240 - if (err || !user) {  
241 - if (err.code === 4189) {  
242 - let obj = {  
243 - code: 4189,  
244 - message: err || '登录出错请重试',  
245 - url: '//m.yohobuy.com/passport/sms_login'  
246 - };  
247 -  
248 - return res.json(obj);  
249 - } else {  
250 - let obj = {  
251 - code: 400,  
252 - message: err || '登录出错请重试',  
253 - data: '',  
254 - captchaShow: true  
255 - };  
256 -  
257 - cache.set(`loginErrorIp:${req.yoho.clientIp}`, true, 3600).catch(log.error);  
258 - return res.json(obj);  
259 - }  
260 - } else {  
261 - let refer = req.cookies.refer;  
262 -  
263 - if (refer) {  
264 - refer = decodeURI(req.cookies.refer);  
265 - } else {  
266 - refer = `${config.siteUrl}/home`;  
267 - }  
268 -  
269 - if (/sign|login/.test(refer)) {  
270 - refer = `${config.siteUrl}/home`;  
271 - }  
272 -  
273 - refer = utils.refererLimit(refer);  
274 -  
275 - user.session = refer;  
276 - user.href = refer;  
277 - res.cookie('_LOGIN_TYPE', 0, {  
278 - domain: 'm.yohobuy.com'  
279 - });  
280 -  
281 -  
282 - // 弱密码返回数据  
283 - let passwordWeakReturn;  
284 -  
285 - if (user.weakPassword) {  
286 - req.session.passwordWeak = user;  
287 - passwordWeakReturn = {  
288 - code: 510,  
289 - url: '/passport/password/resetpage',  
290 - pwdTip: _.get(user, 'pwdTip', '密码应为6-20位字母、数字的组合'),  
291 - uid: _.get(user, 'uid', '')  
292 - };  
293 - }  
294 -  
295 - // 不可以跳过,不登录用户  
296 - if (user.weakPassword && user.canSkip !== 'Y') {  
297 - return res.json(passwordWeakReturn);  
298 - }  
299 -  
300 - AuthHelper.syncUserSession(user.uid, req, res, user.session_key).then(() => {  
301 - if (user.weakPassword) {  
302 - return res.json(passwordWeakReturn);  
303 - } else {  
304 - res.json({  
305 - code: 200,  
306 - data: user  
307 - });  
308 - }  
309 - }).catch(next);  
310 - }  
311 - })(req, res, next);  
312 - },  
313 - logout: (req, res) => {  
314 - res.clearCookie('_SPK');  
315 - res.cookie('_WX_PASS_LOGIN', true, {  
316 - domain: 'm.yohobuy.com'  
317 - });  
318 - let refer = req.get('Referer') || config.siteUrl;  
319 -  
320 - refer = utils.refererLimit(refer);  
321 - res.redirect(refer);  
322 - }  
323 -};  
324 -  
325 -const wechat = {  
326 - login: (req, res, next) => {  
327 - // 设置为原链接标识originalUrl  
328 - req.session.originalUrl = 'true';  
329 - req.session.authState = uuid.v4();  
330 - res.clearCookie('_WX_PASS_LOGIN', {  
331 - domain: 'm.yohobuy.com'  
332 - });  
333 - return passport.authenticate('weixin', {  
334 - state: req.session.authState  
335 - })(req, res, next);  
336 - },  
337 - callback: (req, res, next) => {  
338 - if (req.session && req.session.authState && req.session.authState === req.query.state) {  
339 - passport.authenticate('weixin', (err, user) => {  
340 - if (err || !user) {  
341 - log.error(`wechat authenticate error : ${JSON.stringify(err)}`);  
342 - return res.redirect(loginPage);  
343 - }  
344 - let nickname = user._json.nickname || user.displayName;  
345 - let openId = user._json.unionid || user.id;  
346 -  
347 - res.cookie('_WX_OPENID', _.get(user, '_json.openid'), {  
348 - domain: 'm.yohobuy.com'  
349 - });  
350 - res.cookie('_WX_UNIONID', _.get(user, '_json.unionid'), {  
351 - domain: 'm.yohobuy.com'  
352 - });  
353 - res.cookie('_LOGIN_TYPE', 4, {  
354 - domain: 'm.yohobuy.com'  
355 - });  
356 - doPassportCallback(openId, nickname, 'wechat', req, res).catch(next);  
357 - })(req, res, next);  
358 - } else {  
359 - log.error('Auth State Mismatch:' + req.originalUrl);  
360 - return res.redirect(loginPage);  
361 - }  
362 - }  
363 -};  
364 -  
365 -const sina = {  
366 - login: (req, res, next) => {  
367 - // 设置为原链接标识originalUrl  
368 - req.session.originalUrl = 'true';  
369 - req.session.authState = uuid.v4();  
370 - return passport.authenticate('sina', {  
371 - state: req.session.authState  
372 - })(req, res, next);  
373 - },  
374 - callback: (req, res, next) => {  
375 - if (req.session && req.session.authState && req.session.authState === req.query.state) {  
376 - passport.authenticate('sina', (err, user) => {  
377 - if (err || !user) {  
378 - log.error(`sina authenticate error : ${JSON.stringify(err)}`);  
379 - return res.redirect(loginPage);  
380 - }  
381 - let nickname = user.screen_name;  
382 - let openId = user.id;  
383 -  
384 - res.cookie('_LOGIN_TYPE', 2, {  
385 - domain: 'm.yohobuy.com'  
386 - });  
387 - doPassportCallback(openId, nickname, 'sina', req, res).catch(next);  
388 - })(req, res, next);  
389 - } else {  
390 - log.error('Auth State Mismatch:' + req.originalUrl);  
391 - return res.redirect(loginPage);  
392 - }  
393 - }  
394 -};  
395 -  
396 -const qq = {  
397 - login: (req, res, next) => {  
398 - // 设置为原链接标识originalUrl  
399 - req.session.originalUrl = 'true';  
400 - req.session.authState = uuid.v4();  
401 - return passport.authenticate('qq', {  
402 - state: req.session.authState  
403 - })(req, res, next);  
404 - },  
405 - callback: (req, res, next) => {  
406 - if (req.session && req.session.authState && req.session.authState === req.query.state) {  
407 - passport.authenticate('qq', (err, user) => {  
408 - if (err) {  
409 - log.error(`qq authenticate error : ${JSON.stringify(err)}`);  
410 - return res.redirect(loginPage);  
411 - }  
412 -  
413 - let nickname = user.nickname;  
414 - let openId = user.id;  
415 -  
416 - res.cookie('_LOGIN_TYPE', 1, {  
417 - domain: 'm.yohobuy.com'  
418 - });  
419 - doPassportCallback(openId, nickname, 'qq', req, res).catch(next);  
420 - })(req, res, next);  
421 - } else {  
422 - log.error('Auth State Mismatch:' + req.originalUrl);  
423 - return res.redirect(loginPage);  
424 - }  
425 - }  
426 -};  
427 -  
428 -const alipay = {  
429 - login: (req, res, next) => {  
430 - // 设置为原链接标识originalUrl  
431 - req.session.originalUrl = 'true';  
432 - return passport.authenticate('alipay')(req, res, next);  
433 - },  
434 - callback: (req, res, next) => {  
435 - passport.authenticate('alipay', (err, user) => {  
436 - if (err || !user) {  
437 - log.error(`alipay authenticate error : ${JSON.stringify(err)}`);  
438 - return res.redirect(loginPage);  
439 - }  
440 - let nickname = user.realName;  
441 - let openId = user.userId;  
442 -  
443 - res.cookie('_LOGIN_TYPE', 3, {  
444 - domain: 'm.yohobuy.com'  
445 - });  
446 - doPassportCallback(openId, nickname, 'alipay', req, res).catch(next);  
447 - })(req, res, next);  
448 - }  
449 -};  
450 -  
451 -exports.user = function(req, res, next) {  
452 - let result = {  
453 - code: 403,  
454 - message: '未登录',  
455 - data: ''  
456 - };  
457 -  
458 - if (req.user.uid) {  
459 - result.code = 200;  
460 - result.message = '已登录';  
461 - result.data = req.user.uid.toString();  
462 - }  
463 - res.jsonp(result);  
464 -};  
465 -  
466 -/**  
467 - * 中间件  
468 - * 根据用户登录是否成功决定是否展示验证码  
469 - */  
470 -exports.loginShowCaptchaByIp = function(req, res, next) {  
471 - // 总开关状态  
472 - req.yoho.captchaShow = !_.get(req.app.locals.wap, 'close.loginValidation', false);  
473 -  
474 - // 开关打开,不走任何验证逻辑  
475 - if (!req.yoho.captchaShow) {  
476 - return next();  
477 - } else {  
478 - req.yoho.captchaShow = false;  
479 - }  
480 -  
481 - co(function*() {  
482 - let hasErrorLog = yield cache.get(`loginErrorIp:${req.yoho.clientIp}`);  
483 -  
484 - log.info(`Pagerender clientip ${req.yoho.clientIp} status is ` + hasErrorLog);  
485 -  
486 - if (hasErrorLog) {  
487 - req.yoho.captchaShow = true;  
488 - }  
489 - next();  
490 - })().catch(function(e) {  
491 - req.yoho.captchaShow = true;  
492 - next();  
493 - });  
494 -};  
495 -  
496 -exports.common = common;  
497 -exports.local = local;  
498 -exports.wechat = wechat;  
499 -exports.sina = sina;  
500 -exports.qq = qq;  
501 -exports.alipay = alipay;  
@@ -5,10 +5,43 @@ @@ -5,10 +5,43 @@
5 */ 5 */
6 'use strict'; 6 'use strict';
7 const _ = require('lodash'); 7 const _ = require('lodash');
  8 +const moment = require('moment');
8 const EventEmitter = require('events'); 9 const EventEmitter = require('events');
9 const SmsModel = require('../models/sms'); 10 const SmsModel = require('../models/sms');
10 11
11 const smsController = { 12 const smsController = {
  13 +
  14 + /**
  15 + * 发送短信前
  16 + * @param req
  17 + * @param res
  18 + * @param next
  19 + * @returns {*}
  20 + */
  21 + beforeSend(req, res, next) {
  22 + const count = _.get(req.session, 'smsSend.count');
  23 + const interval = _.get(req.session, 'smsSend.interval', 0);
  24 +
  25 + // 重发次数用完了, 冻结5min
  26 + // 过了冻结期, count重设为5次
  27 + // 没有用完, 判断是否请求太频繁
  28 + const now = Date.now();
  29 + const during = moment.duration(interval - now, 'ms').minutes();
  30 + const msg = {
  31 + code: 429,
  32 + message: `请${during || 1}分钟后再试`
  33 + };
  34 +
  35 + if (interval > now) {
  36 + return res.json(msg);
  37 + }
  38 +
  39 + // 重置可发送次数
  40 + !count && _.set(req.session, 'smsSend.count', 5);
  41 +
  42 + next();
  43 + },
  44 +
12 /** 45 /**
13 * 发送短信验证码 46 * 发送短信验证码
14 * @param req 47 * @param req
@@ -17,17 +50,26 @@ const smsController = { @@ -17,17 +50,26 @@ const smsController = {
17 */ 50 */
18 sendCode(req, res) { 51 sendCode(req, res) {
19 const em = new EventEmitter(); 52 const em = new EventEmitter();
20 - const area = (req.body.area || '').trim(); 53 + const area = (req.body.area || '86').trim();
21 const mobile = (req.body.mobile || '').trim(); 54 const mobile = (req.body.mobile || '').trim();
22 const inValid = [area, mobile].some(v => v === ''); 55 const inValid = [area, mobile].some(v => v === '');
23 56
24 em.on('resolve', () => { 57 em.on('resolve', () => {
25 - _.set(req.session, 'smsLogin.area', area);  
26 - _.set(req.session, 'smsLogin.mobile', mobile);  
27 -  
28 req.ctx(SmsModel).sendSMS(mobile, area, 1) 58 req.ctx(SmsModel).sendSMS(mobile, area, 1)
29 .then(result => { 59 .then(result => {
30 if (result.code === 200) { 60 if (result.code === 200) {
  61 + _.set(req.session, 'smsSend.area', area);
  62 + _.set(req.session, 'smsSend.mobile', mobile);
  63 +
  64 + // 剩余次数
  65 + --req.session.smsSend.count;
  66 +
  67 + if (!req.session.smsSend.count) {
  68 + _.set(req.session, 'smsSend.interval', Date.now() + 5 * 60 * 1000);
  69 + } else {
  70 + _.set(req.session, 'smsSend.interval', Date.now() + 60 * 1000);
  71 + }
  72 +
31 return res.json({ 73 return res.json({
32 code: 200, 74 code: 200,
33 message: '验证码发送成功' 75 message: '验证码发送成功'
@@ -64,8 +106,8 @@ const smsController = { @@ -64,8 +106,8 @@ const smsController = {
64 */ 106 */
65 checkCode(req, res, next) { 107 checkCode(req, res, next) {
66 const code = (req.body.code || '').trim(); 108 const code = (req.body.code || '').trim();
67 - const area = _.get(req.session, 'smsLogin.area', '');  
68 - const mobile = _.get(req.session, 'smsLogin.mobile', ''); 109 + const area = _.get(req.session, 'smsSend.area', '');
  110 + const mobile = _.get(req.session, 'smsSend.mobile', '');
69 111
70 req.ctx(SmsModel).verifySMS(mobile, area, code, 1) 112 req.ctx(SmsModel).verifySMS(mobile, area, code, 1)
71 .then(result => { 113 .then(result => {
@@ -3,16 +3,14 @@ @@ -3,16 +3,14 @@
3 * @author: leo <qi.li@yoho.cn> 3 * @author: leo <qi.li@yoho.cn>
4 * @date: 2017/06/26 4 * @date: 2017/06/26
5 */ 5 */
6 -  
7 'use strict'; 6 'use strict';
8 const express = require('express'); 7 const express = require('express');
9 const sms = require('./controllers/sms'); 8 const sms = require('./controllers/sms');
10 const user = require('./controllers/user'); 9 const user = require('./controllers/user');
11 -  
12 const router = express.Router(); // eslint-disable-line 10 const router = express.Router(); // eslint-disable-line
13 11
14 // SMS 短信 12 // SMS 短信
15 -router.post('/sms/sendCode', sms.sendCode); 13 +router.post('/sms/sendCode', sms.beforeSend, sms.sendCode);
16 router.post('/sms/checkCode', sms.checkCode, user.userInfo); 14 router.post('/sms/checkCode', sms.checkCode, user.userInfo);
17 15
18 module.exports = router; 16 module.exports = router;