Merge branch 'feature/anti-spider' into 'master'
爬虫添加验证页面 See merge request !338
Showing
17 changed files
with
416 additions
and
3 deletions
@@ -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 |
apps/3party/controllers/check.js
0 → 100644
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; |
apps/3party/views/action/check.hbs
0 → 100644
apps/3party/views/partial/.gitkeep
0 → 100644
@@ -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) { |
doraemon/middleware/limiter.js
0 → 100644
doraemon/middleware/limiter/index.js
0 → 100644
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 | +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 | +}; |
doraemon/middleware/limiter/rules/ip-list.js
0 → 100644
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 | +}; |
public/js/3party/check.page.js
0 → 100644
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 | +}); |
public/scss/3party/check.page.css
0 → 100644
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 | +} |
-
Please register or login to post a comment