今天一个同事,对现有的项目做了一个测试,发现一个很严重的Bug。正常登录进来后,复制完整的cookie值,(我们是用cookie加密进行会话的)
来进行curl模拟其它操作,频繁的插入操作,最终导致表数据量大。
他和说我,对该表数据做一个最大条数限制。对于需求固定的来话,这是一个解决方法。对连续变动变态需求,这些方法往往不行。
这里先不说前端的防护,最终还是要归到后端处理检测。
最终想了个办法,
第一层防护:
先给各个模块下的异步请求,做全局限制,只要是ajax异步请求的,才可以访问我们的功能方法。我们的代码结构是按各个功能模块,独自创建异步控制器,如用户模板,可能这样的“UserAjax.class.php”;
这样,我们可以在析构方法里添加一个方法。
public function __construct() {
parent::__construct ();
$this->common->isAjax();
}
假设isAjax在common通用方法中:
public function isAjax() {
if (!isset($_SERVER['HTTP_X_REQUESTED_WITH'])) {
$this->jsonmsg(array('code' => 0, 'msg' => '非法请求'));
}
}
第二层防护:
对每个请求频率做限制,这个要写一下方法。方法的思路是这样的,对每个请求的IP进行会话保存,存储会话时间戳,再检验时间内规定的请求次数。
下面的方法同样写在common通用方法类中
a、获取IP
public function getIp() {
if (getenv('REMOTE_ADDR') && strcasecmp(getenv('REMOTE_ADDR'), 'unknown')) {
$ip = getenv('REMOTE_ADDR');
} elseif (isset($_SERVER['REMOTE_ADDR']) && $_SERVER['REMOTE_ADDR'] && strcasecmp($_SERVER['REMOTE_ADDR'], 'unknown')) {
$ip = $_SERVER['REMOTE_ADDR'];
} elseif (getenv('HTTP_CLIENT_IP') && strcasecmp(getenv('HTTP_CLIENT_IP'), 'unknown')) {
$ip = getenv('HTTP_CLIENT_IP');
} elseif (getenv('HTTP_X_FORWARDED_FOR') && strcasecmp(getenv('HTTP_X_FORWARDED_FOR'), 'unknown')) {
$ip = getenv('HTTP_X_FORWARDED_FOR');
}
$tmpIp = filter_var($ip, FILTER_VALIDATE_IP);
$ip = $tmpIp ? $tmpIp : 'unknown';
return $ip;
}
b、检测时间内请求次数,详情见方法注释
public function checkRequestNumber() {
//获取IP
$ip = $this->getIp();
//请求会话的URI,组合后进行md5加密,因为我们通常一个请求,通过不同的参数调用不同的业务逻辑
//如ajax_user?op=checkEmail,checkUsername
$key = md5($_SERVER['REQUEST_URI'].$_REQUEST['op']);
$errMsg = '提交过于频繁,请稍后再试吧!';
//IP时间栈数组
if (!is_array($_SESSION[$ip][$key])) {
$_SESSION[$ip][$key] = array();
}
if (isset($_SESSION[$ip][$key][0])) {
$_SESSION[$ip][$key][] = time();
//session保存时间为60秒,清空session
$requestFir = time() - $_SESSION[$ip][$key][0];
if ($requestFir > 60) {
$_SESSION[$ip][$key] = array();
}
//两次提交小于1s禁止提交
$requestSec = time() - $_SESSION[$ip][$key][count($_SESSION[$ip][$key]) - 3];
if ($requestSec < 1) {
$this->jsonmsg(array('code' => 0, 'msg' => $errMsg . 1));
};
//您在10s内已经提交了3请求,禁止提交
$requestThi = time() - $_SESSION[$ip][$key][count($_SESSION[$ip][$key]) - 3];
if (isset($_SESSION[$ip][$key][3]) && ($requestThi < 10)) {
$this->jsonmsg(array('code' => 0, 'msg' => $errMsg . 2));
}
//您在1分钟期间已经提交了5请求,禁止提交
$requestFif = time() - $_SESSION[$ip][$key][count($_SESSION[$ip][$key]) - 3];
if (isset($_SESSION[$ip][$key][5]) && ($requestFif < 60)) {
$this->jsonmsg(array('code' => 0, 'msg' => $errMsg . 3));
}
} else {
$_SESSION[$ip][$key][] = time();
}
}
在基类中调用 checkRequestNumber(),代码如下:
if (isset($_SERVER['HTTP_X_REQUESTED_WITH'])) {
$common->checkRequestNumber();
}
最后,你也可以对请求频繁的加图片验证码,提示用户操作请求频繁~
附上同事请求的CURL模拟小方法:
echo post();
function post($url = 'http://www.test.com/ajax_user', $post_data = '', $timeout = 5){
$post_data = 'email='.mt_rand(10000,99999999).'@qq.com&op=addemail';
$cookie = '........';
$ch = curl_init();
curl_setopt ($ch, CURLOPT_URL, $url);
curl_setopt ($ch, CURLOPT_POST, 1);
curl_setopt ($ch, CURLOPT_COOKIE, $cookie);
if($post_data != ''){
curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data);
}
curl_setopt ($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt ($ch, CURLOPT_CONNECTTIMEOUT, $timeout);
curl_setopt($ch, CURLOPT_HEADER, false);
$file_contents = curl_exec($ch);
curl_close($ch);
return $file_contents;
}