Authored by 陈轩

Merge branch 'hotfix/smslogin-improve' into release/5.2

1 /* eslint no-unused-vars: ["error", { "args": "none" }] */ 1 /* eslint no-unused-vars: ["error", { "args": "none" }] */
2 'use strict'; 2 'use strict';
  3 +const _ = require('lodash');
3 const helpers = global.yoho.helpers; 4 const helpers = global.yoho.helpers;
4 const cookie = global.yoho.cookie; 5 const cookie = global.yoho.cookie;
5 const RegService = require('../models/reg-service'); 6 const RegService = require('../models/reg-service');
6 const PhoneService = require('../models/phone-service'); 7 const PhoneService = require('../models/phone-service');
7 const AuthHelper = require('../models/auth-helper'); 8 const AuthHelper = require('../models/auth-helper');
  9 +const captchaService = require('../models/captcha-service');
8 10
9 // constrant 11 // constrant
10 const CODE_REQUIRED = '请输入校验码'; 12 const CODE_REQUIRED = '请输入校验码';
@@ -23,7 +25,7 @@ exports.beforeIn = (req, res, next) => { @@ -23,7 +25,7 @@ exports.beforeIn = (req, res, next) => {
23 }); 25 });
24 26
25 if (!req.xhr && req.user.uid) { 27 if (!req.xhr && req.user.uid) {
26 - return res.redirect(req.cookies.refer); 28 + return res.redirect(req.cookies.refer || '/');
27 } 29 }
28 30
29 next(); 31 next();
@@ -31,6 +33,12 @@ exports.beforeIn = (req, res, next) => { @@ -31,6 +33,12 @@ exports.beforeIn = (req, res, next) => {
31 33
32 // 短信登录 第一步: 展现页面 34 // 短信登录 第一步: 展现页面
33 const _step1 = (req, res, next) => { 35 const _step1 = (req, res, next) => {
  36 + _.set(req.session, 'smsLogin.step', 1);
  37 +
  38 + if (req.session.smsLogin.count == null) { // eslint-disable-line
  39 + req.session.smsLogin.count = 5;
  40 + }
  41 +
34 let template = 'sms/login'; 42 let template = 'sms/login';
35 let viewData = { 43 let viewData = {
36 module: 'passport', 44 module: 'passport',
@@ -38,6 +46,7 @@ const _step1 = (req, res, next) => { @@ -38,6 +46,7 @@ const _step1 = (req, res, next) => {
38 title: '手机短信登录', 46 title: '手机短信登录',
39 isPassportPage: true, 47 isPassportPage: true,
40 headerText: '手机号码快捷登录', 48 headerText: '手机号码快捷登录',
  49 + captchaUrl: helpers.urlFormat('/passport/sms_login/captcha.png', {t: Date.now()}),
41 areaCode: '+86', // 默认的区号 50 areaCode: '+86', // 默认的区号
42 countrys: RegService.getAreaData() // 地区信息列表 51 countrys: RegService.getAreaData() // 地区信息列表
43 }; 52 };
@@ -58,7 +67,7 @@ const _step2 = (req, res, next) => { @@ -58,7 +67,7 @@ const _step2 = (req, res, next) => {
58 title: '手机短信登录', 67 title: '手机短信登录',
59 isPassportPage: true, 68 isPassportPage: true,
60 headerText: '手机号码快捷登录', 69 headerText: '手机号码快捷登录',
61 - canResend: interval < Date.now(), 70 + countdown: Math.ceil((interval - Date.now()) / 1000),
62 mobile, 71 mobile,
63 area 72 area
64 }; 73 };
@@ -83,7 +92,7 @@ const _step3 = (req, res, next) => { @@ -83,7 +92,7 @@ const _step3 = (req, res, next) => {
83 // 短信 登录 92 // 短信 登录
84 exports.loginPage = (req, res, next) => { 93 exports.loginPage = (req, res, next) => {
85 let step = Number(req.query.step) || 1; 94 let step = Number(req.query.step) || 1;
86 - let smsLoginStep = req.session.smsLoginStep || 1; 95 + let smsLoginStep = _.get(req.session, 'smsLogin.step', 1);
87 96
88 if (step === 2 && smsLoginStep !== 2) { 97 if (step === 2 && smsLoginStep !== 2) {
89 return res.redirect(req.path); 98 return res.redirect(req.path);
@@ -109,23 +118,54 @@ exports.loginPage = (req, res, next) => { @@ -109,23 +118,54 @@ exports.loginPage = (req, res, next) => {
109 exports.tokenBefore = (req, res, next) => { 118 exports.tokenBefore = (req, res, next) => {
110 let area = req.query.area = (req.query.area || '').trim(); 119 let area = req.query.area = (req.query.area || '').trim();
111 let mobile = req.query.mobile = (req.query.mobile || '').trim(); 120 let mobile = req.query.mobile = (req.query.mobile || '').trim();
  121 + let step = _.get(req.session, 'smsLogin.step');
  122 + let count = _.get(req.session, 'smsLogin.count');
  123 + let interval = _.get(req.session, 'smsLogin.interval');
112 124
113 - if (!req.xhr) { 125 + if (!req.xhr && !req.session.smsLogin) {
114 return next(404); 126 return next(404);
115 } 127 }
116 128
117 - if (req.session.smsLogin && req.session.smsLogin.interval > Date.now()) { 129 + if ([area, mobile].some(val => val === '')) {
118 return res.json({ 130 return res.json({
119 - code: 429,  
120 - message: TOO_MANY 131 + code: 401,
  132 + message: '请求参数,无法处理'
121 }); 133 });
122 } 134 }
123 135
  136 + // step1 要 校验图形验证码
  137 + if (step === 1) {
  138 + let captcha1 = _.get(req.session, 'smsLogin.captcha');
  139 + let captcha2 = (req.query.captcha || '').trim();
124 140
125 - if ([area, mobile].some(val => val === '')) { 141 + if (captcha1 !== captcha2) {
  142 + return res.json({
  143 + code: 400,
  144 + message: VERIFY_ERROR
  145 + });
  146 + }
  147 + }
  148 +
  149 + let now = Date.now();
  150 +
  151 + // 重发次数用完了, 回冻结5min
  152 + // 1. 过了冻结期, count 重设为 5次
  153 + // 2. 没过冻结期, end
  154 + // 没有用完, 判断是否请求太频繁
  155 + if (!count) {
  156 + if (interval > now) {
  157 + return res.json({
  158 + code: 400,
  159 + message: TOO_MANY,
  160 + during: Math.ceil((interval - now) / 1000)
  161 + });
  162 + } else {
  163 + _.set(req.session, 'smsLogin.count', 5);
  164 + }
  165 + } else if (interval > now) {
126 return res.json({ 166 return res.json({
127 - code: 401,  
128 - message: '请求参数,无法处理' 167 + code: 429,
  168 + message: TOO_MANY
129 }); 169 });
130 } 170 }
131 171
@@ -139,12 +179,18 @@ exports.token = (req, res, next) => { @@ -139,12 +179,18 @@ exports.token = (req, res, next) => {
139 179
140 PhoneService.sendSMS(mobile, area, 1).then(result => { 180 PhoneService.sendSMS(mobile, area, 1).then(result => {
141 if (result.code === 200) { 181 if (result.code === 200) {
142 - req.session.smsLogin = {  
143 - interval: Date.now() + 60 * 1000, // 重发验证码 间隔: 60s  
144 - area,  
145 - mobile  
146 - };  
147 - req.session.smsLoginStep = 2; // 进入短信登录 step2 182 +
  183 + _.set(req.session, 'smsLogin.step', 2);
  184 + _.set(req.session, 'smsLogin.area', area);
  185 + _.set(req.session, 'smsLogin.mobile', mobile);
  186 +
  187 + --req.session.smsLogin.count;
  188 +
  189 + if (!req.session.smsLogin.count) {
  190 + _.set(req.session, 'smsLogin.interval', Date.now() + 5 * 60 * 1000);
  191 + } else {
  192 + _.set(req.session, 'smsLogin.interval', Date.now() + 60 * 1000);
  193 + }
148 194
149 result.redirect = '/passport/sms_login?step=2'; 195 result.redirect = '/passport/sms_login?step=2';
150 res.json(result); 196 res.json(result);
@@ -157,8 +203,9 @@ exports.token = (req, res, next) => { @@ -157,8 +203,9 @@ exports.token = (req, res, next) => {
157 203
158 exports.checkBefore = (req, res, next) => { 204 exports.checkBefore = (req, res, next) => {
159 let code = req.query.code = (req.query.code || '').trim(); 205 let code = req.query.code = (req.query.code || '').trim();
  206 + let step = _.get(req.session, 'smsLogin.step');
160 207
161 - if (!req.xhr && req.session.smsLoginStep !== 2) { 208 + if (!req.xhr && step !== 2) {
162 return next(404); 209 return next(404);
163 } 210 }
164 211
@@ -214,7 +261,7 @@ exports.check = (req, res, next) => { @@ -214,7 +261,7 @@ exports.check = (req, res, next) => {
214 // 手机号码 没注册 261 // 手机号码 没注册
215 if (r1.data.is_register !== 'Y') { 262 if (r1.data.is_register !== 'Y') {
216 redirect = '/passport/sms_login?step=3'; 263 redirect = '/passport/sms_login?step=3';
217 - req.session.smsLoginStep = 3; 264 + _.set(req.session, 'smsLogin.step', 3);
218 265
219 res.json({ 266 res.json({
220 code: 200, 267 code: 200,
@@ -247,7 +294,6 @@ exports.check = (req, res, next) => { @@ -247,7 +294,6 @@ exports.check = (req, res, next) => {
247 }); 294 });
248 295
249 delete req.session.smsLogin; 296 delete req.session.smsLogin;
250 - delete req.session.smsLoginStep;  
251 }) 297 })
252 .catch(error => { 298 .catch(error => {
253 res.json(error); 299 res.json(error);
@@ -259,7 +305,9 @@ exports.check = (req, res, next) => { @@ -259,7 +305,9 @@ exports.check = (req, res, next) => {
259 305
260 // AJAX 短信登录 设置密码 in step3 306 // AJAX 短信登录 设置密码 in step3
261 exports.password = (req, res, next) => { 307 exports.password = (req, res, next) => {
262 - if (req.session.smsLoginStep !== 3) { 308 + let step = _.get(req.session, 'smsLogin.step');
  309 +
  310 + if (step !== 3) {
263 return next(); 311 return next();
264 } 312 }
265 313
@@ -269,9 +317,8 @@ exports.password = (req, res, next) => { @@ -269,9 +317,8 @@ exports.password = (req, res, next) => {
269 message: BAD_PASSWORD 317 message: BAD_PASSWORD
270 }; 318 };
271 319
272 - let smsLogin = req.session.smsLogin || {};  
273 - let mobile = smsLogin.mobile;  
274 - let area = smsLogin.area; 320 + let mobile = _.get(req.session, 'smsLogin.mobile');
  321 + let area = _.get(req.session, 'smsLogin.area');
275 let password = (req.body.password || '').trim(); 322 let password = (req.body.password || '').trim();
276 let smsCode = +req.body.smsCode || 0; 323 let smsCode = +req.body.smsCode || 0;
277 324
@@ -306,11 +353,23 @@ exports.password = (req, res, next) => { @@ -306,11 +353,23 @@ exports.password = (req, res, next) => {
306 res.json({ 353 res.json({
307 code: 200, 354 code: 200,
308 message: LOGIN_SUCCSS, 355 message: LOGIN_SUCCSS,
309 - redirect: req.cookies.refer 356 + redirect: req.cookies.refer || '/'
310 }); 357 });
311 delete req.session.smsLogin; 358 delete req.session.smsLogin;
312 - delete req.session.smsLoginStep;  
313 }).catch(next); 359 }).catch(next);
  360 +};
  361 +
  362 +
  363 +/**
  364 + * 生成 校验码
  365 + */
  366 +exports.genCaptcha = (req, res) => {
  367 + let captcha = captchaService.generateCaptcha(90, 52, 4);
314 368
  369 + _.set(req.session, 'smsLogin.captcha', captcha.text);
315 370
  371 + res.type('png')
  372 + .set('Cache-Control', 'no-cache')
  373 + .status(200)
  374 + .send(captcha.image);
316 }; 375 };
@@ -37,15 +37,16 @@ router.get('/passport/international', login.common.beforeLogin, login.local.inte @@ -37,15 +37,16 @@ router.get('/passport/international', login.common.beforeLogin, login.local.inte
37 router.post('/passport/login/auth', login.local.login); 37 router.post('/passport/login/auth', login.local.login);
38 38
39 // SMS 短信 39 // SMS 短信
40 -// router.use('/passport/sms_login', login.common.beforeLogin, smsLogin.beforeIn);  
41 -// router.get('/passport/sms_login', smsLogin.loginPage);  
42 -// router.get('/passport/sms_login/token.json',  
43 -// smsLogin.tokenBefore,  
44 -// smsLogin.token); // only ajax;  
45 -// router.get('/passport/sms_login/check.json',  
46 -// smsLogin.checkBefore,  
47 -// smsLogin.check); // only ajax  
48 -// router.post('/passport/sms_login/password.json', smsLogin.password); 40 +router.use('/passport/sms_login', login.common.beforeLogin, smsLogin.beforeIn);
  41 +router.get('/passport/sms_login', smsLogin.loginPage);
  42 +router.get('/passport/sms_login/token.json',
  43 + smsLogin.tokenBefore,
  44 + smsLogin.token); // only ajax;
  45 +router.get('/passport/sms_login/check.json',
  46 + smsLogin.checkBefore,
  47 + smsLogin.check); // only ajax
  48 +router.post('/passport/sms_login/password.json', smsLogin.password);
  49 +router.get('/passport/sms_login/captcha.png', smsLogin.genCaptcha);
49 50
50 // 微信登录 51 // 微信登录
51 router.get('/passport/login/wechat', login.common.beforeLogin, login.wechat.login); 52 router.get('/passport/login/wechat', login.common.beforeLogin, login.wechat.login);
@@ -15,5 +15,5 @@ @@ -15,5 +15,5 @@
15 <input type="hidden" name="area" id="area" value="{{area}}"> 15 <input type="hidden" name="area" id="area" value="{{area}}">
16 </div> 16 </div>
17 <script> 17 <script>
18 - var canResend = {{canResend}}; 18 + var countdown = {{countdown}};
19 </script> 19 </script>
@@ -7,6 +7,10 @@ @@ -7,6 +7,10 @@
7 <input id="phone-num" class="input phone-num" type="text" placeholder="手机号"> 7 <input id="phone-num" class="input phone-num" type="text" placeholder="手机号">
8 <button class="clear-input" type="button"></button> 8 <button class="clear-input" type="button"></button>
9 </div> 9 </div>
  10 + <div class="passport-captcha row">
  11 + <input type="text">
  12 + <div class="passport-captcha-img"><img src="{{captchaUrl}}" alt=""></div>
  13 + </div>
10 <button id="btn-next" class="btn btn-next disable row" disabled>获取短信验证码</button> 14 <button id="btn-next" class="btn btn-next disable row" disabled>获取短信验证码</button>
11 </div> 15 </div>
12 </div> 16 </div>
@@ -22,8 +22,8 @@ page = { @@ -22,8 +22,8 @@ page = {
22 init: function() { 22 init: function() {
23 this.domInit(); 23 this.domInit();
24 this.bindEvents(); 24 this.bindEvents();
25 - if (!window.canResend) {  
26 - this.countDown(); 25 + if (window.countdown > 0) {
  26 + this.countDown(window.countdown);
27 } 27 }
28 }, 28 },
29 29
@@ -72,11 +72,14 @@ page = { @@ -72,11 +72,14 @@ page = {
72 }); 72 });
73 }, 73 },
74 74
75 - countDown: function() { 75 + countDown: function(during) {
76 var self = this; 76 var self = this;
77 var second = this.time; 77 var second = this.time;
78 78
79 - if (this.timerId) { 79 + if (during) {
  80 + clearInterval(this.timerId);
  81 + second = during;
  82 + } else if (this.timerId) {
80 return; 83 return;
81 } 84 }
82 85
@@ -116,6 +119,8 @@ page = { @@ -116,6 +119,8 @@ page = {
116 if (res.code === 200) { 119 if (res.code === 200) {
117 self.countDown(); 120 self.countDown();
118 return; 121 return;
  122 + } else {
  123 + res.during && (self.countDown(res.during));
119 } 124 }
120 125
121 tip.show(res.message); 126 tip.show(res.message);
@@ -6,7 +6,10 @@ var $countrySelect, @@ -6,7 +6,10 @@ var $countrySelect,
6 $areaCode, 6 $areaCode,
7 $nextBtn, 7 $nextBtn,
8 $resetBtn, 8 $resetBtn,
  9 + $captcha,
  10 + $captchaPNG,
9 $phoneNum; 11 $phoneNum;
  12 +
10 var page; 13 var page;
11 14
12 require('js/common'); 15 require('js/common');
@@ -25,6 +28,8 @@ page = { @@ -25,6 +28,8 @@ page = {
25 $nextBtn = $('#btn-next'); 28 $nextBtn = $('#btn-next');
26 $phoneNum = $('#phone-num'); 29 $phoneNum = $('#phone-num');
27 $resetBtn = $('.clear-input'); 30 $resetBtn = $('.clear-input');
  31 + $captcha = $('.passport-captcha input');
  32 + $captchaPNG = $('.passport-captcha-img img');
28 }, 33 },
29 bindEvent: function() { 34 bindEvent: function() {
30 var self = this; 35 var self = this;
@@ -36,6 +41,12 @@ page = { @@ -36,6 +41,12 @@ page = {
36 self.toggleNextBtn(); 41 self.toggleNextBtn();
37 }); 42 });
38 43
  44 + $captcha.on('input', function() {
  45 + self.toggleNextBtn();
  46 + });
  47 +
  48 + $captchaPNG.on('click', $.proxy(this.refreshCapatch, this));
  49 +
39 $nextBtn.on('click', function() { 50 $nextBtn.on('click', function() {
40 self.goNext(); 51 self.goNext();
41 }); 52 });
@@ -51,7 +62,7 @@ page = { @@ -51,7 +62,7 @@ page = {
51 62
52 // 切换$nextBtn disable状态 63 // 切换$nextBtn disable状态
53 toggleNextBtn: function() { 64 toggleNextBtn: function() {
54 - var bool = Boolean($.trim($phoneNum.val())); 65 + var bool = Boolean($.trim($phoneNum.val())) && Boolean($.trim($captcha.val()));
55 66
56 $nextBtn 67 $nextBtn
57 .toggleClass('disable', !bool) 68 .toggleClass('disable', !bool)
@@ -60,10 +71,16 @@ page = { @@ -60,10 +71,16 @@ page = {
60 $resetBtn.toggle(bool); 71 $resetBtn.toggle(bool);
61 }, 72 },
62 73
  74 + refreshCapatch: function() {
  75 + $captchaPNG.attr('src', '/passport/sms_login/captcha.png?t=' + Date.now());
  76 + },
  77 +
63 // 提交按钮 78 // 提交按钮
64 goNext: function() { 79 goNext: function() {
  80 + var self = this;
65 var areaCode = $countrySelect.val(); 81 var areaCode = $countrySelect.val();
66 var phone = $.trim($phoneNum.val()); 82 var phone = $.trim($phoneNum.val());
  83 + var captcha = $.trim($captcha.val());
67 84
68 if ($nextBtn.prop('disabled')) { 85 if ($nextBtn.prop('disabled')) {
69 return; 86 return;
@@ -77,7 +94,8 @@ page = { @@ -77,7 +94,8 @@ page = {
77 $nextBtn.prop('disabled', true); 94 $nextBtn.prop('disabled', true);
78 $.get('/passport/sms_login/token.json', { 95 $.get('/passport/sms_login/token.json', {
79 area: areaCode.replace('+', ''), 96 area: areaCode.replace('+', ''),
80 - mobile: phone 97 + mobile: phone,
  98 + captcha: captcha
81 }) 99 })
82 .done(function(data) { 100 .done(function(data) {
83 if (data.code === 200) { 101 if (data.code === 200) {
@@ -85,6 +103,7 @@ page = { @@ -85,6 +103,7 @@ page = {
85 $nextBtn.off(); 103 $nextBtn.off();
86 location.href = data.redirect; 104 location.href = data.redirect;
87 } else { 105 } else {
  106 + self.refreshCapatch();
88 tip.show(data.message); 107 tip.show(data.message);
89 } 108 }
90 }) 109 })