Authored by 毕凯

Merge branch 'feature/anti-spider' into 'master'

爬虫添加验证页面



See merge request !338
@@ -51,6 +51,11 @@ app.set('etag', false); @@ -51,6 +51,11 @@ app.set('etag', false);
51 51
52 app.enable('trust proxy'); 52 app.enable('trust proxy');
53 53
  54 +// 请求限制中间件
  55 +if (!app.locals.devEnv) {
  56 + app.use(require('./doraemon/middleware/limiter'));
  57 +}
  58 +
54 // 指定libray目录 59 // 指定libray目录
55 global.utils = path.resolve('./utils'); 60 global.utils = path.resolve('./utils');
56 61
  1 +'use strict';
  2 +const _ = require('lodash');
  3 +const cache = global.yoho.cache.master;
  4 +
  5 +exports.index = (req, res) => {
  6 + res.render('check', {
  7 + width750: true,
  8 + localCss: true
  9 + });
  10 +};
  11 +
  12 +exports.submit = (req, res) => {
  13 + let captchaCode = _.get(req.session, 'captcha');
  14 + let remoteIp = req.get('X-Forwarded-For') || req.ip;
  15 +
  16 + if (remoteIp.indexOf(',') > 0) {
  17 + let arr = remoteIp.split(',');
  18 +
  19 + remoteIp = arr[0];
  20 + }
  21 +
  22 + if (req.body.captcha === captchaCode) {
  23 + let key = `pc:limiter:${remoteIp}`;
  24 + cache.delAsync(key).then(() => {
  25 + return res.json({
  26 + code: 200
  27 + });
  28 + }).catch(() => {
  29 + return res.json({
  30 + code: 400
  31 + });
  32 + });
  33 + } else {
  34 + return res.json({
  35 + code: 400
  36 + });
  37 + }
  38 +
  39 +};
@@ -9,9 +9,12 @@ @@ -9,9 +9,12 @@
9 const router = require('express').Router(); // eslint-disable-line 9 const router = require('express').Router(); // eslint-disable-line
10 const cRoot = './controllers'; 10 const cRoot = './controllers';
11 const ads = require(`${cRoot}/ads`); 11 const ads = require(`${cRoot}/ads`);
  12 +const check = require(`${cRoot}/check`);
12 13
13 // routers 14 // routers
14 15
15 router.get('/ads', ads.index); 16 router.get('/ads', ads.index);
  17 +router.get('/check', check.index);
  18 +router.post('/check/submit', check.submit);
16 19
17 module.exports = router; 20 module.exports = router;
  1 +<div class="check-page">
  2 + <div class="title">请输入正确的验证码,继续访问</div>
  3 + <div id="js-img-check"></div>
  4 + <div class="submit">
  5 + 确认
  6 + </div>
  7 +</div>
