Authored by 陈峰

Merge branch 'feature/risk' into 'release/9.19'

Feature/risk



See merge request !1544
@@ -137,6 +137,10 @@ app.use((req, res, next) => { @@ -137,6 +137,10 @@ app.use((req, res, next) => {
137 next(); 137 next();
138 }); 138 });
139 139
  140 +function isOpenApmRisk(req) {
  141 + return _.get(req.app.locals, 'wap.open.apmrisk', false);
  142 +}
  143 +
140 // dispatcher 144 // dispatcher
141 try { 145 try {
142 const tdkUrl = require('./doraemon/middleware/redis-url'); 146 const tdkUrl = require('./doraemon/middleware/redis-url');
@@ -154,6 +158,8 @@ try { @@ -154,6 +158,8 @@ try {
154 const pageCache = require('./doraemon/middleware/page-cache'); 158 const pageCache = require('./doraemon/middleware/page-cache');
155 const downloadBar = require('./doraemon/middleware/download-bar'); 159 const downloadBar = require('./doraemon/middleware/download-bar');
156 const routeEncode = require('./doraemon/middleware/route-encode'); 160 const routeEncode = require('./doraemon/middleware/route-encode');
  161 + const ifElseMd = require('./doraemon/middleware/ifElseMd');
  162 + const riskManagementApm = require('./doraemon/middleware/risk-management2');
157 const riskManagement = require('./doraemon/middleware/risk-management'); 163 const riskManagement = require('./doraemon/middleware/risk-management');
158 const statics = require('./doraemon/middleware/statics'); 164 const statics = require('./doraemon/middleware/statics');
159 165
@@ -168,7 +174,10 @@ try { @@ -168,7 +174,10 @@ try {
168 174
169 // 请求限制中间件 175 // 请求限制中间件
170 if (!app.locals.devEnv) { 176 if (!app.locals.devEnv) {
171 - app.use(require('./doraemon/middleware/limiter')); 177 + const limiter = require('./doraemon/middleware/limiter');
  178 + const limiterApm = require('./doraemon/middleware/limiter/index2');
  179 +
  180 + app.use(ifElseMd(isOpenApmRisk, limiterApm, limiter));
172 } 181 }
173 182
174 if (app.locals.devEnv) { 183 if (app.locals.devEnv) {
@@ -181,7 +190,7 @@ try { @@ -181,7 +190,7 @@ try {
181 app.use(pageCache()); 190 app.use(pageCache());
182 app.use(routeEncode.md); 191 app.use(routeEncode.md);
183 app.use(downloadBar()); 192 app.use(downloadBar());
184 - app.use(riskManagement()); 193 + app.use(ifElseMd(isOpenApmRisk, riskManagementApm(), riskManagement()));
185 app.use(statics(app)); 194 app.use(statics(app));
186 195
187 require('./dispatch')(app); 196 require('./dispatch')(app);
@@ -44,6 +44,7 @@ exports.index = (req, res) => { @@ -44,6 +44,7 @@ exports.index = (req, res) => {
44 }); 44 });
45 }; 45 };
46 46
  47 +const limitKey = 'limit2';
47 48
48 const submitValidate = { 49 const submitValidate = {
49 errRes: { 50 errRes: {
@@ -54,6 +55,7 @@ const submitValidate = { @@ -54,6 +55,7 @@ const submitValidate = {
54 }, 55 },
55 clearLimitIp(req) { 56 clearLimitIp(req) {
56 let remoteIp = req.yoho.clientIp; 57 let remoteIp = req.yoho.clientIp;
  58 + let operations = [];
57 59
58 if (remoteIp.indexOf(',') > 0) { 60 if (remoteIp.indexOf(',') > 0) {
59 let arr = remoteIp.split(','); 61 let arr = remoteIp.split(',');
@@ -61,7 +63,32 @@ const submitValidate = { @@ -61,7 +63,32 @@ const submitValidate = {
61 remoteIp = arr[0]; 63 remoteIp = arr[0];
62 } 64 }
63 65
64 - let operations = [cache.delAsync(`${config.app}:limiter:${remoteIp}`)]; 66 + const isOpenApmrisk = _.get(req.app.locals, 'wap.open.apmrisk', false);
  67 +
  68 + // 新的计数
  69 + if (isOpenApmrisk) {
  70 + operations.push(cache.delAsync(`${config.app}:${limitKey}:${remoteIp}`));
  71 +
  72 + if (req.session.apiLimitValidate || req.session.apiRiskValidate) {
  73 + operations.push(cache.setAsync(
  74 + `${config.app}:limiter:api:ishuman:${remoteIp}`,
  75 + 1,
  76 + config.LIMITER_IP_TIME
  77 + ));
  78 + } else {
  79 + operations.push(cache.setAsync(
  80 + `${config.app}:${limitKey}:ishuman:${remoteIp}`,
  81 + 1,
  82 + config.LIMITER_IP_TIME
  83 + ));
  84 +
  85 + }
  86 +
  87 + _.forEach(config.REQUEST_LIMIT, (val, key) => {
  88 + operations.push(cache.delAsync(`${config.app}:${limitKey}:${key}:max:${remoteIp}`));
  89 + });
  90 + } else {
  91 + operations.push(cache.delAsync(`${config.app}:limiter:${remoteIp}`));
65 92
66 // 验证码之后一小时之内不再限制qps 93 // 验证码之后一小时之内不再限制qps
67 if (req.session.apiLimitValidate || req.session.apiRiskValidate) { 94 if (req.session.apiLimitValidate || req.session.apiRiskValidate) {
@@ -78,6 +105,11 @@ const submitValidate = { @@ -78,6 +105,11 @@ const submitValidate = {
78 )); 105 ));
79 } 106 }
80 107
  108 + _.forEach(config.REQUEST_LIMIT, (val, key) => {
  109 + operations.push(cache.delAsync(`${config.app}:limiter:${key}:max:${remoteIp}`));
  110 + });
  111 + }
  112 +
81 delete req.session.apiLimitValidate; 113 delete req.session.apiLimitValidate;
82 delete req.session.apiRiskValidate; 114 delete req.session.apiRiskValidate;
83 115
@@ -87,10 +119,6 @@ const submitValidate = { @@ -87,10 +119,6 @@ const submitValidate = {
87 operations.push(cache.delAsync(riskPid)); 119 operations.push(cache.delAsync(riskPid));
88 } 120 }
89 121
90 - _.forEach(config.REQUEST_LIMIT, (val, key) => {  
91 - operations.push(cache.delAsync(`${config.app}:limiter:${key}:max:${remoteIp}`));  
92 - });  
93 -  
94 return Promise.all(operations); 122 return Promise.all(operations);
95 }, 123 },
96 geetest(req, res) { 124 geetest(req, res) {
@@ -135,7 +163,7 @@ const submitValidate = { @@ -135,7 +163,7 @@ const submitValidate = {
135 imgCheckRisk(req, res) { 163 imgCheckRisk(req, res) {
136 const self = this; 164 const self = this;
137 165
138 - co(function * () { 166 + co(function*() {
139 let result = yield req.ctx(checkModel).verifyImgCheckRisk(req.cookies.udid, req.body.captcha); 167 let result = yield req.ctx(checkModel).verifyImgCheckRisk(req.cookies.udid, req.body.captcha);
140 168
141 if (result.code === 200) { 169 if (result.code === 200) {
@@ -200,7 +200,11 @@ exports.serverError = () => { @@ -200,7 +200,11 @@ exports.serverError = () => {
200 } 200 }
201 201
202 if (!isHuman) { 202 if (!isHuman) {
  203 + if (_.get(req.app.locals, 'wap.open.apmrisk', false)) {
  204 + cache.setAsync(`${config.app}:limit2:${remoteIp}`, 1, config.LIMITER_IP_TIME);
  205 + } else {
203 cache.setAsync(`${config.app}:limiter:${remoteIp}`, 1, config.LIMITER_IP_TIME); 206 cache.setAsync(`${config.app}:limiter:${remoteIp}`, 1, config.LIMITER_IP_TIME);
  207 + }
204 208
205 req.session[sessionLimitKey] = true; 209 req.session[sessionLimitKey] = true;
206 210
  1 +
  2 +
  3 +module.exports = (predict, ifTrueFn, elseFn) => {
  4 + if (!ifTrueFn) {
  5 + ifTrueFn = (req, res, next) => next();
  6 + }
  7 +
  8 + if (!elseFn) {
  9 + elseFn = (req, res, next) => next();
  10 + }
  11 +
  12 + return (req, res, next) => {
  13 + if (predict(req, res)) {
  14 + ifTrueFn(req, res, next);
  15 + } else {
  16 + elseFn(req, res, next);
  17 + }
  18 + };
  19 +};
  1 +'use strict';
  2 +
  3 +const _ = require('lodash');
  4 +const logger = global.yoho.logger;
  5 +const ip = require('./rules/ip-list2');
  6 +const userAgent = require('./rules/useragent2');
  7 +
  8 +// const asynchronous = require('./rules/asynchronous');
  9 +// const fakerLimiter = require('./rules/faker-limit');
  10 +const captchaPolicy = require('./policies/captcha');
  11 +
  12 +// const reporterPolicy = require('./policies/reporter');
  13 +
  14 +const limiter = (rule, policy, context) => {
  15 + return rule(context, policy);
  16 +};
  17 +
  18 +module.exports = (req, res, next) => {
  19 + const remoteIp = req.yoho.clientIp || '';
  20 + const enabled = !_.get(req.app.locals, 'wap.sys.noLimiter');
  21 +
  22 + // 开关为关或者未获取到remoteIp,放行
  23 + if (!enabled || !remoteIp) {
  24 + logger.debug(`request remote ip: ${remoteIp}; enabled: ${enabled}`);
  25 + return next();
  26 + }
  27 +
  28 + (async function() {
  29 + const context = {
  30 + req: req,
  31 + res: res,
  32 + next: next,
  33 + remoteIp: remoteIp
  34 + };
  35 +
  36 + let results = await Promise.all([
  37 + limiter(userAgent, captchaPolicy, context),
  38 + limiter(ip, captchaPolicy, context),
  39 + ]);
  40 +
  41 + let allPass = true, exclusion = false, policy = null;
  42 +
  43 + logger.debug('limiter result: ' + JSON.stringify(results));
  44 +
  45 + _.forEach(results, (result) => {
  46 + if (typeof result === 'object' && !exclusion) {
  47 + exclusion = result.exclusion;
  48 + }
  49 +
  50 + if (typeof result === 'function') {
  51 + allPass = false;
  52 + policy = result;
  53 + }
  54 + });
  55 +
  56 + if (exclusion) {
  57 + return next();
  58 + } else if (!allPass && policy) {
  59 + policy(req, res, next);
  60 + } else {
  61 + return next();
  62 + }
  63 + }()).catch(err => {
  64 + logger.error(err);
  65 + return next();
  66 + });
  67 +};
  1 +'use strict';
  2 +
  3 +const cache = global.yoho.cache.master;
  4 +const logger = global.yoho.logger;
  5 +const config = global.yoho.config;
  6 +
  7 +const limitKey = 'limit2';
  8 +
  9 +module.exports = (limiter, policy) => {
  10 + const ipBlackKey = `pc:limiter:${limiter.remoteIp}`; // ci ip黑名单
  11 + const ipLimitKey = `${config.app}:${limitKey}:${limiter.remoteIp}`; // 业务黑名单
  12 +
  13 + return Promise.all([
  14 + cache.getAsync(ipBlackKey),
  15 + cache.getAsync(ipLimitKey)
  16 + ]).then(result => {
  17 + let ipBlackRes = result[0];
  18 + let ipLimitRes = result[1];
  19 +
  20 + logger.debug(ipBlackKey, ipBlackRes);
  21 + logger.debug(ipLimitKey, ipLimitRes);
  22 +
  23 + if ((ipBlackRes && +ipBlackRes > 0) || (ipLimitRes && +ipLimitRes > 0)) {
  24 + return Promise.resolve(policy);
  25 + } else {
  26 + return Promise.resolve(true);
  27 + }
  28 + });
  29 +};
  1 +'use strict';
  2 +
  3 +const cache = global.yoho.cache.master;
  4 +const _ = require('lodash');
  5 +const logger = global.yoho.logger;
  6 +
  7 +
  8 +module.exports = (limiter, policy) => {
  9 + const blackKey = 'pc:limiter:ua:black';
  10 +
  11 + const ua = limiter.req.header('User-Agent');
  12 +
  13 + cache.getAsync(blackKey).then((args) => {
  14 + let blacklist = [];
  15 +
  16 + try {
  17 + blacklist = JSON.parse(args) || [];
  18 + } catch (error) {
  19 + logger.error(error);
  20 + }
  21 +
  22 + if (blacklist.length === 0) {
  23 + return Promise.resolve(true);
  24 + }
  25 +
  26 + const test = (list) => {
  27 + let result = false;
  28 +
  29 + _.each(list, (item) => {
  30 + let regexp;
  31 +
  32 + try {
  33 + regexp = new RegExp(item);
  34 + } catch (e) {
  35 + logger.error(e);
  36 + }
  37 +
  38 + if (regexp.test(ua)) {
  39 + result = true;
  40 + }
  41 + });
  42 +
  43 + return result;
  44 + };
  45 +
  46 + if (test(blacklist)) {
  47 + return Promise.resolve(policy);
  48 + } else {
  49 + return Promise.resolve(true);
  50 + }
  51 + });
  52 +
  53 +};
  1 +/**
  2 + * 控制路由请求次数
  3 + * @date: 2018/03/05
  4 + */
  5 +'use strict';
  6 +
  7 +const _ = require('lodash');
  8 +const cache = global.yoho.cache.master;
  9 +const helpers = global.yoho.helpers;
  10 +const pathToRegexp = require('path-to-regexp');
  11 +const logger = global.yoho.logger;
  12 +const md5 = require('yoho-md5');
  13 +
  14 +const statusCode = {
  15 + code: 4403,
  16 + data: {},
  17 + message: '亲,您的访问次数过多,请稍后再试哦...'
  18 +};
  19 +
  20 +const _jumpUrl = (req, res, next, result) => {
  21 + if (result.code === 4403) {
  22 + if (req.xhr) {
  23 + res.set({
  24 + 'Cache-Control': 'no-cache',
  25 + Pragma: 'no-cache',
  26 + Expires: (new Date(1900, 0, 1, 0, 0, 0, 0)).toUTCString()
  27 + });
  28 + return res.status(403).json(result);
  29 + }
  30 + return res.redirect(`${result.data.url}&refer=${req.originalUrl}`);
  31 + }
  32 +
  33 + return next();
  34 +};
  35 +
  36 +const limitKey = 'limit2';
  37 +
  38 +module.exports = () => {
  39 + return (req, res, next) => {
  40 + // default open
  41 + if (_.get(req.app.locals.wap, 'close.risk', false)) {
  42 + return next();
  43 + }
  44 +
  45 + let ip = _.get(req.yoho, 'clientIp', '');
  46 + let path = req.path || '';
  47 + let risks = _.get(req.app.locals.wap, 'json.risk', []);
  48 + let router = {};
  49 +
  50 + logger.debug(`risk => risks: ${JSON.stringify(risks)}, path: ${path}, ip: ${ip}`); // eslint-disable-line
  51 + if (_.isEmpty(path) || _.isEmpty(risks)) {
  52 + return next();
  53 + }
  54 +
  55 + _.isArray(risks) && risks.some(item => {
  56 + if (item.state === 'off') {
  57 + return false;
  58 + }
  59 +
  60 + if (!item.regRoute) {
  61 + item.regRoute = pathToRegexp(item.route);
  62 + }
  63 +
  64 + if (item.regRoute.test(path)) {
  65 + router = item;
  66 + return true;
  67 + }
  68 +
  69 + return false;
  70 + });
  71 +
  72 + logger.debug(`risk => router: ${JSON.stringify(router)}, path: ${path}`); // eslint-disable-line
  73 + if (_.isEmpty(router)) {
  74 + return next();
  75 + }
  76 +
  77 + let keyPath = md5(`${router.regRoute}`);
  78 + let limitEnable = `wap:risk:${limitKey}:${keyPath}:${ip}`;
  79 + let checkUrl = helpers.urlFormat('/3party/check', {
  80 + pid: `wap:risk:${limitKey}:${keyPath}`
  81 + });
  82 +
  83 + cache.getAsync(limitEnable)
  84 + .then(result => {
  85 + logger.debug(`risk => getCache: ${JSON.stringify(result)}, path: ${path}`); // eslint-disable-line
  86 + if (result) {
  87 + return Object.assign({}, statusCode, {
  88 + data: {
  89 + url: checkUrl
  90 + }
  91 + });
  92 + } else {
  93 + return {
  94 + code: 200
  95 + };
  96 + }
  97 + }).then(result => {
  98 + logger.debug(`risk => result: ${JSON.stringify(result)}, path: ${path}`); // eslint-disable-line
  99 + return _jumpUrl(req, res, next, result);
  100 + }).catch(e => {
  101 + console.log(`risk => path: ${path}, err: ${e.message}`);
  102 + return next();
  103 + });
  104 + };
  105 +};