Authored by 陈轩

注册 安全

@@ -15,7 +15,47 @@ const RegService = require('../models/reg-service'); @@ -15,7 +15,47 @@ const RegService = require('../models/reg-service');
15 const AuthHelper = require('../models/auth-helper'); 15 const AuthHelper = require('../models/auth-helper');
16 const captchaService = require('../models/captcha-service'); 16 const captchaService = require('../models/captcha-service');
17 17
  18 +/*
  19 + session 结构
  20 + phoneReg: {
  21 + step //当前步骤
  22 + captcha // step1 的校验码
  23 + count: 5 // 默认可以重发5次, 当count: 0, 冻结30min,之后解冻
  24 + expire // 解冻时间
  25 + }
  26 +*/
  27 +
  28 +/**
  29 + * 步骤校验
  30 + * step: 预期步骤
  31 + */
  32 +let guardStep = function(step) {
  33 + return (req, res, next) => {
  34 + let curStep = _.get(req.session, 'phoneReg.step');
  35 +
  36 + if (curStep !== step) {
  37 + if (req.xhr) {
  38 + return res.json({
  39 + code: 400,
  40 + refer: '/reg.html'
  41 + });
  42 + } else {
  43 + return res.redirect('/reg.html');
  44 + }
  45 + }
  46 +
  47 + return next();
  48 + };
  49 +};
  50 +
  51 +/**
  52 + * Step1: 输入手机号码 + 验证码
  53 + */
18 let index = (req, res) => { 54 let index = (req, res) => {
  55 + if (req.user.uid) {
  56 + return res.redirect(req.get('refer') || '/');
  57 + }
  58 +
19 // 设置注册有效时间30分钟, 防机器刷 59 // 设置注册有效时间30分钟, 防机器刷
20 // req.session.REG_EXPIRE = Date.now() + 1800000; 60 // req.session.REG_EXPIRE = Date.now() + 1800000;
21 let refer = req.query.refer; 61 let refer = req.query.refer;
@@ -24,6 +64,13 @@ let index = (req, res) => { @@ -24,6 +64,13 @@ let index = (req, res) => {
24 domain: 'yohobuy.com' 64 domain: 'yohobuy.com'
25 }); 65 });
26 66
  67 + // session init
  68 + _.set(req.session, 'phoneReg.step', 1);
  69 +
  70 + if (req.session.phoneReg.count == null) { // eslint-disable-line
  71 + req.session.phoneReg.count = 5;
  72 + }
  73 +
27 res.render('reg/index', { 74 res.render('reg/index', {
28 module: 'passport', 75 module: 'passport',
29 page: 'reg', 76 page: 'reg',
@@ -36,6 +83,8 @@ let index = (req, res) => { @@ -36,6 +83,8 @@ let index = (req, res) => {
36 countrys: RegService.getAreaData() // 地区信息列表 83 countrys: RegService.getAreaData() // 地区信息列表
37 }); 84 });
38 }; 85 };
  86 +
  87 +
