Authored by 陈峰

Merge branch 'release/0711' into 'gray'

Release/0711

api risk

See merge request !1470
1 'use strict'; 1 'use strict';
2 2
3 const _ = require('lodash'); 3 const _ = require('lodash');
  4 +const headerModel = require('../../../doraemon/models/header');
  5 +const checkModel = require('..//models/check');
4 const decodeURIComponent = require('../../../utils/string-process').decodeURIComponent; 6 const decodeURIComponent = require('../../../utils/string-process').decodeURIComponent;
5 const logger = global.yoho.logger; 7 const logger = global.yoho.logger;
6 const Geetest = require('geetest'); 8 const Geetest = require('geetest');
@@ -14,7 +16,12 @@ const captcha = new Geetest({ @@ -14,7 +16,12 @@ const captcha = new Geetest({
14 16
15 exports.index = (req, res) => { 17 exports.index = (req, res) => {
16 req.yoho.captchaShow = false; 18 req.yoho.captchaShow = false;
17 - res.locals.useGeetest = true; 19 +
  20 + if (req.session.apiRiskValidate) {
  21 + res.locals.useRiskImg = true;
  22 + } else {
  23 + res.locals.useGeetest = true;
  24 + }
18 25
19 if (_.has(res, 'locals.loadJsBefore')) { 26 if (_.has(res, 'locals.loadJsBefore')) {
20 res.locals.loadJsBefore.push({ 27 res.locals.loadJsBefore.push({
@@ -27,89 +34,135 @@ exports.index = (req, res) => { @@ -27,89 +34,135 @@ exports.index = (req, res) => {
27 } 34 }
28 ]; 35 ];
29 } 36 }
  37 +
30 res.render('check', { 38 res.render('check', {
  39 + pageHeader: headerModel.setNav({
  40 + navTitle: '友情提醒'
  41 + }),
31 width750: true, 42 width750: true,
32 localCss: true 43 localCss: true
33 }); 44 });
34 }; 45 };
35 46
36 -exports.submit = (req, res) => {  
37 - co(function * () {  
38 - let challenge = req.body.geetest_challenge,  
39 - validate = req.body.geetest_validate,  
40 - seccode = req.body.geetest_seccode;  
41 -  
42 - let errRes = {  
43 - code: 400,  
44 - message: '验证码错误',  
45 - captchaShow: true,  
46 - changeCaptcha: true  
47 - };  
48 -  
49 - if (!challenge || !validate || !seccode) {  
50 - return res.json(errRes); 47 +
  48 +const submitValidate = {
  49 + errRes: {
  50 + code: 400,
  51 + message: '验证码错误',
  52 + captchaShow: true,
  53 + changeCaptcha: true
  54 + },
  55 + clearLimitIp(req) {
  56 + let remoteIp = req.yoho.clientIp;
  57 +
  58 + if (remoteIp.indexOf(',') > 0) {
  59 + let arr = remoteIp.split(',');
  60 +
  61 + remoteIp = arr[0];
51 } 62 }
52 63
53 - let geetestRes = yield captcha.validate({  
54 - challenge,  
55 - validate,  
56 - seccode  
57 - }); 64 + // pc:limiter:IP 和PC端共用
  65 + let operations = [cache.delAsync(`pc:limiter:${remoteIp}`)];
58 66
59 - if (geetestRes) {  
60 - logger.info('geetest success'); 67 + // 验证码之后一小时之内不再限制qps
  68 + if (req.session.apiLimitValidate || req.session.apiRiskValidate) {
  69 + operations.push(cache.setAsync(
  70 + `${config.app}:limiter:api:ishuman:${remoteIp}`,
  71 + 1,
  72 + config.LIMITER_IP_TIME
  73 + ));
  74 + } else {
  75 + operations.push(cache.setAsync(
  76 + `${config.app}:limiter:ishuman:${remoteIp}`,
  77 + 1,
  78 + config.LIMITER_IP_TIME
  79 + ));
  80 + }
  81 +
  82 + delete req.session.apiLimitValidate;
  83 + delete req.session.apiRiskValidate;
  84 +
  85 + if (req.body.pid) {
  86 + let riskPid = decodeURIComponent(req.body.pid) + ':' + _.get(req.yoho, 'clientIp', '');
  87 +
  88 + operations.push(cache.delAsync(riskPid));
  89 + }
  90 +
  91 + _.forEach(config.REQUEST_LIMIT, (val, key) => {
  92 + operations.push(cache.delAsync(`${config.app}:limiter:${key}:max:${remoteIp}`));
  93 + });
61 94
62 - let remoteIp = req.yoho.clientIp; 95 + return Promise.all(operations);
  96 + },
  97 + geetest(req, res) {
  98 + const self = this;
63 99
64 - if (remoteIp.indexOf(',') > 0) {  
65 - let arr = remoteIp.split(','); 100 + co(function * () {
  101 + let challenge = req.body.geetest_challenge,
  102 + validate = req.body.geetest_validate,
  103 + seccode = req.body.geetest_seccode;
66 104
67 - remoteIp = arr[0]; 105 + if (!challenge || !validate || !seccode) {
  106 + return res.json(self.errRes);
68 } 107 }
69 108
70 - // pc:limiter:IP 和PC端共用  
71 - let operations = [cache.delAsync(`pc:limiter:${remoteIp}`)]; 109 + let geetestRes = yield captcha.validate({
  110 + challenge,
  111 + validate,
  112 + seccode
  113 + });
  114 +
  115 + if (geetestRes) {
  116 + logger.info('geetest success');
  117 +
  118 + yield self.clearLimitIp(req);
72 119
73 - // 验证码之后一小时之内不再限制qps  
74 - if (req.session.apiLimitValidate) {  
75 - operations.push(cache.setAsync(  
76 - `${config.app}:limiter:api:ishuman:${remoteIp}`,  
77 - 1,  
78 - config.LIMITER_IP_TIME  
79 - )); 120 + // 图形验证码关闭时通过极验证后解锁接口风控
  121 + if (req.session.apiRiskClear) {
  122 + delete req.session.apiRiskClear;
  123 + yield req.ctx(checkModel).verifyImgCheckRisk(req.cookies.udid, '1,2,3,4').catch(console.error);
  124 + }
  125 +
  126 + return res.json({
  127 + code: 200
  128 + });
80 } else { 129 } else {
81 - operations.push(cache.setAsync(  
82 - `${config.app}:limiter:ishuman:${remoteIp}`,  
83 - 1,  
84 - config.LIMITER_IP_TIME  
85 - )); 130 + logger.info('geetest faild');
  131 + return res.json(self.errRes);
86 } 132 }
87 133
88 - delete req.session.apiLimitValidate; 134 + })();
  135 + },
  136 + imgCheckRisk(req, res) {
  137 + const self = this;
89 138
90 - if (req.body.pid) {  
91 - let riskPid = decodeURIComponent(req.body.pid) + ':' + _.get(req.yoho, 'clientIp', ''); 139 + co(function * () {
  140 + let result = yield req.ctx(checkModel).verifyImgCheckRisk(req.cookies.udid, req.body.captcha);
92 141
93 - operations.push(cache.delAsync(riskPid));  
94 - } 142 + if (result.code === 200) {
  143 + yield self.clearLimitIp(req);
95 144
96 - _.forEach(config.REQUEST_LIMIT, (val, key) => {  
97 - operations.push(cache.delAsync(`${config.app}:limiter:${key}:max:${remoteIp}`));  
98 - }); 145 + return res.json(result);
  146 + } else {
  147 + logger.info('api risk img verify faild');
  148 + return res.json(self.errRes);
  149 + }
  150 + })();
  151 + }
  152 +};
99 153
100 - yield Promise.all(operations); 154 +exports.submit = (req, res) => {
  155 + let validateType = 'geetest';
101 156
102 - return res.json({  
103 - code: 200  
104 - });  
105 - } else {  
106 - logger.info('geetest faild');  
107 - return res.json(errRes);  
108 - } 157 + if (req.session.apiRiskValidate && req.body.apiRiskValidate) {
  158 + validateType = 'imgCheckRisk';
  159 + }
109 160
110 - })().catch(() => { 161 + try {
  162 + return submitValidate[validateType](req, res);
  163 + } catch (err) {
111 return res.json({ 164 return res.json({
112 code: 400 165 code: 400
113 }); 166 });
114 - }); 167 + }
115 }; 168 };
@@ -116,7 +116,7 @@ const siteMap = (req, res, next) => { @@ -116,7 +116,7 @@ const siteMap = (req, res, next) => {
116 let siteList = ['m', 'list', 'item', 'guang'], 116 let siteList = ['m', 'list', 'item', 'guang'],
117 subdomain = req.subdomains[1] || 'm'; 117 subdomain = req.subdomains[1] || 'm';
118 118
119 - if (_.find(siteList, subdomain)) { 119 + if (_.indexOf(siteList, subdomain) < 0) {
120 res.end('end'); 120 res.end('end');
121 return; 121 return;
122 } 122 }
  1 +const PAGE = 'H5';
  2 +const logger = global.yoho.logger;
  3 +
  4 +module.exports = class extends global.yoho.BaseModel {
  5 + constructor(ctx) {
  6 + super(ctx);
  7 + }
  8 +
  9 + verifyImgCheckRisk(udid, degrees) {
  10 + return this.get({
  11 + data: {
  12 + method: 'app.graphic.verify',
  13 + udid: udid,
  14 + fromPage: PAGE,
  15 + degrees: degrees
  16 + }
  17 + }).then(result => {
  18 + logger.info(`app.graphic.verify result: ${JSON.stringify(result)}`);
  19 + return result;
  20 + });
  21 + }
  22 +};
1 <div class="check-page"> 1 <div class="check-page">
2 - <div class="title">请确认之后,继续访问</div> 2 + <p class="wran-tip">
  3 + <i class="iconfont">&#xe628;</i>
  4 + 您的操作太频繁了~请完成以下操作后继续
  5 + </p>
3 {{!--图片验证--}} 6 {{!--图片验证--}}
4 - <div data-geetest="{{useGeetest}}" id="js-img-check"></div> 7 + <div{{#if useGeetest}} data-geetest="true"{{/if}}{{#if useRiskImg}} data-riskimg="true"{{/if}} id="js-img-check"></div>
5 <div class="submit"> 8 <div class="submit">
6 确认 9 确认
7 </div> 10 </div>
@@ -38,6 +38,37 @@ exports.imgCheck = (req, res, next) => { @@ -38,6 +38,37 @@ exports.imgCheck = (req, res, next) => {
38 }).catch(next); 38 }).catch(next);
39 }; 39 };
40 40
  41 +exports.imgCheckRisk = (req, res, next) => {
  42 + if (!req.session.apiRiskValidate) {
  43 + return next();
  44 + }
  45 +
  46 + return req.ctx(imgCheckServiceModel).getRiskCheckImg(req.cookies.udid).then(result => {
  47 + return request({
  48 + url: result,
  49 + headers: {
  50 + 'X-request-ID': req.reqID || '',
  51 + 'X-YOHO-IP': req.yoho.clientIp || '',
  52 + 'X-Forwarded-For': req.yoho.clientIp || '',
  53 + 'User-Agent': 'yoho/nodejs'
  54 + }
  55 + }).on('response', response => {
  56 + // status code 204 接口关闭图形验证码,通过cookie通知验证页刷行切换验证方式
  57 + if (response.statusCode === 204) {
  58 + res.cookie('refresh_page', 1, {
  59 + path: '/',
  60 + maxAge: 60000
  61 + });
  62 +
  63 + delete req.session.apiRiskValidate;
  64 + req.session.apiRiskClear = true;
  65 +
  66 + return res.json({code: 204});
  67 + }
  68 + }).pipe(res); // eslint-disable-line
  69 + }).catch(next);
  70 +};
  71 +
41 /** 72 /**
42 * 验证img-check验证码 73 * 验证img-check验证码
43 */ 74 */
@@ -2,7 +2,10 @@ @@ -2,7 +2,10 @@
2 const PAGE = 'H5'; 2 const PAGE = 'H5';
3 const logger = global.yoho.logger; 3 const logger = global.yoho.logger;
4 const serviceAPI = global.yoho.ServiceAPI.ApiUrl; 4 const serviceAPI = global.yoho.ServiceAPI.ApiUrl;
  5 +const ApiUrl = global.yoho.API.ApiUrl;
5 const config = global.yoho.config; 6 const config = global.yoho.config;
  7 +const sign = global.yoho.sign;
  8 +const querystring = require('querystring');
6 9
7 module.exports = class extends global.yoho.BaseModel { 10 module.exports = class extends global.yoho.BaseModel {
8 constructor(ctx) { 11 constructor(ctx) {
@@ -57,4 +60,12 @@ module.exports = class extends global.yoho.BaseModel { @@ -57,4 +60,12 @@ module.exports = class extends global.yoho.BaseModel {
57 return result; 60 return result;
58 }); 61 });
59 } 62 }
  63 +
  64 + getRiskCheckImg(udid) {
  65 + return Promise.resolve(`${ApiUrl}?${querystring.stringify(sign.apiSign({
  66 + method: 'app.graphic.img',
  67 + udid,
  68 + fromPage: PAGE
  69 + }))}`);
  70 + }
60 }; 71 };
@@ -133,6 +133,7 @@ let captcha = require('./controllers/captcha'); @@ -133,6 +133,7 @@ let captcha = require('./controllers/captcha');
133 133
134 router.get('/passport/captcha/get', captcha.get); 134 router.get('/passport/captcha/get', captcha.get);
135 router.get('/passport/img-check.jpg', captcha.imgCheck); 135 router.get('/passport/img-check.jpg', captcha.imgCheck);
  136 +router.get('/passport/img-check-risk.jpg', captcha.imgCheckRisk);
136 137
137 /** 138 /**
138 * 注册 139 * 注册
@@ -14,6 +14,9 @@ const pathWhiteList = require('./limiter/rules/path-white-list'); @@ -14,6 +14,9 @@ const pathWhiteList = require('./limiter/rules/path-white-list');
14 const ipWhiteList = require('./limiter/rules/ip-white-list'); 14 const ipWhiteList = require('./limiter/rules/ip-white-list');
15 const _ = require('lodash'); 15 const _ = require('lodash');
16 16
  17 +const replaceKey = '${refer}';
  18 +const checkRefer = helpers.urlFormat('/3party/check', {refer: replaceKey});
  19 +
17 const forceNoCache = (res) => { 20 const forceNoCache = (res) => {
18 if (res && !res.finished) { 21 if (res && !res.finished) {
19 res.set({ 22 res.set({
@@ -166,35 +169,42 @@ exports.serverError = () => { @@ -166,35 +169,42 @@ exports.serverError = () => {
166 refer: req.originalUrl 169 refer: req.originalUrl
167 })); 170 }));
168 } 171 }
169 - } else if (err.code === 9999991 || err.code === 9999992) { 172 + } else if (err.apiRisk || err.code === 9999991 || err.code === 9999992) {
170 let remoteIp = req.yoho.clientIp; 173 let remoteIp = req.yoho.clientIp;
171 174
172 - if (!_.includes(pathWhiteList(), req.path) && !(await ipWhiteList(remoteIp))) {  
173 - const isHuman = await cache.getAsync(`${config.app}:limiter:api:ishuman:${remoteIp}`); 175 + if (!err.apiRisk && (_.includes(pathWhiteList(), req.path) || (await ipWhiteList(remoteIp)))) {
  176 + return _err510(req, res, 510, err);
  177 + }
174 178
175 - if (!isHuman) {  
176 - if (remoteIp.indexOf(',') > 0) {  
177 - let arr = remoteIp.split(','); 179 + if (remoteIp.indexOf(',') > 0) {
  180 + let arr = remoteIp.split(',');
178 181
179 - remoteIp = arr[0];  
180 - }  
181 - cache.setAsync(`${config.app}:limiter:${remoteIp}`, 1, config.LIMITER_IP_TIME); 182 + remoteIp = arr[0];
  183 + }
182 184
183 - let limitAPI = helpers.urlFormat('/3party/check', {refer: req.get('Referer') || ''});  
184 - let limitPage = helpers.urlFormat('/3party/check', {  
185 - refer: req.protocol + '://' + req.get('host') + req.originalUrl  
186 - }); 185 + let sessionLimitKey;
  186 + let isHuman;
  187 +
  188 + if (err.apiRisk) {
  189 + sessionLimitKey = 'apiRiskValidate';
  190 + } else {
  191 + sessionLimitKey = 'apiLimitValidate';
  192 + isHuman = await cache.getAsync(`${config.app}:limiter:api:ishuman:${remoteIp}`);
  193 + }
  194 +
  195 + if (!isHuman) {
  196 + cache.setAsync(`${config.app}:limiter:${remoteIp}`, 1, config.LIMITER_IP_TIME);
187 197
188 - req.session.apiLimitValidate = true;  
189 - if (req.xhr) {  
190 - return res.status(510).json({  
191 - code: err.code,  
192 - data: {refer: limitAPI}  
193 - });  
194 - } 198 + req.session[sessionLimitKey] = true;
195 199
196 - return res.redirect(limitPage); 200 + if (req.xhr) {
  201 + return res.status(510).json({
  202 + code: err.code,
  203 + data: {refer: checkRefer.replace(replaceKey, req.get('Referer') || '')}
  204 + });
197 } 205 }
  206 +
  207 + return res.redirect(checkRefer.replace(replaceKey, req.protocol + '://' + req.get('host') + req.originalUrl));
198 } 208 }
199 209
200 return _err510(req, res, 510, err); 210 return _err510(req, res, 510, err);
@@ -9,6 +9,7 @@ const DEFAULT_PATH_WHITE_LIST = [ @@ -9,6 +9,7 @@ const DEFAULT_PATH_WHITE_LIST = [
9 '/3party/check/submit', 9 '/3party/check/submit',
10 '/passport/captcha/get', 10 '/passport/captcha/get',
11 '/passport/img-check.jpg', 11 '/passport/img-check.jpg',
  12 + '/passport/img-check-risk.jpg',
12 '/passport/geetest/register', 13 '/passport/geetest/register',
13 '/activity/individuation', 14 '/activity/individuation',
14 '/activity/individuation/coupon', 15 '/activity/individuation/coupon',
1 { 1 {
2 "name": "yohobuywap-node", 2 "name": "yohobuywap-node",
3 - "version": "6.6.19", 3 + "version": "6.6.20",
4 "private": true, 4 "private": true,
5 "description": "A New Yohobuy Project With Express", 5 "description": "A New Yohobuy Project With Express",
6 "repository": { 6 "repository": {
@@ -87,7 +87,7 @@ @@ -87,7 +87,7 @@
87 "xml2js": "^0.4.19", 87 "xml2js": "^0.4.19",
88 "yoho-express-session": "^2.0.0", 88 "yoho-express-session": "^2.0.0",
89 "yoho-md5": "^2.0.0", 89 "yoho-md5": "^2.0.0",
90 - "yoho-node-lib": "=0.6.17", 90 + "yoho-node-lib": "=0.6.18",
91 "yoho-zookeeper": "^1.0.10" 91 "yoho-zookeeper": "^1.0.10"
92 }, 92 },
93 "devDependencies": { 93 "devDependencies": {
1 require('3party/check.page.css'); 1 require('3party/check.page.css');
2 require('common'); 2 require('common');
3 3
4 -// 图片验证码  
5 -let Validate = require('plugin/validata'); 4 +let $ = require('yoho-jquery'),
  5 + Validate = require('plugin/validata');
6 6
7 -let validate = new Validate('#js-img-check', { 7 +let $check = $('#js-img-check');
  8 +
  9 +let baseInfo = {pid: window.queryString.pid};
  10 +let validateOptions = {
8 useREM: { 11 useREM: {
9 rootFontSize: 40, 12 rootFontSize: 40,
10 - picWidth: 150 13 + picWidth: 140
11 } 14 }
12 -}); 15 +};
  16 +
  17 +if ($check.data('riskimg')) {
  18 + validateOptions.imgSrc = '/passport/img-check-risk.jpg';
  19 + baseInfo.apiRiskValidate = true;
  20 +}
  21 +
  22 +let validate = new Validate($check, validateOptions);
13 23
14 validate.init(); 24 validate.init();
15 25
16 $(function() { 26 $(function() {
  27 +
  28 + // 定时监测cookie中refresh_page刷新页面也换验证方式
  29 + setInterval(function() {
  30 + if (window.cookie('refresh_page') > 0) {
  31 + window.setCookie('refresh_page', 0, {
  32 + path: '/'
  33 + });
  34 + window.location.reload();
  35 + }
  36 + }, 1000);
  37 +
17 $('.submit').on('click', function() { 38 $('.submit').on('click', function() {
18 validate.getResults().then((result) => { 39 validate.getResults().then((result) => {
19 - $.extend(result, {pid: window.queryString.pid}); 40 + $.extend(result, baseInfo);
20 41
21 $.ajax({ 42 $.ajax({
22 method: 'POST', 43 method: 'POST',
@@ -148,8 +148,9 @@ ImgCheck.prototype = { @@ -148,8 +148,9 @@ ImgCheck.prototype = {
148 */ 148 */
149 refresh: function() { 149 refresh: function() {
150 const self = this; 150 const self = this;
  151 + const imgSrc = self.imgSrc || '/passport/img-check.jpg';
151 152
152 - self.render({imgSrc: `/passport/img-check.jpg?t=${Date.now()}`}); 153 + self.render({imgSrc: `${imgSrc}?t=${Date.now()}`});
153 }, 154 },
154 155
155 156
@@ -2,16 +2,50 @@ @@ -2,16 +2,50 @@
2 2
3 .check-page { 3 .check-page {
4 margin: 20px auto; 4 margin: 20px auto;
5 - width: 700px; 5 + width: 590px;
  6 +
  7 + .wran-tip {
  8 + font-size: 30px;
  9 + color: #444;
  10 + text-align: center;
  11 + margin-bottom: 150px;
  12 + margin-top: 60px;
  13 +
  14 + .iconfont {
  15 + font-size: 110px;
  16 + display: block;
  17 + color: #bbb;
  18 + }
  19 + }
  20 +
  21 + .img-check-header {
  22 + font-size: 24px;
  23 + }
  24 +
  25 + .img-check-pics > li {
  26 + width: 140px;
  27 + height: 140px;
  28 + background-size: 560px 560px;
  29 + position: relative;
  30 +
  31 + &:before {
  32 + content: "";
  33 + position: absolute;
  34 + width: 100%;
  35 + height: 100%;
  36 + box-sizing: border-box;
  37 + border: 1px solid #e8e8e8;
  38 + }
  39 + }
6 40
7 .submit { 41 .submit {
8 width: 100%; 42 width: 100%;
9 - height: 100px;  
10 - line-height: 100px; 43 + height: 90px;
  44 + line-height: 90px;
11 text-align: center; 45 text-align: center;
12 - font-size: 32px; 46 + font-size: 30px;
13 color: #fff; 47 color: #fff;
14 - background: #5cb85c;  
15 - border-radius: 10px; 48 + background: #444;
  49 + border-radius: 8px;
16 } 50 }
17 } 51 }