实际上Laravel自带有访问控制路由中间件 throttle
默认是 throttle:60,1
就是限制同一个IP在1分钟60次请求,超出的会被拦截,下一分钟才会恢复访问,它没有黑名单和白名单的功能,需要自己重新实现。
存储方式同cache driver
关于throttle:
https://learnku.com/articles/20073
https://learnku.com/articles/27682
https://segmentfault.com/a/1190000013499082
此处的频率是任意接口的访问总和,如果想限制到每一个接口,则需要自己拷贝 throttle 重新实现了。
以下是自己写的中间件,可定制功能。
<?php
namespace App\Http\Middleware;
use Closure;
use App\Traits\Controller\OpApiAjaxTraits;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;
class GateWayPlus
{
use OpApiAjaxTraits;
const IP_LIMIT_NUM_KEY = 'ipLimit:ipLimitNum';
const IP_BLACK_LIST_KEY = 'ipLimit:ipBlackList';
public $prefix = 'gateway';
public $delaySeconds = 60; // 观察时间跨度,秒
public $maxAttempts = 10000; // 限制请求数
public $blackSeconds = 0; // 封禁时长,秒,0-不封禁
/**
* @param \Illuminate\Http\Request $request
* @param Closure $next
* @param int $maxAttempts
* @param int $delaySeconds
* @param int $blackSeconds
* @param string $prefix
* @return \App\Traits\Controller\json|mixed
*/
public function handle($request, Closure $next, $prefix = null, $delaySeconds = null, $maxAttempts = null, $blackSeconds = null)
{
$path = $request->path();
$clientIp = $request->getClientIp();
if (!is_null($prefix) && !empty($prefix)) {
$this->prefix = $prefix;
}
// redis配置集群时必须
$this->prefix = '{' . $this->prefix . '}';
if (!is_null($maxAttempts)) {
$this->maxAttempts = intval($maxAttempts);
}
if (!is_null($delaySeconds)) {
$this->delaySeconds = intval($delaySeconds);
}
if (!is_null($blackSeconds)) {
$this->blackSeconds = intval($blackSeconds);
}
$param = [
'path' => $path,
'clientIp' => $clientIp,
];
$result = $this->main($param);
if ($result === false) {
return $this->ajaxApiError('当前IP请求过于频繁,暂时被封禁~');
}
return $next($request);
}
private function main($param)
{
// 预知的IP黑名单
$blackList = [];
if (in_array($param['clientIp'], $blackList)) {
return false;
}
// 预知的IP白名单
$whiteList = [];
if (in_array($param['clientIp'], $whiteList)) {
return true;
}
$blackKey = $this->prefix . ':' . self::IP_BLACK_LIST_KEY;
$limitKey = $this->prefix . ':' . self::IP_LIMIT_NUM_KEY;
$time = time();
$item = md5($param['path'] . '|' . $param['clientIp']);
return $this->luaScript($blackKey, $limitKey, $item, $time);
}
/*
* 普通模式
*/
public function normal($blackKey, $limitKey, $item, $time)
{
if ($this->blackSeconds > 0) {
$timeout = intval(Redis::hget($blackKey, $item));
if ($timeout) {
if ($timeout > $time) {
// 未解封
return false;
}
// 已解封,移除黑名单
Redis::hdel($blackKey, $item);
}
}
$last = intval(Redis::hget($limitKey, $item));
if ($last >= $this->maxAttempts) {
return false;
}
$num = Redis::hincrby($limitKey, $item, 1);
if (Redis::ttl($limitKey) == -1) {
Redis::expire($limitKey, $this->delaySeconds);
}
if ($num >= $this->maxAttempts && $this->blackSeconds > 0) {
// 加入黑名单
Redis::hset($blackKey, $item, $time + $this->blackSeconds);
// 删除记录
Redis::hdel($limitKey, $item);
}
return true;
}
/*
* LUA脚本模式
* 支持redis集群部署
*/
public function luaScript($blackKey, $limitKey, $item, $time)
{
$script = <<<'LUA'
local blackSeconds = tonumber(ARGV[5])
if(blackSeconds > 0)
then
local timeout = redis.call('hget', KEYS[1], ARGV[1])
if(timeout ~= false)
then
if(tonumber(timeout) > tonumber(ARGV[2]))
then
return false
end
redis.call('hdel', KEYS[1], ARGV[1])
end
end
local last = redis.call('hget', KEYS[2], ARGV[1])
if(last ~= false and tonumber(last) >= tonumber(ARGV[3]))
then
return false
end
local num = redis.call('hincrby', KEYS[2], ARGV[1], 1)
local ttl = redis.call('ttl', KEYS[2])
if(ttl == -1)
then
redis.call('expire', KEYS[2], ARGV[4])
end
if(tonumber(num) >= tonumber(ARGV[3]) and blackSeconds > 0)
then
redis.call('hset', KEYS[1], ARGV[1], ARGV[2] + ARGV[5])
redis.call('hdel', KEYS[2], ARGV[1])
end
return true
LUA;
$result = Redis::eval($script, 2, $blackKey, $limitKey, $item, $time, $this->maxAttempts, $this->delaySeconds, $this->blackSeconds);
if ($result) {
return true;
} else {
return false;
}
}
}
注册中间件
路由中的使用
$router->group(['namespace' => 'xxx', 'prefix' => 'yyy', 'middleware' => ['GateWayPlus:zzz,60,2,120']], function () use ($router) {
}