@@ -72,7 +72,6 @@ module.exports = { @@ -72,7 +72,6 @@ module.exports = {
72 udp: { // send by udp 72 udp: { // send by udp
73 measurement: 'yohobuy_wap_node_log', 73 measurement: 'yohobuy_wap_node_log',
74 level: 'error', // logger level 74 level: 'error', // logger level
75 - host: 'influxdblog.web.yohoops.org', // influxdb host  
76 port: '4444' // influxdb port 75 port: '4444' // influxdb port
77 }, 76 },
78 console: { 77 console: {
@@ -107,7 +106,8 @@ module.exports = { @@ -107,7 +106,8 @@ module.exports = {
107 key: '7e6f3307b64cc87c79c472814b88f7fb', 106 key: '7e6f3307b64cc87c79c472814b88f7fb',
108 appSecret: 'ce21ae4a3f93852279175a167e54509b', 107 appSecret: 'ce21ae4a3f93852279175a167e54509b',
109 notifyUrl: domains.service + 'payment/weixin_notify', 108 notifyUrl: domains.service + 'payment/weixin_notify',
110 - } 109 + },
  110 + maxQps: 1200
111 }; 111 };
112 112
113 if (isProduction) { 113 if (isProduction) {
  1 +'use strict';
  2 +const limiter = require('../middleware/limiter/index');
  3 +
  4 +module.exports = (req, res, next) => {
  5 + return limiter(req, res, next);
  6 +};
  1 +'use strict';
  2 +
  3 +const _ = require('lodash');
  4 +const logger = global.yoho.logger;
  5 +const ip = require('./rules/ip-list');
  6 +const userAgent = require('./rules/useragent');
  7 +const qpsLimiter = require('./rules/qps-limit');
  8 +const fakerLimiter = require('./rules/faker-limit');
  9 +const captchaPolicy = require('./policies/captcha');
  10 +const reporterPolicy = require('./policies/reporter');
  11 +
  12 +const IP_WHITE_LIST = [
  13 + // '106.38.38.146',
  14 + // '218.94.75.58'
  15 +];
  16 +
  17 +const limiter = (rule, policy, context) => {
  18 + return rule(context, policy);
  19 +};
  20 +
  21 +module.exports = (req, res, next) => {
  22 + let remoteIp = req.get('X-Forwarded-For') || req.connection.remoteAddress;
  23 + logger.debug('request remote ip: ', remoteIp);
  24 +
  25 + if (remoteIp.indexOf(',') > 0) {
  26 + let arr = remoteIp.split(',');
  27 +
  28 + remoteIp = arr[0];
  29 + }
  30 +
  31 + const excluded = _.includes(IP_WHITE_LIST, remoteIp);
  32 + const enabled = !_.get(req.app.locals, 'wap.sys.noLimiter');
  33 +
  34 + // 判断获取remoteIp成功,并且开关未关闭
  35 + if (enabled && remoteIp && !excluded) {
  36 + const context = {
  37 + req: req,
  38 + res: res,
  39 + next: next,
  40 + remoteIp: remoteIp
  41 + };
  42 +
  43 + Promise.all([
  44 + limiter(userAgent, captchaPolicy, context),
  45 + limiter(ip, captchaPolicy, context),
  46 + limiter(qpsLimiter, captchaPolicy, context),
  47 + //limiter(fakerLimiter, reporterPolicy, context)
  48 + ]).then((results) => {
  49 + let allPass = true, exclusion = false, policy = null;
  50 +
  51 + logger.debug('limiter result: ' + JSON.stringify(results));
  52 +
  53 + _.forEach(results, (result) => {
  54 + if (typeof result === 'object' && !exclusion) {
  55 + exclusion = result.exclusion;
  56 + }
  57 +
  58 + if (!excluded && typeof result === 'function') {
  59 + allPass = false;
  60 + }
  61 +
  62 + if (typeof result === 'function') {
  63 + policy = result;
  64 + }
  65 + });
  66 +
  67 + if (exclusion) {
  68 + return next();
  69 + } else if (!allPass && policy) {
  70 + policy(req, res, next);
  71 + } else {
  72 + return next();
  73 + }
  74 +
  75 + }).catch((err) => {
  76 + logger.error(err);
  77 + return next();
  78 + });
  79 + } else {
  80 + return next();
  81 + }
  82 +};
  1 +'use strict';
  2 +
  3 +const helpers = global.yoho.helpers;
  4 +const _ = require('lodash');
  5 +
  6 +const WHITE_LIST = [
  7 + '/3party/check',
  8 + '/passport/imagesNode',
  9 + '/passport/cert/headerTip',
  10 + '/passport/captcha/get',
  11 + '/3party/check/submit'
  12 +];
  13 +
  14 +module.exports = (req, res, next) => {
  15 + let refer = req.method === 'GET' ? req.get('Referer') : '';
  16 + let limitAPI = helpers.urlFormat('/3party/check', {refer: refer});
  17 + let limitPage = helpers.urlFormat('/3party/check', {refer: req.protocol + '://' + req.get('host') + req.originalUrl});
  18 +
  19 + if (_.indexOf(WHITE_LIST, req.path) >= 0) {
  20 + return next();
  21 + }
  22 +
  23 + if (req.xhr) {
  24 + return res.json({
  25 + code: 400,
  26 + data: {refer: limitAPI}
  27 + });
  28 + }
  29 +
  30 + return res.redirect(limitPage);
  31 +};
  1 +'use strict';
  2 +
  3 +module.exports = (req, res, next) => {
  4 + return next();
  5 +};
  1 +'use strict';
  2 +
  3 +const logger = global.yoho.logger;
  4 +const cache = global.yoho.cache.master;
  5 +const ONE_DAY = 60 * 60 * 24;
  6 +
  7 +module.exports = (limiter, policy) => {
  8 + const req = limiter.req,
  9 + res = limiter.res,
  10 + next = limiter.next;
  11 +
  12 + const key = `pc:limiter:faker:${limiter.remoteIp}`;
  13 +
  14 + if (req.header('X-Requested-With') === 'XMLHttpRequest') {
  15 + cache.decrAsync(key, 1);
  16 + }
  17 +
  18 + res.on('render', function() {
  19 + cache.incrAsync(key, 1);
  20 + });
  21 +
  22 + return cache.getAsync(key).then((result) => {
  23 + if (result) {
  24 + if (result > 100) {
  25 + return Promise.resolve(policy);//policy(req, res, next);
  26 + } else {
  27 + return Promise.resolve(true);
  28 + }
  29 + } else {
  30 + cache.setAsync(key, 1, ONE_DAY); // 设置key,1m失效
  31 + return Promise.resolve(true);
  32 + }
  33 + });
  34 +};
  1 +'use strict';
  2 +
  3 +const cache = global.yoho.cache.master;
  4 +const _ = require('lodash');
  5 +
  6 +module.exports = (limiter) => {
  7 + const key = `pc:limiter:${limiter.remoteIp}`;
  8 +
  9 + return cache.getAsync(key).then((result) => {
  10 + if (result && _.isNumber(result)) {
  11 + return Promise.resolve({
  12 + exclusion: result === -1
  13 + });
  14 + } else {
  15 + return Promise.resolve(true);
  16 + }
  17 + });
  18 +};
  1 +'use strict';
  2 +
  3 +const logger = global.yoho.logger;
  4 +const cache = global.yoho.cache.master;
  5 +const config = global.yoho.config;
  6 +const ONE_DAY = 60 * 60 * 24;
  7 +const MAX_QPS = config.maxQps;
  8 +const _ = require('lodash');
  9 +
  10 +const PAGES = {
  11 + '/product/\\/pro_([\\d]+)_([\\d]+)\\/(.*)/': 5,
  12 + '/product/list/index': 5
  13 +};
  14 +
  15 +function urlJoin(a, b) {
  16 + if (_.endsWith(a, '/') && _.startsWith(b, '/')) {
  17 + return a + b.substring(1, b.length);
  18 + } else if (!_.endsWith(a, '/') && !_.startsWith(b, '/')) {
  19 + return a + '/' + b;
  20 + } else {
  21 + return a + b;
  22 + }
  23 +}
  24 +
  25 +module.exports = (limiter, policy) => {
  26 + const req = limiter.req,
  27 + res = limiter.res,
  28 + next = limiter.next;
  29 +
  30 + const key = `pc:limiter:${limiter.remoteIp}`;
  31 +
  32 + res.on('render', function() {
  33 + let route = req.route ? req.route.path : '';
  34 + let appPath = req.app.mountpath;
  35 +
  36 + if (_.isArray(route) && route.length > 0) {
  37 + route = route[0];
  38 + }
  39 +
  40 + let pageKey = urlJoin(appPath, route.toString()); // route may be a regexp
  41 + let pageIncr = PAGES[pageKey] || 0;
  42 +
  43 + if (/^\/p([\d]+)/.test(req.path)) {
  44 + pageIncr = 5;
  45 + }
  46 +
  47 + if (pageIncr > 0) {
  48 + cache.incrAsync(key, pageIncr, (err) => {});
  49 + }
  50 + });
  51 +
  52 + return cache.getAsync(key).then((result) => {
  53 + logger.debug('qps limiter: ' + key + '@' + result + ' max: ' + MAX_QPS);
  54 +
  55 + if (result && _.isNumber(result)) {
  56 +
  57 + if (result === -1) {
  58 + return Promise.resolve(true);
  59 + }
  60 +
  61 + if (result > MAX_QPS) { // 判断 qps
  62 + cache.touch(key, ONE_DAY);
  63 + logger.debug('req limit', key);
  64 +
  65 + return Promise.resolve(policy);
  66 + } else {
  67 + cache.incrAsync(key, 1); // qps + 1
  68 + return Promise.resolve(true);
  69 +
  70 + }
  71 + } else {
  72 + cache.setAsync(key, 1, 60); // 设置key,1m失效
  73 + return Promise.resolve(true);
  74 + }
  75 + });
  76 +};
  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 req = limiter.req,
  10 + res = limiter.res,
  11 + next = limiter.next;
  12 + const blackKey = 'pc:limiter:ua:black',
  13 + whiteKey = 'pc:limiter:ua:white';
  14 +
  15 + const ua = limiter.req.header('User-Agent');
  16 +
  17 + return Promise.all([
  18 + cache.getAsync(blackKey),
  19 + cache.getAsync(whiteKey)
  20 + ]).then((args) => {
  21 + const blacklist = args[0] || [], whitelist = args[1] || [];
  22 +
  23 + if (blacklist.length === 0 && whitelist.length === 0) {
  24 + return Promise.resolve(true);
  25 + }
  26 +
  27 + const test = (list) => {
  28 + let result = false;
  29 +
  30 + _.each(list, (item) => {
  31 + let regexp;
  32 +
  33 + try {
  34 + regexp = new RegExp(item);
  35 + } catch (e) {
  36 + logger.error(e);
  37 + }
  38 +
  39 + if (regexp.test(ua)) {
  40 + result = true;
  41 + }
  42 + });
  43 +
  44 + return result;
  45 + };
  46 +
  47 + if (test(blacklist)) {
  48 + return Promise.resolve(policy);
  49 + } else if (test(whitelist)) {
  50 + return Promise.resolve({
  51 + exclusion: true
  52 + });
  53 + } else {
  54 + return Promise.resolve(true);
  55 + }
  56 + });
  57 +
  58 +};
1 { 1 {
2 "name": "m-yohobuy-node", 2 "name": "m-yohobuy-node",
3 - "version": "5.4.26", 3 + "version": "5.4.27",
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": {
  1 +require('3party/check.page.css');
  2 +require('../common');
  3 +// 图片验证码
  4 +let ImgCheck = require('plugin/img-check');
  5 +
  6 +let imgCheck = new ImgCheck('#js-img-check', {
  7 + useREM: {
  8 + rootFontSize: 40,
  9 + picWidth: 150
  10 + }
  11 +});
  12 +
  13 +imgCheck.init();
  14 +
  15 +$(function() {
  16 + $('.submit').on('click', function() {
  17 + $.ajax({
  18 + method: 'POST',
  19 + url: '/3party/check/submit',
  20 + data: {
  21 + captcha: $.trim(imgCheck.getResults())
  22 + },
  23 + success: function(ret) {
  24 + if (ret.code === 200) {
  25 + window.location.href = decodeURIComponent(window.queryString.refer) || '//m.yohobuy.com';
  26 + } else {
  27 + imgCheck.refresh();
  28 + }
  29 + }
  30 + });
  31 + });
  32 +});
  1 +@import "layout/img-check";
  2 +
  3 +.check-page {
  4 + margin: 20px auto;
  5 + width: 700px;
  6 +
  7 + .submit {
  8 + width: 100%;
  9 + height: 100px;
  10 + line-height: 100px;
  11 + text-align: center;
  12 + font-size: 32px;
  13 + color: #fff;
  14 + background: #5cb85c;
  15 + border-radius: 10px;
  16 + }
  17 +}