Authored by 毕凯

Merge remote-tracking branch 'origin/feature/limiter' into gray

... ... @@ -4,6 +4,7 @@ const _ = require('lodash');
const decodeURIComponent = require('../../../utils/string-process').decodeURIComponent;
const logger = global.yoho.logger;
const Geetest = require('geetest');
const config = global.yoho.config;
const co = Promise.coroutine;
const cache = global.yoho.cache.master;
const captcha = new Geetest({
... ... @@ -66,21 +67,18 @@ exports.submit = (req, res) => {
remoteIp = arr[0];
}
let key = `pc:limiter:${remoteIp}`;
let key10m = `pc:limiter:10m:${remoteIp}`;
let dels = [
cache.delAsync(key),
cache.delAsync(key10m)
];
let operations = [cache.delAsync(`${config.app}:limiter:${remoteIp}`)];
if (req.body.pid) {
dels.push(cache.delAsync(decodeURIComponent(req.body.pid)));
operations.push(cache.delAsync(decodeURIComponent(req.body.pid)));
}
yield Promise.all(dels).catch(e => {
console.log(`check.js err: ${e.message}`);
_.forEach(config.REQUEST_LIMIT, (val, key) => {
operations.push(cache.delAsync(`${config.app}:limiter:${key}:max:${remoteIp}`));
});
yield Promise.all(operations);
return res.json({
code: 200
});
... ...
... ... @@ -126,8 +126,6 @@ module.exports = {
appSecret: 'ce21ae4a3f93852279175a167e54509b',
notifyUrl: domains.service + 'payment/weixin_notify',
},
maxQps: 1200,
maxQps10m: 2500,
geetestJs: '//static.geetest.com/static/tools/gt.js',
jsSdk: '//cdn.yoho.cn/js-sdk/1.3.0/jssdk.js',
redis: {
... ... @@ -149,6 +147,19 @@ module.exports = {
}
}
},
REQUEST_LIMIT: {
// 10s 最多访问5次
10: 10,
// 30s 最多访问15次
30: 20,
// 60s 最多访问15次
60: 30,
// 100s 最多访问15次
600: 100
},
superCapture: '93c70db61fe276f93ce781ad17dc47cd',
from: from
};
... ...
... ... @@ -5,15 +5,33 @@ const logger = global.yoho.logger;
const ip = require('./rules/ip-list');
const userAgent = require('./rules/useragent');
const qpsLimiter = require('./rules/qps-limit');
// const asynchronous = require('./rules/asynchronous');
// const fakerLimiter = require('./rules/faker-limit');
const captchaPolicy = require('./policies/captcha');
// const reporterPolicy = require('./policies/reporter');
const IP_WHITE_LIST = [
'106.38.38.146',
'106.38.38.147',
'106.39.86.227',
'218.94.75.58',
'218.94.75.50',
'218.94.77.166'
];
const PATH_WHITE_LIST = [
'/3party/check',
'/3party/check/submit',
'/passport/imagesNode',
'/passport/cert/headerTip',
'/passport/captcha/get',
'/passport/images',
'/passport/img-check.jpg',
'/passport/geetest/register'
];
const limiter = (rule, policy, context) => {
return rule(context, policy);
};
... ... @@ -33,8 +51,12 @@ module.exports = (req, res, next) => {
remoteIp = req.get('X-Real-IP');
}
const excluded = _.includes(IP_WHITE_LIST, remoteIp);
const enabled = !_.get(req.app.locals, 'wap.sys.noLimiter');
// 排除条件:ip白名单/路径白名单/异步请求/登录用户
const excluded = _.includes(IP_WHITE_LIST, remoteIp) ||
_.includes(PATH_WHITE_LIST, req.path) || req.xhr || !_.isEmpty(_.get(req, 'user.uid'));
const enabled = !_.get(req.app.locals, 'wap.sys.noLimite');
logger.debug(`request remote ip: ${remoteIp}; excluded: ${excluded}; enabled: ${enabled}`);
// 判断获取remoteIp成功,并且开关未关闭
if (enabled && remoteIp && !excluded) {
... ... @@ -48,8 +70,9 @@ module.exports = (req, res, next) => {
Promise.all([
limiter(userAgent, captchaPolicy, context),
limiter(ip, captchaPolicy, context),
limiter(qpsLimiter, captchaPolicy, context),
limiter(qpsLimiter, captchaPolicy, context)
// limiter(asynchronous, captchaPolicy, context)
// limiter(fakerLimiter, reporterPolicy, context)
]).then((results) => {
let allPass = true, exclusion = false, policy = null;
... ... @@ -61,11 +84,8 @@ module.exports = (req, res, next) => {
exclusion = result.exclusion;
}
if (!excluded && typeof result === 'function') {
allPass = false;
}
if (typeof result === 'function') {
allPass = false;
policy = result;
}
});
... ...
... ... @@ -2,7 +2,6 @@
const helpers = global.yoho.helpers;
const _ = require('lodash');
const WHITE_LIST = [
'/3party/check',
'/3party/check/submit',
... ... @@ -23,7 +22,7 @@ module.exports = (req, res, next) => {
return next();
}
if (_.toNumber(res.statusCode) === 403) {
if (res.statusCode === 403) {
return res.end();
}
... ...
'use strict';
const cache = global.yoho.cache.master;
const _ = require('lodash');
const logger = global.yoho.logger;
const ASYNCHRONOUSPAGES = {
'/product/index/isFavoriteShop': 1,
'/common/suggestfeedback': 1,
'/product/detail/hotarea': 1,
'/common/getbanner': 1,
'/passport/cert/headerTip': 1
};
function isNormalSpider(userAgent) {
let normalReg = /(spider)|(bot.html)/i;
if (normalReg.test(userAgent)) {
return true;
} else {
return false;
}
}
module.exports = (limiter, policy) => {
const ua = limiter.req.header('User-Agent');
const synchronizeKey = `wap:limiter:synchronize:${limiter.remoteIp}`; // 同步
const asynchronousKey = `wap:limiter:asynchronous:${limiter.remoteIp}`; // 异步
const spiderKey = `wap:limiter:spider:${limiter.remoteIp}`; // 异步
// 正常蜘蛛直接过
if (isNormalSpider(ua)) {
return Promise.resolve(true);
}
const req = limiter.req,
res = limiter.res;
res.on('render', function() {
cache.incrAsync(synchronizeKey, 1).catch(e=>console.log(e)); // eslint-disable-line
});
return cache.getMultiAsync([synchronizeKey, asynchronousKey, spiderKey]).then((results) => {
logger.debug(results);
if (results[spiderKey]) {
logger.info(results);
return Promise.resolve(policy);
}
// 默认数据设置
if (!results[synchronizeKey] && !_.isNumber(results[synchronizeKey])) {
cache.setAsync(synchronizeKey, 1, 600);
}
// 默认数据设置
if (ASYNCHRONOUSPAGES[req.path] > 0) {
cache.setAsync(asynchronousKey, 1, 600);
cache.setAsync(synchronizeKey, 1, 600);
}
if (results[synchronizeKey] > 10 && !results[asynchronousKey]) {
cache.setAsync(spiderKey, 1, 60 * 60 * 24);
logger.info(results);
return Promise.resolve(policy);
}
return Promise.resolve(true);
});
};
... ...
'use strict';
const logger = global.yoho.logger; // eslint-disable-line
const cache = global.yoho.cache.master;
const ONE_DAY = 60 * 60 * 24;
module.exports = (limiter, policy) => {
const req = limiter.req,
res = limiter.res;
res = limiter.res,
next = limiter.next; // eslint-disable-line
const key = `pc:limiter:faker:${limiter.remoteIp}`;
const key = `wap:limiter:faker:${limiter.remoteIp}`;
if (req.header('X-Requested-With') === 'XMLHttpRequest') {
cache.decrAsync(key, 1);
... ... @@ -20,7 +22,7 @@ module.exports = (limiter, policy) => {
return cache.getAsync(key).then((result) => {
if (result) {
if (result > 100) {
return Promise.resolve(policy);// policy(req, res, next);
return Promise.resolve(policy); // policy(req, res, next);
} else {
return Promise.resolve(true);
}
... ...
... ... @@ -2,15 +2,17 @@
const cache = global.yoho.cache.master;
const _ = require('lodash');
const config = global.yoho.config;
const logger = global.yoho.logger;
module.exports = (limiter) => {
const key = `pc:limiter:${limiter.remoteIp}`;
module.exports = (limiter, policy) => {
const key = `${config.app}:limiter:${limiter.remoteIp}`;
return cache.getAsync(key).then((result) => {
logger.debug(key, result);
if (result && _.isNumber(result)) {
return Promise.resolve({
exclusion: result === -1
});
return Promise.resolve(policy);
} else {
return Promise.resolve(true);
}
... ...
/**
* 限制页面访问次数,如超过限制次数,返回相应策略(目前是ip加入黑名单,跳转图形验证码页面,解除限制)
* 当前规则只针对未登录用户
*/
'use strict';
const logger = global.yoho.logger;
const cache = global.yoho.cache.master;
const config = global.yoho.config;
const ONE_DAY = 60 * 60 * 24;
const MAX_QPS = config.maxQps;
const MAX_QPS_10m = config.maxQps10m;
const _ = require('lodash');
const PAGES = {
'/product/^\\/(\\d+)\\.html/': 5,
'/product/list/index': 5,
'/product/index/index': 5,
'/product/search/list': 5
};
// 超出访问限制ip限制访问1小时
const limiterIpTime = 3600;
function urlJoin(a, b) {
if (_.endsWith(a, '/') && _.startsWith(b, '/')) {
return a + b.substring(1, b.length);
} else if (!_.endsWith(a, '/') && !_.startsWith(b, '/')) {
return a + '/' + b;
} else {
return a + b;
}
}
// 页面访问限制
const MAX_TIMES = config.REQUEST_LIMIT;
module.exports = (limiter, policy) => {
const req = limiter.req,
res = limiter.res;
const key = `pc:limiter:${limiter.remoteIp}`;
const key10m = `pc:limiter:10m:${limiter.remoteIp}`;
res.on('render', function() {
let route = req.route ? req.route.path : '';
let appPath = req.app.mountpath;
if (_.isArray(route) && route.length > 0) {
route = route[0];
}
// 存储规则的cache keys
let ruleKeys = {};
let getOp = {};
let pageKey = urlJoin(appPath, route.toString()); // route may be a regexp
let pageIncr = PAGES[pageKey] || 0;
if (pageIncr > 0) {
cache.incrAsync(key, pageIncr);
cache.incrAsync(key10m, pageIncr);
}
_.forEach(MAX_TIMES, (val, key) => {
ruleKeys[key] = `${config.app}:limiter:${key}:max:${limiter.remoteIp}`;
getOp[key] = cache.getAsync(ruleKeys[key]);
});
return cache.getMultiAsync([key, key10m]).then((results) => {
let result = results[key];
let result10m = results[key10m];
logger.debug('qps limiter: ' + key + '@' + result + ' max: ' + MAX_QPS);
logger.debug('qps limiter 10m: ' + key10m + '@' + result10m + ' max: ' + MAX_QPS_10m);
// 默认数据设置
if (!result && !_.isNumber(result)) {
cache.setAsync(key, 1, 60); // 设置key,1m失效
}
if (!result10m && !_.isNumber(result10m)) {
cache.setAsync(key10m, 1, 600); // 设置key,10m失效
}
return Promise.props(getOp).then((results) => {
// 第一次访问,都没计数,直接过
if (!result && !_.isNumber(result) && !result10m && !_.isNumber(result10m)) {
return Promise.resolve(true);
}
logger.debug(MAX_TIMES);
logger.debug(_.values(ruleKeys));
logger.debug(results);
if (result === -1 || result10m === -1) {
return Promise.resolve(true);
}
// 遍历限制规则,若满足返回相应处理策略, 否则页面访问次数加1
let operation = [];
// 判断 qps 10分钟
if (result10m > MAX_QPS_10m) {
cache.touch(key10m, ONE_DAY);
logger.debug('req limit', key10m);
_.forEach(MAX_TIMES, (val, key) => {
let cacheKey = ruleKeys[key];
return Promise.resolve(policy);
}
if (!results[key]) {
operation.push(cache.setAsync(cacheKey, 1, +key));
} else if (+results[key] > +val) {
// 判断 qps 1分钟
if (result > MAX_QPS) {
cache.touch(key, ONE_DAY);
logger.debug('req limit', key);
// ip限制1小时
operation.push(cache.setAsync(`${config.app}:limiter:${limiter.remoteIp}`, 1, limiterIpTime));
return Promise.resolve(policy);
} else {
operation.push(cache.incrAsync(cacheKey, 1));
}
});
return Promise.resolve(policy);
}
Promise.all(operation);
cache.incrAsync(key, 1); // qps + 1
cache.incrAsync(key10m, 1); // qps + 1
// 不满足任何限制规则,继续访问
return Promise.resolve(true);
}).catch(err=>{
logger.error(err);
});
};
... ...
... ... @@ -6,8 +6,8 @@ const logger = global.yoho.logger;
module.exports = (limiter, policy) => {
const blackKey = 'pc:limiter:ua:black',
whiteKey = 'pc:limiter:ua:white';
const blackKey = 'wap:limiter:ua:black',
whiteKey = 'wap:limiter:ua:white';
const ua = limiter.req.header('User-Agent');
... ... @@ -15,7 +15,8 @@ module.exports = (limiter, policy) => {
cache.getAsync(blackKey),
cache.getAsync(whiteKey)
]).then((args) => {
const blacklist = args[0] || [], whitelist = args[1] || [];
const blacklist = args[0] || [],
whitelist = args[1] || [];
if (blacklist.length === 0 && whitelist.length === 0) {
return Promise.resolve(true);
... ...
{
"name": "yohobuywap-node",
"version": "6.5.20",
"version": "96.5.21",
"private": true,
"description": "A New Yohobuy Project With Express",
"repository": {
... ...