39 let verifyMobile = (req, res, next) => { 88 let verifyMobile = (req, res, next) => {
40 let data = { 89 let data = {
41 code: 400, 90 code: 400,
@@ -45,14 +94,13 @@ let verifyMobile = (req, res, next) => { @@ -45,14 +94,13 @@ let verifyMobile = (req, res, next) => {
45 94
46 let mobile = +req.body.phoneNum; 95 let mobile = +req.body.phoneNum;
47 let area = +(req.body.areaCode || 86); 96 let area = +(req.body.areaCode || 86);
48 - let captcha = +(req.body.captcha); 97 + let captcha = (req.body.captcha || '').trim();
49 98
50 -  
51 -  
52 - if (captcha !== req.session.regCaptch) { 99 + if (captcha !== _.get(req.session, 'phoneReg.captcha')) {
53 return res.json({ 100 return res.json({
54 - code: 111,  
55 - message: '校验码不正确' 101 + code: 400,
  102 + message: '校验码不正确',
  103 + refreshCaptcha: true
56 }); 104 });
57 } 105 }
58 106
@@ -70,6 +118,7 @@ let verifyMobile = (req, res, next) => { @@ -70,6 +118,7 @@ let verifyMobile = (req, res, next) => {
70 // return res.json(data); 118 // return res.json(data);
71 // } 119 // }
72 120
  121 +
73 // 向手机发送注册验证码 122 // 向手机发送注册验证码
74 RegService.sendCodeToMobile(area, mobile).then((result) => { 123 RegService.sendCodeToMobile(area, mobile).then((result) => {
75 if (!result.code) { 124 if (!result.code) {
@@ -80,6 +129,8 @@ let verifyMobile = (req, res, next) => { @@ -80,6 +129,8 @@ let verifyMobile = (req, res, next) => {
80 if (result.code === 200) { 129 if (result.code === 200) {
81 let token = sign.makeToken(mobile); 130 let token = sign.makeToken(mobile);
82 131
  132 + _.set(req.session, 'phoneReg.step', 2); // go step 2
  133 +
83 result.data = helpers.urlFormat('/passport/reg/code', { 134 result.data = helpers.urlFormat('/passport/reg/code', {
84 token: token, 135 token: token,
85 phoneNum: mobile, 136 phoneNum: mobile,
@@ -90,6 +141,11 @@ let verifyMobile = (req, res, next) => { @@ -90,6 +141,11 @@ let verifyMobile = (req, res, next) => {
90 return res.json(result); 141 return res.json(result);
91 }).catch(next); 142 }).catch(next);
92 }; 143 };
  144 +
  145 +
  146 +/**
  147 + * Step2: 校验 手机验证码
  148 + */
93 let codeAction = (req, res, next) => { 149 let codeAction = (req, res, next) => {
94 let token = req.query.token; 150 let token = req.query.token;
95 let mobile = +req.query.phoneNum; 151 let mobile = +req.query.phoneNum;
@@ -115,6 +171,36 @@ let codeAction = (req, res, next) => { @@ -115,6 +171,36 @@ let codeAction = (req, res, next) => {
115 serviceUrl: 'http://chat8.live800.com/live800/chatClient/chatbox.jsp?companyID=620092&configID=149091&jid=8732423409&info=' // 在线客服 171 serviceUrl: 'http://chat8.live800.com/live800/chatClient/chatbox.jsp?companyID=620092&configID=149091&jid=8732423409&info=' // 在线客服
116 }); 172 });
117 }; 173 };
  174 +
  175 +let sendCodeBusyBoy = (req, res, next) => {
  176 + let count = _.get(req.session, 'phoneReg.count');
  177 + let expire = _.get(req.session, 'phoneReg.expire');
  178 +
  179 + if (count) {
  180 + return next();
  181 + } else {
  182 +
  183 + /*
  184 + 如果 count === 0
  185 + 1. 没过解冻期
  186 + 2. 过了解冻期, count reset to 5
  187 + */
  188 + let now = Date.now();
  189 +
  190 + if (now > expire) {
  191 + _.set(req.session, 'phoneReg.count', 5);
  192 + return next();
  193 +
  194 + } else {
  195 + return res.json({
  196 + code: 400,
  197 + message: '错误次数太多, 5分钟稍后再试'
  198 + });
  199 + }
  200 +
  201 + }
  202 +};
  203 +
118 let sendCode = (req, res, next) => { 204 let sendCode = (req, res, next) => {
119 let data = { 205 let data = {
120 code: 400, 206 code: 400,
@@ -141,9 +227,23 @@ let sendCode = (req, res, next) => { @@ -141,9 +227,23 @@ let sendCode = (req, res, next) => {
141 227
142 // 向手机发送注册验证码 228 // 向手机发送注册验证码
143 RegService.sendCodeToMobile(area, mobile).then((result) => { 229 RegService.sendCodeToMobile(area, mobile).then((result) => {
144 - return result.code ? res.json(result) : res.json(data); 230 + let code = _.get(result, 'code');
  231 +
  232 + if (code) {
  233 + --req.session.phoneReg.count;
  234 +
  235 + // count is 0, will freeze;
  236 + if (!req.session.phoneReg.count) {
  237 + _.set(req.session, 'phoneReg.expire', Date.now() + 5 * 60 * 1000);
  238 + }
  239 + return res.json(result);
  240 + } else {
  241 + return res.json(data);
  242 + }
145 }).catch(next); 243 }).catch(next);
146 }; 244 };
  245 +
  246 +
147 let verifyCode = (req, res, next) => { 247 let verifyCode = (req, res, next) => {
148 let data = { 248 let data = {
149 code: 400, 249 code: 400,
@@ -175,22 +275,31 @@ let verifyCode = (req, res, next) => { @@ -175,22 +275,31 @@ let verifyCode = (req, res, next) => {
175 return res.json(data); 275 return res.json(data);
176 } 276 }
177 277
178 - // 返回跳转到设置密码的链接  
179 - if (result.code === 200) {  
180 - let token = sign.makeToken(mobile);  
181 -  
182 - result.data = helpers.urlFormat('/passport/reg/password', {  
183 - token: token,  
184 - phoneNum: mobile,  
185 - areaCode: area  
186 - });  
187 - } else if (result.code === 404) {  
188 - result.message = '验证码错误'; // 统一验证提示 278 + let resultCode = _.get(result, 'code');
  279 + let token = sign.makeToken(mobile);
  280 +
  281 + switch (resultCode) {
  282 + case 200:
  283 + _.set(req.session, 'phoneReg.step', 3); // go step 3
  284 + result.data = helpers.urlFormat('/passport/reg/password', {
  285 + token: token,
  286 + phoneNum: mobile,
  287 + areaCode: area
  288 + });
  289 + break;
  290 + case 404:
  291 + default:
  292 + result = data;
189 } 293 }
190 294
191 return res.json(result); 295 return res.json(result);
192 }).catch(next); 296 }).catch(next);
193 }; 297 };
  298 +
  299 +/**
  300 + * Step3: set Password
  301 + */
  302 +
194 let passwordAction = (req, res, next) => { 303 let passwordAction = (req, res, next) => {
195 let token = req.query.token; 304 let token = req.query.token;
196 let mobile = +req.query.phoneNum; 305 let mobile = +req.query.phoneNum;
@@ -215,6 +324,7 @@ let passwordAction = (req, res, next) => { @@ -215,6 +324,7 @@ let passwordAction = (req, res, next) => {
215 token: token // 访问令牌 324 token: token // 访问令牌
216 }); 325 });
217 }; 326 };
  327 +
218 let setPassword = (req, res, next) => { 328 let setPassword = (req, res, next) => {
219 let data = { 329 let data = {
220 code: 400, 330 code: 400,
@@ -270,6 +380,8 @@ let setPassword = (req, res, next) => { @@ -270,6 +380,8 @@ let setPassword = (req, res, next) => {
270 refer = '/home'; 380 refer = '/home';
271 } 381 }
272 382
  383 + delete req.session.phoneNum;
  384 +
273 return res.json({ 385 return res.json({
274 code: 200, 386 code: 200,
275 message: '注册成功', 387 message: '注册成功',
@@ -287,7 +399,7 @@ let setPassword = (req, res, next) => { @@ -287,7 +399,7 @@ let setPassword = (req, res, next) => {
287 const genCaptcha = (req, res) => { 399 const genCaptcha = (req, res) => {
288 let captcha = captchaService.generateCaptcha(90, 52, 4); 400 let captcha = captchaService.generateCaptcha(90, 52, 4);
289 401
290 - req.session.regCaptch = captcha.token; 402 + _.set(req.session, 'phoneReg.captcha', captcha.text);
291 403
292 res.type('png') 404 res.type('png')
293 .set('Cache-Control', 'no-cache') 405 .set('Cache-Control', 'no-cache')
@@ -296,6 +408,8 @@ const genCaptcha = (req, res) => { @@ -296,6 +408,8 @@ const genCaptcha = (req, res) => {
296 }; 408 };
297 409
298 module.exports = { 410 module.exports = {
  411 + guardStep,
  412 + sendCodeBusyBoy,
299 index, 413 index,
300 verifyMobile, 414 verifyMobile,
301 code: codeAction, 415 code: codeAction,
@@ -86,11 +86,11 @@ router.post('/passport/bind/changeMobile', bind.changeMobile); @@ -86,11 +86,11 @@ router.post('/passport/bind/changeMobile', bind.changeMobile);
86 router.get('/passport/reg/index', reg.index); 86 router.get('/passport/reg/index', reg.index);
87 router.get('/passport/reg/captcha.png', reg.genCaptcha); 87 router.get('/passport/reg/captcha.png', reg.genCaptcha);
88 router.post('/passport/reg/verifymobile', reg.verifyMobile); 88 router.post('/passport/reg/verifymobile', reg.verifyMobile);
89 -router.get('/passport/reg/code', reg.code);  
90 -router.post('/passport/reg/sendcode', reg.sendCode);  
91 -router.post('/passport/reg/verifycode', reg.verifyCode);  
92 -router.get('/passport/reg/password', reg.password);  
93 -router.post('/passport/reg/setpassword', reg.setPassword); 89 +router.get('/passport/reg/code', reg.guardStep(2), reg.code);
  90 +router.post('/passport/reg/sendcode', reg.guardStep(2), reg.sendCodeBusyBoy, reg.sendCode);
  91 +router.post('/passport/reg/verifycode', reg.guardStep(2), reg.verifyCode);
  92 +router.get('/passport/reg/password', reg.guardStep(3), reg.password);
  93 +router.post('/passport/reg/setpassword', reg.guardStep(3), reg.setPassword);
94 94
95 /** 95 /**
96 * 密码找回 96 * 密码找回
@@ -49,6 +49,7 @@ function checkEnableNext() { @@ -49,6 +49,7 @@ function checkEnableNext() {
49 * 刷新 校验码 49 * 刷新 校验码
50 */ 50 */
51 function refreshCaptcha() { 51 function refreshCaptcha() {
  52 + $captcha.val('').focus();
52 $captchaPNG.attr('src', ['//m.yohobuy.com/passport/reg/captcha.png', '?t=', Date.now()].join('')); 53 $captchaPNG.attr('src', ['//m.yohobuy.com/passport/reg/captcha.png', '?t=', Date.now()].join(''));
53 } 54 }
54 55
@@ -101,6 +102,8 @@ $btnNext.on('touchstart', function() { @@ -101,6 +102,8 @@ $btnNext.on('touchstart', function() {
101 if (data.code === 200) { 102 if (data.code === 200) {
102 location.href = data.data; 103 location.href = data.data;
103 } else { 104 } else {
  105 + data.refreshCaptcha && refreshCaptcha();
  106 +
104 showErrTip(data.message); 107 showErrTip(data.message);
105 requested = false; 108 requested = false;
106 } 109 }
@@ -3,12 +3,11 @@ @@ -3,12 +3,11 @@
3 结构: 3 结构:
4 div.passport-captch 4 div.passport-captch
5 input 5 input
6 - div 6 + div.passport-captcha-img
7 img 7 img
8 - button  
9 8
10 emmet: 9 emmet:
11 -div.passport-captch>input+div>img+button 10 +div.passport-captcha>input+div.passport-captcha-img>img
12 11
13 */ 12 */
14 13