Authored by 陈峰

Merge branch 'release/0711' into 'gray'

Release/0711

api risk

See merge request !1470
'use strict';
const _ = require('lodash');
const headerModel = require('../../../doraemon/models/header');
const checkModel = require('..//models/check');
const decodeURIComponent = require('../../../utils/string-process').decodeURIComponent;
const logger = global.yoho.logger;
const Geetest = require('geetest');
... ... @@ -14,7 +16,12 @@ const captcha = new Geetest({
exports.index = (req, res) => {
req.yoho.captchaShow = false;
res.locals.useGeetest = true;
if (req.session.apiRiskValidate) {
res.locals.useRiskImg = true;
} else {
res.locals.useGeetest = true;
}
if (_.has(res, 'locals.loadJsBefore')) {
res.locals.loadJsBefore.push({
... ... @@ -27,89 +34,135 @@ exports.index = (req, res) => {
}
];
}
res.render('check', {
pageHeader: headerModel.setNav({
navTitle: '友情提醒'
}),
width750: true,
localCss: true
});
};
exports.submit = (req, res) => {
co(function * () {
let challenge = req.body.geetest_challenge,
validate = req.body.geetest_validate,
seccode = req.body.geetest_seccode;
let errRes = {
code: 400,
message: '验证码错误',
captchaShow: true,
changeCaptcha: true
};
if (!challenge || !validate || !seccode) {
return res.json(errRes);
const submitValidate = {
errRes: {
code: 400,
message: '验证码错误',
captchaShow: true,
changeCaptcha: true
},
clearLimitIp(req) {
let remoteIp = req.yoho.clientIp;
if (remoteIp.indexOf(',') > 0) {
let arr = remoteIp.split(',');
remoteIp = arr[0];
}
let geetestRes = yield captcha.validate({
challenge,
validate,
seccode
});
// pc:limiter:IP 和PC端共用
let operations = [cache.delAsync(`pc:limiter:${remoteIp}`)];
if (geetestRes) {
logger.info('geetest success');
// 验证码之后一小时之内不再限制qps
if (req.session.apiLimitValidate || req.session.apiRiskValidate) {
operations.push(cache.setAsync(
`${config.app}:limiter:api:ishuman:${remoteIp}`,
1,
config.LIMITER_IP_TIME
));
} else {
operations.push(cache.setAsync(
`${config.app}:limiter:ishuman:${remoteIp}`,
1,
config.LIMITER_IP_TIME
));
}
delete req.session.apiLimitValidate;
delete req.session.apiRiskValidate;
if (req.body.pid) {
let riskPid = decodeURIComponent(req.body.pid) + ':' + _.get(req.yoho, 'clientIp', '');
operations.push(cache.delAsync(riskPid));
}
_.forEach(config.REQUEST_LIMIT, (val, key) => {
operations.push(cache.delAsync(`${config.app}:limiter:${key}:max:${remoteIp}`));
});
let remoteIp = req.yoho.clientIp;
return Promise.all(operations);
},
geetest(req, res) {
const self = this;
if (remoteIp.indexOf(',') > 0) {
let arr = remoteIp.split(',');
co(function * () {
let challenge = req.body.geetest_challenge,
validate = req.body.geetest_validate,
seccode = req.body.geetest_seccode;
remoteIp = arr[0];
if (!challenge || !validate || !seccode) {
return res.json(self.errRes);
}
// pc:limiter:IP 和PC端共用
let operations = [cache.delAsync(`pc:limiter:${remoteIp}`)];
let geetestRes = yield captcha.validate({
challenge,
validate,
seccode
});
if (geetestRes) {
logger.info('geetest success');
yield self.clearLimitIp(req);
// 验证码之后一小时之内不再限制qps
if (req.session.apiLimitValidate) {
operations.push(cache.setAsync(
`${config.app}:limiter:api:ishuman:${remoteIp}`,
1,
config.LIMITER_IP_TIME
));
// 图形验证码关闭时通过极验证后解锁接口风控
if (req.session.apiRiskClear) {
delete req.session.apiRiskClear;
yield req.ctx(checkModel).verifyImgCheckRisk(req.cookies.udid, '1,2,3,4').catch(console.error);
}
return res.json({
code: 200
});
} else {
operations.push(cache.setAsync(
`${config.app}:limiter:ishuman:${remoteIp}`,
1,
config.LIMITER_IP_TIME
));
logger.info('geetest faild');
return res.json(self.errRes);
}
delete req.session.apiLimitValidate;
})();
},
imgCheckRisk(req, res) {
const self = this;
if (req.body.pid) {
let riskPid = decodeURIComponent(req.body.pid) + ':' + _.get(req.yoho, 'clientIp', '');
co(function * () {
let result = yield req.ctx(checkModel).verifyImgCheckRisk(req.cookies.udid, req.body.captcha);
operations.push(cache.delAsync(riskPid));
}
if (result.code === 200) {
yield self.clearLimitIp(req);
_.forEach(config.REQUEST_LIMIT, (val, key) => {
operations.push(cache.delAsync(`${config.app}:limiter:${key}:max:${remoteIp}`));
});
return res.json(result);
} else {
logger.info('api risk img verify faild');
return res.json(self.errRes);
}
})();
}
};
yield Promise.all(operations);
exports.submit = (req, res) => {
let validateType = 'geetest';
return res.json({
code: 200
});
} else {
logger.info('geetest faild');
return res.json(errRes);
}
if (req.session.apiRiskValidate && req.body.apiRiskValidate) {
validateType = 'imgCheckRisk';
}
})().catch(() => {
try {
return submitValidate[validateType](req, res);
} catch (err) {
return res.json({
code: 400
});
});
}
};
... ...
... ... @@ -116,7 +116,7 @@ const siteMap = (req, res, next) => {
let siteList = ['m', 'list', 'item', 'guang'],
subdomain = req.subdomains[1] || 'm';
if (_.find(siteList, subdomain)) {
if (_.indexOf(siteList, subdomain) < 0) {
res.end('end');
return;
}
... ...
const PAGE = 'H5';
const logger = global.yoho.logger;
module.exports = class extends global.yoho.BaseModel {
constructor(ctx) {
super(ctx);
}
verifyImgCheckRisk(udid, degrees) {
return this.get({
data: {
method: 'app.graphic.verify',
udid: udid,
fromPage: PAGE,
degrees: degrees
}
}).then(result => {
logger.info(`app.graphic.verify result: ${JSON.stringify(result)}`);
return result;
});
}
};
... ...
<div class="check-page">
<div class="title">请确认之后,继续访问</div>
<p class="wran-tip">
<i class="iconfont">&#xe628;</i>
您的操作太频繁了~请完成以下操作后继续
</p>
{{!--图片验证--}}
<div data-geetest="{{useGeetest}}" id="js-img-check"></div>
<div{{#if useGeetest}} data-geetest="true"{{/if}}{{#if useRiskImg}} data-riskimg="true"{{/if}} id="js-img-check"></div>
<div class="submit">
确认
</div>
... ...
... ... @@ -38,6 +38,37 @@ exports.imgCheck = (req, res, next) => {
}).catch(next);
};
exports.imgCheckRisk = (req, res, next) => {
if (!req.session.apiRiskValidate) {
return next();
}
return req.ctx(imgCheckServiceModel).getRiskCheckImg(req.cookies.udid).then(result => {
return request({
url: result,
headers: {
'X-request-ID': req.reqID || '',
'X-YOHO-IP': req.yoho.clientIp || '',
'X-Forwarded-For': req.yoho.clientIp || '',
'User-Agent': 'yoho/nodejs'
}
}).on('response', response => {
// status code 204 接口关闭图形验证码,通过cookie通知验证页刷行切换验证方式
if (response.statusCode === 204) {
res.cookie('refresh_page', 1, {
path: '/',
maxAge: 60000
});
delete req.session.apiRiskValidate;
req.session.apiRiskClear = true;
return res.json({code: 204});
}
}).pipe(res); // eslint-disable-line
}).catch(next);
};
/**
* 验证img-check验证码
*/
... ...
... ... @@ -2,7 +2,10 @@
const PAGE = 'H5';
const logger = global.yoho.logger;
const serviceAPI = global.yoho.ServiceAPI.ApiUrl;
const ApiUrl = global.yoho.API.ApiUrl;
const config = global.yoho.config;
const sign = global.yoho.sign;
const querystring = require('querystring');
module.exports = class extends global.yoho.BaseModel {
constructor(ctx) {
... ... @@ -57,4 +60,12 @@ module.exports = class extends global.yoho.BaseModel {
return result;
});
}
getRiskCheckImg(udid) {
return Promise.resolve(`${ApiUrl}?${querystring.stringify(sign.apiSign({
method: 'app.graphic.img',
udid,
fromPage: PAGE
}))}`);
}
};
... ...
... ... @@ -133,6 +133,7 @@ let captcha = require('./controllers/captcha');
router.get('/passport/captcha/get', captcha.get);
router.get('/passport/img-check.jpg', captcha.imgCheck);
router.get('/passport/img-check-risk.jpg', captcha.imgCheckRisk);
/**
* 注册
... ...
... ... @@ -14,6 +14,9 @@ const pathWhiteList = require('./limiter/rules/path-white-list');
const ipWhiteList = require('./limiter/rules/ip-white-list');
const _ = require('lodash');
const replaceKey = '${refer}';
const checkRefer = helpers.urlFormat('/3party/check', {refer: replaceKey});
const forceNoCache = (res) => {
if (res && !res.finished) {
res.set({
... ... @@ -166,35 +169,42 @@ exports.serverError = () => {
refer: req.originalUrl
}));
}
} else if (err.code === 9999991 || err.code === 9999992) {
} else if (err.apiRisk || err.code === 9999991 || err.code === 9999992) {
let remoteIp = req.yoho.clientIp;
if (!_.includes(pathWhiteList(), req.path) && !(await ipWhiteList(remoteIp))) {
const isHuman = await cache.getAsync(`${config.app}:limiter:api:ishuman:${remoteIp}`);
if (!err.apiRisk && (_.includes(pathWhiteList(), req.path) || (await ipWhiteList(remoteIp)))) {
return _err510(req, res, 510, err);
}
if (!isHuman) {
if (remoteIp.indexOf(',') > 0) {
let arr = remoteIp.split(',');
if (remoteIp.indexOf(',') > 0) {
let arr = remoteIp.split(',');
remoteIp = arr[0];
}
cache.setAsync(`${config.app}:limiter:${remoteIp}`, 1, config.LIMITER_IP_TIME);
remoteIp = arr[0];
}
let limitAPI = helpers.urlFormat('/3party/check', {refer: req.get('Referer') || ''});
let limitPage = helpers.urlFormat('/3party/check', {
refer: req.protocol + '://' + req.get('host') + req.originalUrl
});
let sessionLimitKey;
let isHuman;
if (err.apiRisk) {
sessionLimitKey = 'apiRiskValidate';
} else {
sessionLimitKey = 'apiLimitValidate';
isHuman = await cache.getAsync(`${config.app}:limiter:api:ishuman:${remoteIp}`);
}
if (!isHuman) {
cache.setAsync(`${config.app}:limiter:${remoteIp}`, 1, config.LIMITER_IP_TIME);
req.session.apiLimitValidate = true;
if (req.xhr) {
return res.status(510).json({
code: err.code,
data: {refer: limitAPI}
});
}
req.session[sessionLimitKey] = true;
return res.redirect(limitPage);
if (req.xhr) {
return res.status(510).json({
code: err.code,
data: {refer: checkRefer.replace(replaceKey, req.get('Referer') || '')}
});
}
return res.redirect(checkRefer.replace(replaceKey, req.protocol + '://' + req.get('host') + req.originalUrl));
}
return _err510(req, res, 510, err);
... ...
... ... @@ -9,6 +9,7 @@ const DEFAULT_PATH_WHITE_LIST = [
'/3party/check/submit',
'/passport/captcha/get',
'/passport/img-check.jpg',
'/passport/img-check-risk.jpg',
'/passport/geetest/register',
'/activity/individuation',
'/activity/individuation/coupon',
... ...
{
"name": "yohobuywap-node",
"version": "6.6.19",
"version": "6.6.20",
"private": true,
"description": "A New Yohobuy Project With Express",
"repository": {
... ... @@ -87,7 +87,7 @@
"xml2js": "^0.4.19",
"yoho-express-session": "^2.0.0",
"yoho-md5": "^2.0.0",
"yoho-node-lib": "=0.6.17",
"yoho-node-lib": "=0.6.18",
"yoho-zookeeper": "^1.0.10"
},
"devDependencies": {
... ...
require('3party/check.page.css');
require('common');
// 图片验证码
let Validate = require('plugin/validata');
let $ = require('yoho-jquery'),
Validate = require('plugin/validata');
let validate = new Validate('#js-img-check', {
let $check = $('#js-img-check');
let baseInfo = {pid: window.queryString.pid};
let validateOptions = {
useREM: {
rootFontSize: 40,
picWidth: 150
picWidth: 140
}
});
};
if ($check.data('riskimg')) {
validateOptions.imgSrc = '/passport/img-check-risk.jpg';
baseInfo.apiRiskValidate = true;
}
let validate = new Validate($check, validateOptions);
validate.init();
$(function() {
// 定时监测cookie中refresh_page刷新页面也换验证方式
setInterval(function() {
if (window.cookie('refresh_page') > 0) {
window.setCookie('refresh_page', 0, {
path: '/'
});
window.location.reload();
}
}, 1000);
$('.submit').on('click', function() {
validate.getResults().then((result) => {
$.extend(result, {pid: window.queryString.pid});
$.extend(result, baseInfo);
$.ajax({
method: 'POST',
... ...
... ... @@ -148,8 +148,9 @@ ImgCheck.prototype = {
*/
refresh: function() {
const self = this;
const imgSrc = self.imgSrc || '/passport/img-check.jpg';
self.render({imgSrc: `/passport/img-check.jpg?t=${Date.now()}`});
self.render({imgSrc: `${imgSrc}?t=${Date.now()}`});
},
... ...
... ... @@ -2,16 +2,50 @@
.check-page {
margin: 20px auto;
width: 700px;
width: 590px;
.wran-tip {
font-size: 30px;
color: #444;
text-align: center;
margin-bottom: 150px;
margin-top: 60px;
.iconfont {
font-size: 110px;
display: block;
color: #bbb;
}
}
.img-check-header {
font-size: 24px;
}
.img-check-pics > li {
width: 140px;
height: 140px;
background-size: 560px 560px;
position: relative;
&:before {
content: "";
position: absolute;
width: 100%;
height: 100%;
box-sizing: border-box;
border: 1px solid #e8e8e8;
}
}
.submit {
width: 100%;
height: 100px;
line-height: 100px;
height: 90px;
line-height: 90px;
text-align: center;
font-size: 32px;
font-size: 30px;
color: #fff;
background: #5cb85c;
border-radius: 10px;
background: #444;
border-radius: 8px;
}
}
... ...