Authored by yyq

api risk

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,129 @@ exports.index = (req, res) => { @@ -27,89 +34,129 @@ 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');
72 117
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 - )); 118 + yield self.clearLimitIp(req);
  119 +
  120 + return res.json({
  121 + code: 200
  122 + });
80 } else { 123 } else {
81 - operations.push(cache.setAsync(  
82 - `${config.app}:limiter:ishuman:${remoteIp}`,  
83 - 1,  
84 - config.LIMITER_IP_TIME  
85 - )); 124 + logger.info('geetest faild');
  125 + return res.json(self.errRes);
86 } 126 }
87 127
88 - delete req.session.apiLimitValidate; 128 + })();
  129 + },
  130 + imgCheckRisk(req, res) {
  131 + const self = this;
89 132
90 - if (req.body.pid) {  
91 - let riskPid = decodeURIComponent(req.body.pid) + ':' + _.get(req.yoho, 'clientIp', ''); 133 + co(function * () {
  134 + let result = yield req.ctx(checkModel).verifyImgCheckRisk(req.cookies.udid, req.body.captcha);
92 135
93 - operations.push(cache.delAsync(riskPid));  
94 - } 136 + if (result.code === 200) {
  137 + yield self.clearLimitIp(req);
95 138
96 - _.forEach(config.REQUEST_LIMIT, (val, key) => {  
97 - operations.push(cache.delAsync(`${config.app}:limiter:${key}:max:${remoteIp}`));  
98 - }); 139 + return res.json(result);
  140 + } else {
  141 + logger.info('api risk img verify faild');
  142 + return res.json(self.errRes);
  143 + }
  144 + })();
  145 + }
  146 +};
99 147
100 - yield Promise.all(operations); 148 +exports.submit = (req, res) => {
  149 + let validateType = 'geetest';
101 150
102 - return res.json({  
103 - code: 200  
104 - });  
105 - } else {  
106 - logger.info('geetest faild');  
107 - return res.json(errRes);  
108 - } 151 + if (req.session.apiRiskValidate && req.body.apiRiskValidate) {
  152 + validateType = 'imgCheckRisk';
  153 + }
109 154
110 - })().catch(() => { 155 + try {
  156 + return submitValidate[validateType](req, res);
  157 + } catch (err) {
111 return res.json({ 158 return res.json({
112 code: 400 159 code: 400
113 }); 160 });
114 - }); 161 + }
115 }; 162 };
  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,24 @@ exports.imgCheck = (req, res, next) => { @@ -38,6 +38,24 @@ 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 + }).pipe(res); // eslint-disable-line
  56 + }).catch(next);
  57 +};
  58 +
41 /** 59 /**
42 * 验证img-check验证码 60 * 验证img-check验证码
43 */ 61 */
@@ -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,11 @@ module.exports = class extends global.yoho.BaseModel { @@ -57,4 +60,11 @@ 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}/passport/img-verify?${querystring.stringify(sign.apiSign({
  66 + udid,
  67 + fromPage: PAGE
  68 + }))}`);
  69 + }
60 }; 70 };
@@ -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 * 注册
@@ -13,6 +13,9 @@ const routeEncode = require('./route-encode'); @@ -13,6 +13,9 @@ const routeEncode = require('./route-encode');
13 const pathWhiteList = require('./limiter/rules/path-white-list'); 13 const pathWhiteList = require('./limiter/rules/path-white-list');
14 const _ = require('lodash'); 14 const _ = require('lodash');
15 15
  16 +const replaceKey = '${refer}';
  17 +const checkRefer = helpers.urlFormat('/3party/check', {refer: replaceKey});
  18 +
16 const forceNoCache = (res) => { 19 const forceNoCache = (res) => {
17 if (res && !res.finished) { 20 if (res && !res.finished) {
18 res.set({ 21 res.set({
@@ -163,35 +166,42 @@ exports.serverError = () => { @@ -163,35 +166,42 @@ exports.serverError = () => {
163 refer: req.originalUrl 166 refer: req.originalUrl
164 })); 167 }));
165 } 168 }
166 - } else if (err.code === 9999991 || err.code === 9999992) {  
167 - if (!_.includes(pathWhiteList(), req.path)) {  
168 - let remoteIp = req.yoho.clientIp; 169 + } else if (err.apiRisk || err.code === 9999991 || err.code === 9999992) {
  170 + if (!err.apiRisk && _.includes(pathWhiteList(), req.path)) {
  171 + return _err510(req, res, 510, err);
  172 + }
169 173
170 - const isHuman = await cache.getAsync(`${config.app}:limiter:api:ishuman:${remoteIp}`); 174 + let remoteIp = req.yoho.clientIp;
171 175
172 - if (!isHuman) {  
173 - if (remoteIp.indexOf(',') > 0) {  
174 - let arr = remoteIp.split(','); 176 + if (remoteIp.indexOf(',') > 0) {
  177 + let arr = remoteIp.split(',');
175 178
176 - remoteIp = arr[0];  
177 - }  
178 - cache.setAsync(`${config.app}:limiter:${remoteIp}`, 1, config.LIMITER_IP_TIME); 179 + remoteIp = arr[0];
  180 + }
179 181
180 - let limitAPI = helpers.urlFormat('/3party/check', {refer: req.get('Referer') || ''});  
181 - let limitPage = helpers.urlFormat('/3party/check', {  
182 - refer: req.protocol + '://' + req.get('host') + req.originalUrl  
183 - }); 182 + let sessionLimitKey;
  183 + let isHuman;
  184 +
  185 + if (err.apiRisk) {
  186 + sessionLimitKey = 'apiRiskValidate';
  187 + } else {
  188 + sessionLimitKey = 'apiLimitValidate';
  189 + isHuman = await cache.getAsync(`${config.app}:limiter:api:ishuman:${remoteIp}`);
  190 + }
  191 +
  192 + if (!isHuman) {
  193 + cache.setAsync(`${config.app}:limiter:${remoteIp}`, 1, config.LIMITER_IP_TIME);
184 194
185 - req.session.apiLimitValidate = true;  
186 - if (req.xhr) {  
187 - return res.status(510).json({  
188 - code: err.code,  
189 - data: {refer: limitAPI}  
190 - });  
191 - } 195 + req.session[sessionLimitKey] = true;
192 196
193 - return res.redirect(limitPage); 197 + if (req.xhr) {
  198 + return res.status(510).json({
  199 + code: err.code,
  200 + data: {refer: checkRefer.replace(replaceKey, req.get('Referer') || '')}
  201 + });
194 } 202 }
  203 +
  204 + return res.redirect(checkRefer.replace(replaceKey, req.protocol + '://' + req.get('host') + req.originalUrl));
195 } 205 }
196 206
197 return _err510(req, res, 510, err); 207 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 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() {
17 $('.submit').on('click', function() { 27 $('.submit').on('click', function() {
18 validate.getResults().then((result) => { 28 validate.getResults().then((result) => {
19 - $.extend(result, {pid: window.queryString.pid}); 29 + $.extend(result, baseInfo);
20 30
21 $.ajax({ 31 $.ajax({
22 method: 'POST', 32 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 }