first
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
build
|
||||
vendor
|
||||
.vscode
|
||||
.phpunit*
|
||||
composer.lock
|
||||
.idea
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
# webman-throttle
|
||||
|
||||
## 作用
|
||||
通过本中间件可限定用户在一段时间内的访问次数,可用于保护接口防爬防爆破的目的。
|
||||
|
||||
## 安装
|
||||
|
||||
```composer require yzh52521/webman-throttle```
|
||||
|
||||
|
||||
安装后会自动为项目生成 config/plugin/yzh52521/throttle/app.php (***注意:任何时候你都不应该修改最后一行的 hash注释***)配置文件,安装后组件不会自动启用,需要手动设置。
|
||||
|
||||
|
||||
## 开启
|
||||
|
||||
插件以中间件的方式进行工作,因此它的开启与其他中间件一样,例如在全局中间件中使用:
|
||||
|
||||
|
||||
```
|
||||
<?php
|
||||
//cat config/middleware.php
|
||||
|
||||
return [
|
||||
// 全局中间件
|
||||
'' => [
|
||||
// ... 这里省略其它中间件
|
||||
app\middleware\Throttle::class,
|
||||
]
|
||||
];
|
||||
|
||||
```
|
||||
|
||||
|
||||
## 配置说明
|
||||
```
|
||||
<?php
|
||||
// cat config/plugin/yzh52521/throttle/app.php
|
||||
// 中间件配置
|
||||
return [
|
||||
|
||||
// 缓存键前缀,防止键值与其他应用冲突
|
||||
'prefix' => 'throttle_',
|
||||
|
||||
// 缓存的键,true 表示使用来源ip (request->getRealIp(true))
|
||||
'key' => true,
|
||||
|
||||
// 要被限制的请求类型, eg: GET POST PUT DELETE HEAD
|
||||
'visit_method' => ['GET'],
|
||||
|
||||
// 设置访问频率,例如 '10/m' 指的是允许每分钟请求10次。值 null 表示不限制,
|
||||
// eg: null 10/m 20/h 300/d 200/300
|
||||
'visit_rate' => '100/m',
|
||||
|
||||
// 响应体中设置速率限制的头部信息,含义见:https://docs.github.com/en/rest/overview/resources-in-the-rest-api#rate-limiting
|
||||
'visit_enable_show_rate_limit' => true,
|
||||
|
||||
// 访问受限时返回的响应( type: null|callable )
|
||||
'visit_fail_response' => function (Throttle $throttle, Request $request, int $wait_seconds): Response {
|
||||
return response('Too many requests, try again after ' . $wait_seconds . ' seconds.', 429);
|
||||
},
|
||||
|
||||
/*
|
||||
* 设置节流算法,组件提供了四种算法:
|
||||
* - CounterFixed :计数固定窗口
|
||||
* - CounterSlider: 滑动窗口
|
||||
* - TokenBucket : 令牌桶算法
|
||||
* - LeakyBucket : 漏桶限流算法
|
||||
*/
|
||||
'driver_name' => CounterFixed::class,
|
||||
|
||||
// Psr-16通用缓存库规范: https://blog.csdn.net/maquealone/article/details/79651111
|
||||
// Cache驱动必须符合PSR-16缓存库规范,最低实现get/set俩个方法 (且需静态化实现)
|
||||
// static get(string $key, mixed $default=null)
|
||||
// static set(string $key, mixed $value, int $ttl=0);
|
||||
|
||||
//webman默认使用 symfony/cache作为cache组件(https://www.workerman.net/doc/webman/db/cache.html)
|
||||
'cache_drive' => support\Cache::class,
|
||||
|
||||
//使用ThinkCache
|
||||
//'cache_drive' => think\facade\Cache::class,
|
||||
|
||||
];
|
||||
```
|
||||
|
||||
当配置项满足以下条件任何一个时,不会限制访问频率:
|
||||
|
||||
|
||||
```key === false || key === null || visit_rate === null```
|
||||
|
||||
|
||||
|
||||
其中 key 用来设置缓存键的, 而 visit_rate 用来设置访问频率,单位可以是秒,分,时,天。例如:1/s, 10/m, 98/h, 100/d , 也可以是 100/600 (600 秒内最多 100 次请求)。
|
||||
|
||||
|
||||
### 灵活定制
|
||||
|
||||
###### 示例一:针对用户个体做限制,key的值可以设为函数,该函数返回新的缓存键值(需要Session支持),例如:
|
||||
|
||||
```
|
||||
'key' => function($throttle, $request) {
|
||||
return $request->session()->get('user_id');
|
||||
},
|
||||
```
|
||||
|
||||
###### 实例二:在回调函数里针对不同控制器和方法定制生成key,中间件会进行转换:
|
||||
|
||||
```
|
||||
'key' => function($throttle, $request) {
|
||||
return implode('/', [
|
||||
$request->controller,
|
||||
$request->action,
|
||||
$request->getRealIp($safe_mode=true)
|
||||
]);
|
||||
},
|
||||
|
||||
//'key' => 'controller/action/ip' //上述配置的快捷实现
|
||||
|
||||
|
||||
```
|
||||
|
||||
|
||||
###### 示例三:在闭包内修改本次访问频率或临时更换限流策略:(PS:此示例需要本中间件在路由中间件后启用,这样预设的替换功能才会生效。)
|
||||
|
||||
```
|
||||
'key' => function($throttle, $request) {
|
||||
$throttle->setRate('5/m'); // 设置频率
|
||||
$throttle->setDriverClass(CounterSlider::class);// 设置限流策略
|
||||
return true;
|
||||
},
|
||||
```
|
||||
|
||||
###### 示例四:在路由中独立配置
|
||||
|
||||
```
|
||||
|
||||
Route::any('/api/driver-ocr', [ app\api\controller\Ocr::class, 'driver'])->middleware([
|
||||
app\middleware\Throttle::class
|
||||
]);
|
||||
|
||||
|
||||
//正确使用向中间件传参例子 (Webman-framework >= 1.3.16)
|
||||
Route::group('/path', function() {
|
||||
//路由注册
|
||||
...
|
||||
|
||||
})->setParams(['visit_rate' => '20/m',
|
||||
...
|
||||
...
|
||||
])->middleware(\app\middleware\Throttle::class);
|
||||
```
|
||||
|
||||
## 注意:
|
||||
1、Webman-framework >= 1.3.16 支持路由向中间件传参 使用 $route->param() 方法
|
||||
2、禁止访问时,throttle 默认是抛出 HttpResponseException, 当前插件场景下将正常响应一个httpResponse(即不会主动:Throw Exception),特殊需求请在 "visit_fail_response" 匿名函数中配置
|
||||
|
||||
## 申明
|
||||
|
||||
本库提取于[think-throttle v1.3.x](https://github.com/top-think/think-throttle)
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "yzh52521/webman-throttle",
|
||||
"description": "Webman框架限流中间件",
|
||||
"type": "library",
|
||||
"authors": [
|
||||
{
|
||||
"name": "yzh52521",
|
||||
"email": "396751927@qq.com"
|
||||
}
|
||||
],
|
||||
"keywords": [ "webman", "middleware", "throttle", "php"],
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": ">=7.2",
|
||||
"workerman/webman-framework": "^1.2.1"
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"yzh52521\\middleware\\": "src"
|
||||
}
|
||||
}
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace yzh52521\middleware;
|
||||
|
||||
class Install
|
||||
{
|
||||
const WEBMAN_PLUGIN = true;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected static $pathRelation = array(
|
||||
'config/plugin/yzh52521/throttle' => 'config/plugin/yzh52521/throttle',
|
||||
);
|
||||
|
||||
/**
|
||||
* Install
|
||||
* @return void
|
||||
*/
|
||||
public static function install()
|
||||
{
|
||||
copy(__DIR__ . '/middleware.tpl', app_path() . '/middleware/Throttle.php');
|
||||
static::installByRelation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Uninstall
|
||||
* @return void
|
||||
*/
|
||||
public static function uninstall()
|
||||
{
|
||||
if (is_file(app_path() . "/middleware/Throttle.php")) {
|
||||
unlink(app_path() . "/middleware/Throttle.php");
|
||||
}
|
||||
self::uninstallByRelation();
|
||||
}
|
||||
|
||||
/**
|
||||
* installByRelation
|
||||
* @return void
|
||||
*/
|
||||
public static function installByRelation()
|
||||
{
|
||||
foreach (static::$pathRelation as $source => $dest) {
|
||||
if ($pos = strrpos($dest, '/')) {
|
||||
$parent_dir = base_path() . '/' . substr($dest, 0, $pos);
|
||||
if (!is_dir($parent_dir)) {
|
||||
mkdir($parent_dir, 0777, true);
|
||||
}
|
||||
}
|
||||
//symlink(__DIR__ . "/$source", base_path()."/$dest");
|
||||
copy_dir(__DIR__ . "/$source", base_path() . "/$dest");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* uninstallByRelation
|
||||
* @return void
|
||||
*/
|
||||
public static function uninstallByRelation()
|
||||
{
|
||||
foreach (static::$pathRelation as $source => $dest) {
|
||||
$path = base_path() . "/$dest";
|
||||
if (!is_dir($path) && !is_file($path)) {
|
||||
continue;
|
||||
}
|
||||
/*if (is_link($path) {
|
||||
unlink($path);
|
||||
}*/
|
||||
remove_dir($path);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+267
@@ -0,0 +1,267 @@
|
||||
<?php
|
||||
/**
|
||||
*
|
||||
* 访问频率限制中间件
|
||||
* @link https://github.com/yzh52521/webman-throttle
|
||||
* @license http://www.apache.org/licenses/LICENSE-2.0
|
||||
* @copyright The PHP - Tools
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace yzh52521\middleware;
|
||||
|
||||
use Psr\SimpleCache\CacheInterface;
|
||||
use yzh52521\middleware\throttle\{CounterFixed, ThrottleAbstract};
|
||||
use Webman\Config;
|
||||
use support\{Container, Cache};
|
||||
use Webman\Http\{Request,Response};
|
||||
use function sprintf;
|
||||
|
||||
/**
|
||||
* 访问频率限制中间件
|
||||
* Class Throttle
|
||||
* @package app\middleware\Throttle
|
||||
*/
|
||||
class Throttle
|
||||
{
|
||||
/**
|
||||
* 默认配置参数
|
||||
* @var array
|
||||
*/
|
||||
public static $default_config = [
|
||||
'prefix' => 'throttle_', // 缓存键前缀,防止键与其他应用冲突
|
||||
'key' => true, // 节流规则 true为自动规则
|
||||
'visit_method' => ['GET', 'HEAD'], // 要被限制的请求类型
|
||||
'visit_rate' => null, // 节流频率 null 表示不限制 eg: 10/m 20/h 300/d
|
||||
'visit_enable_show_rate_limit' => true, // 在响应体中设置速率限制的头部信息
|
||||
'visit_fail_code' => 429, // 访问受限时返回的http状态码,当没有visit_fail_response时生效
|
||||
'visit_fail_text' => 'Too Many Requests', // 访问受限时访问的文本信息,当没有visit_fail_response时生效
|
||||
'visit_fail_response' => null, // 访问受限时的响应信息闭包回调
|
||||
'driver_name' => CounterFixed::class, // 限流算法驱动
|
||||
'cache_drive' => Cache::class // 缓存驱动
|
||||
];
|
||||
|
||||
public static $duration = [
|
||||
's' => 1,
|
||||
'm' => 60,
|
||||
'h' => 3600,
|
||||
'd' => 86400,
|
||||
];
|
||||
|
||||
/**
|
||||
* 缓存对象
|
||||
* @var CacheInterface
|
||||
*/
|
||||
protected $cache;
|
||||
|
||||
/**
|
||||
* 配置参数
|
||||
* @var array
|
||||
*/
|
||||
protected $config = [];
|
||||
|
||||
protected $key = null; // 解析后的标识
|
||||
protected $wait_seconds = 0; // 下次合法请求还有多少秒
|
||||
protected $now = 0; // 当前时间戳
|
||||
protected $max_requests = 0; // 规定时间内允许的最大请求次数
|
||||
protected $expire = 0; // 规定时间
|
||||
protected $remaining = 0; // 规定时间内还能请求的次数
|
||||
/**
|
||||
* @var ThrottleAbstract|null
|
||||
*/
|
||||
protected $driver_class = null;
|
||||
|
||||
/**
|
||||
* Throttle constructor.
|
||||
* @param Cache $cache
|
||||
* @param Config $config
|
||||
*/
|
||||
public function __construct(array $params = [])
|
||||
{
|
||||
$this->config = array_merge(static::$default_config, Config::get('plugin.yzh52521.throttle.app',[]), $params);
|
||||
$this->cache = Container::make($this->config['cache_drive'], []);
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求是否允许
|
||||
* @param Request $request
|
||||
* @return bool
|
||||
*/
|
||||
protected function allowRequest(Request $request): bool
|
||||
{
|
||||
// 若请求类型不在限制内
|
||||
if (!in_array($request->method(), $this->config['visit_method'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$key = $this->getCacheKey($request);
|
||||
if (null === $key) {
|
||||
return true;
|
||||
}
|
||||
[$max_requests, $duration] = $this->parseRate($this->config['visit_rate']);
|
||||
|
||||
$micronow = microtime(true);
|
||||
$now = (int)$micronow;
|
||||
|
||||
$this->driver_class = Container::make($this->config['driver_name'], []);
|
||||
if (!$this->driver_class instanceof ThrottleAbstract) {
|
||||
throw new \TypeError('The throttle driver must extends ' . ThrottleAbstract::class);
|
||||
}
|
||||
$allow = $this->driver_class->allowRequest($key, $micronow, $max_requests, $duration, $this->cache);
|
||||
|
||||
if ($allow) {
|
||||
// 允许访问
|
||||
$this->now = $now;
|
||||
$this->expire = $duration;
|
||||
$this->max_requests = $max_requests;
|
||||
$this->remaining = $max_requests - $this->driver_class->getCurRequests();
|
||||
return true;
|
||||
}
|
||||
|
||||
$this->wait_seconds = $this->driver_class->getWaitSeconds();
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理限制访问
|
||||
* @param Request $request
|
||||
* @param array $params
|
||||
* @return Response
|
||||
* @exception
|
||||
*/
|
||||
public function handle(Request $request, callable $next, array $params = []): Response
|
||||
{
|
||||
|
||||
if ($params) {
|
||||
$this->config = array_merge($this->config, $params);
|
||||
}
|
||||
|
||||
$allow = $this->allowRequest($request);
|
||||
if (!$allow) {
|
||||
// 访问受限
|
||||
return $this->buildLimitException($this->wait_seconds, $request);
|
||||
}
|
||||
|
||||
$response = $next($request);
|
||||
|
||||
if ((200 <= $response->getStatusCode() || 300 > $response->getStatusCode()) && $this->config['visit_enable_show_rate_limit']) {
|
||||
// 将速率限制 headers 添加到响应中
|
||||
$response->withHeaders($this->getRateLimitHeaders());
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成缓存的 key
|
||||
* @param Request $request
|
||||
* @return null|string
|
||||
*/
|
||||
protected function getCacheKey(Request $request): ?string
|
||||
{
|
||||
$key = $this->config['key'];
|
||||
|
||||
if ($key instanceof \Closure) {
|
||||
$key = $key($this, $request);
|
||||
}
|
||||
|
||||
if ($key === null || $key === false || $this->config['visit_rate'] === null) {
|
||||
// 关闭当前限制
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($key === true) {
|
||||
$key = $request->getRealIp($safe_mode = true);
|
||||
} else {
|
||||
$key = str_replace(
|
||||
[' ', 'controller/action/ip'],
|
||||
['', $request->controller . '/' . $request->action . '/' . $request->getRealIp($safe_mode = true)],
|
||||
strtolower(trim($key))
|
||||
);
|
||||
}
|
||||
return md5($this->config['prefix'] . $key . $this->config['driver_name']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析频率配置项
|
||||
* @param string $rate
|
||||
* @return int[]
|
||||
*/
|
||||
protected function parseRate($rate): array
|
||||
{
|
||||
[$num, $period] = explode("/", $rate);
|
||||
$max_requests = (int)$num;
|
||||
$duration = static::$duration[$period] ?? (int)$period;
|
||||
return [$max_requests, $duration];
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置速率
|
||||
* @param string $rate '10/m' '20/300'
|
||||
* @return $this
|
||||
*/
|
||||
public function setRate(string $rate): self
|
||||
{
|
||||
$this->config['visit_rate'] = $rate;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置缓存驱动
|
||||
* @param CacheInterface $cache
|
||||
* @return $this
|
||||
*/
|
||||
public function setCache(CacheInterface $cache): self
|
||||
{
|
||||
$this->cache = $cache;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置限流算法类
|
||||
* @param string $class_name
|
||||
* @return $this
|
||||
*/
|
||||
public function setDriverClass(string $class_name): self
|
||||
{
|
||||
$this->config['driver_name'] = $class_name;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取速率限制头
|
||||
* @return array
|
||||
*/
|
||||
public function getRateLimitHeaders(): array
|
||||
{
|
||||
return [
|
||||
'X-Rate-Limit-Limit' => $this->max_requests,
|
||||
'X-Rate-Limit-Remaining' => max($this->remaining, 0),
|
||||
'X-Rate-Limit-Reset' => $this->now + $this->expire,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 Response Exception
|
||||
* @param int $wait_seconds
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
public function buildLimitException(int $wait_seconds, Request $request): Response
|
||||
{
|
||||
$visitFail = $this->config['visit_fail_response'] ?? null;
|
||||
if ($visitFail instanceof \Closure) {
|
||||
$response = $visitFail($this, $request, $wait_seconds);
|
||||
if (!$response instanceof Response) {
|
||||
throw new \TypeError(sprintf('The closure must return %s instance', Response::class));
|
||||
}
|
||||
} else {
|
||||
$content = str_replace('__WAIT__', (string)$wait_seconds, $this->config['visit_fail_text']);
|
||||
$response = new Response($this->config['visit_fail_code'], [], $content);
|
||||
}
|
||||
if ($this->config['visit_enable_show_rate_limit']) {
|
||||
$response->withHeaders(['Retry-After' => $wait_seconds]);
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | 节流设置
|
||||
// +----------------------------------------------------------------------
|
||||
use yzh52521\middleware\Throttle;
|
||||
use yzh52521\middleware\throttle\CounterFixed;
|
||||
// use yzh52521\middleware\throttle\CounterSlider;
|
||||
// use yzh52521\middleware\throttle\TokenBucket;
|
||||
// use yzh52521\middleware\throttle\LeakyBucket;
|
||||
use Webman\Http\{Request, Response};
|
||||
|
||||
return [
|
||||
'enable' => true,
|
||||
// 缓存键前缀,防止键值与其他应用冲突
|
||||
'prefix' => 'throttle_',
|
||||
|
||||
// 缓存的键,true 表示使用来源ip (request->getRealIp(true))
|
||||
'key' => true,
|
||||
|
||||
// 要被限制的请求类型, eg: GET POST PUT DELETE HEAD
|
||||
'visit_method' => ['GET'],
|
||||
|
||||
// 设置访问频率,例如 '10/m' 指的是允许每分钟请求10次。值 null 表示不限制,
|
||||
// eg: null 10/m 20/h 300/d 200/300
|
||||
'visit_rate' => '100/m',
|
||||
|
||||
// 响应体中设置速率限制的头部信息,含义见:https://docs.github.com/en/rest/overview/resources-in-the-rest-api#rate-limiting
|
||||
'visit_enable_show_rate_limit' => true,
|
||||
|
||||
// 访问受限时返回的响应( type: null|callable )
|
||||
'visit_fail_response' => function (Throttle $throttle, Request $request, int $wait_seconds): Response {
|
||||
return response('Too many requests, try again after ' . $wait_seconds . ' seconds.');
|
||||
},
|
||||
|
||||
/*
|
||||
* 设置节流算法,组件提供了四种算法:
|
||||
* - CounterFixed :计数固定窗口
|
||||
* - CounterSlider: 滑动窗口
|
||||
* - TokenBucket : 令牌桶算法
|
||||
* - LeakyBucket : 漏桶限流算法
|
||||
*/
|
||||
'driver_name' => CounterFixed::class,
|
||||
|
||||
// Psr-16通用缓存库规范: https://blog.csdn.net/maquealone/article/details/79651111
|
||||
// Cache驱动必须符合PSR-16缓存库规范,最低实现get/set俩个方法 (且需静态化实现)
|
||||
// static get(string $key, mixed $default=null)
|
||||
// static set(string $key, mixed $value, int $ttl=0);
|
||||
|
||||
//webman默认使用 symfony/cache作为cache组件(https://www.workerman.net/doc/webman/db/cache.html)
|
||||
'cache_drive' => support\Cache::class,
|
||||
|
||||
//使用ThinkCache
|
||||
//'cache_drive' => think\facade\Cache::class,
|
||||
];
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
/**
|
||||
* 节流设置
|
||||
* @copyright The PHP-Tools
|
||||
*/
|
||||
|
||||
namespace app\middleware;
|
||||
|
||||
use Webman\MiddlewareInterface;
|
||||
use Webman\Http\Response;
|
||||
use Webman\Http\Request;
|
||||
|
||||
/**
|
||||
* Class StaticFile
|
||||
* @package app\middleware
|
||||
*/
|
||||
class Throttle implements MiddlewareInterface
|
||||
{
|
||||
public function process(Request $request, callable $next):Response
|
||||
{
|
||||
if ( $route = $request->route ) {
|
||||
$params = $route->param();
|
||||
}
|
||||
return (new \yzh52521\middleware\Throttle())->handle($request, $next, $params??[]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace yzh52521\middleware\throttle;
|
||||
|
||||
|
||||
/**
|
||||
* 计数器固定窗口算法
|
||||
* Class CounterFixed
|
||||
* @package yzh52521\middleware\throttle
|
||||
*/
|
||||
class CounterFixed extends ThrottleAbstract
|
||||
{
|
||||
|
||||
public function allowRequest(string $key, float $micronow, int $max_requests, int $duration, $cache): bool
|
||||
{
|
||||
$cur_requests = (int)$cache::get($key, 0);
|
||||
$now = (int)$micronow;
|
||||
$wait_reset_seconds = $duration - $now % $duration; // 距离下次重置还有n秒时间
|
||||
$this->wait_seconds = $wait_reset_seconds % $duration + 1;
|
||||
$this->cur_requests = $cur_requests;
|
||||
|
||||
if ($cur_requests < $max_requests) { // 允许访问
|
||||
$cache::set($key, $this->cur_requests + 1, $wait_reset_seconds);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace yzh52521\middleware\throttle;
|
||||
|
||||
|
||||
/**
|
||||
* 计数器滑动窗口算法
|
||||
* Class CouterSlider
|
||||
* @package yzh52521\middleware\throttle
|
||||
*/
|
||||
class CounterSlider extends ThrottleAbstract
|
||||
{
|
||||
public function allowRequest(string $key, float $micronow, int $max_requests, int $duration, $cache): bool
|
||||
{
|
||||
$history = $cache::get($key, []);
|
||||
$now = (int)$micronow;
|
||||
// 移除过期的请求的记录
|
||||
$history = array_values(array_filter($history, function ($val) use ($now, $duration) {
|
||||
return $val >= $now - $duration;
|
||||
}));
|
||||
|
||||
$this->cur_requests = count($history);
|
||||
if ($this->cur_requests < $max_requests) {
|
||||
// 允许访问
|
||||
$history[] = $now;
|
||||
$cache::set($key, $history, $duration);
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($history) {
|
||||
$wait_seconds = $duration - ($now - $history[0]) + 1;
|
||||
$this->wait_seconds = max($wait_seconds, 0);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace yzh52521\middleware\throttle;
|
||||
|
||||
|
||||
/**
|
||||
* 漏桶算法
|
||||
* Class LeakyBucket
|
||||
* @package yzh52521\middleware\throttle
|
||||
*/
|
||||
class LeakyBucket extends ThrottleAbstract
|
||||
{
|
||||
|
||||
public function allowRequest(string $key, float $micronow, int $max_requests, int $duration, $cache): bool
|
||||
{
|
||||
if ($max_requests <= 0) return false;
|
||||
|
||||
$last_time = (float)$cache::get($key, 0); // 最近一次请求
|
||||
$rate = (float)$duration / $max_requests; // 平均 n 秒一个请求
|
||||
if ($micronow - $last_time < $rate) {
|
||||
$this->cur_requests = 1;
|
||||
$this->wait_seconds = ceil($rate - ($micronow - $last_time));
|
||||
return false;
|
||||
}
|
||||
|
||||
$cache::set($key, $micronow, $duration);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace yzh52521\middleware\throttle;
|
||||
|
||||
use Psr\SimpleCache\CacheInterface;
|
||||
|
||||
abstract class ThrottleAbstract
|
||||
{
|
||||
/** @var int */
|
||||
protected int $cur_requests = 0; // 当前已有的请求数
|
||||
/** @var int */
|
||||
protected int $wait_seconds = 0; // 距离下次合法请求还有多少秒
|
||||
|
||||
/**
|
||||
* 是否允许访问
|
||||
* @param string $key 缓存键
|
||||
* @param float $micronow 当前时间戳,可含毫秒
|
||||
* @param int $max_requests 允许最大请求数
|
||||
* @param int $duration 限流时长
|
||||
* @param CacheInterface $cache 缓存对象
|
||||
* @return bool
|
||||
*/
|
||||
abstract public function allowRequest(string $key, float $micronow, int $max_requests, int $duration, CacheInterface $cache): bool;
|
||||
|
||||
/**
|
||||
* 计算距离下次合法请求还有多少秒
|
||||
* @return int
|
||||
*/
|
||||
public function getWaitSeconds(): int
|
||||
{
|
||||
return $this->wait_seconds;
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前已有的请求数
|
||||
* @return int
|
||||
*/
|
||||
public function getCurRequests(): int
|
||||
{
|
||||
return $this->cur_requests;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace yzh52521\middleware\throttle;
|
||||
|
||||
|
||||
/**
|
||||
* 令牌桶算法
|
||||
* Class TokenBucket
|
||||
* @package yzh52521\middleware\throttle
|
||||
*/
|
||||
class TokenBucket extends ThrottleAbstract
|
||||
{
|
||||
public function allowRequest(string $key, float $micronow, int $max_requests, int $duration, $cache): bool
|
||||
{
|
||||
if ($max_requests <= 0 || $duration <= 0) return false;
|
||||
|
||||
$assist_key = $key . 'store_num'; // 辅助缓存
|
||||
$rate = (float)$max_requests / $duration; // 平均一秒生成 n 个 token
|
||||
|
||||
$last_time = $cache::get($key, null);
|
||||
$store_num = $cache::get($assist_key, null);
|
||||
|
||||
if ($last_time === null || $store_num === null) { // 首次访问
|
||||
$cache::set($key, $micronow, $duration);
|
||||
$cache::set($assist_key, $max_requests - 1, $duration);
|
||||
return true;
|
||||
}
|
||||
|
||||
$create_num = floor(($micronow - $last_time) * $rate); // 推算生成的 token 数
|
||||
$token_left = (int)min($max_requests, $store_num + $create_num); //当前剩余 tokens 数量
|
||||
|
||||
if ($token_left < 1) {
|
||||
$tmp = (int)ceil($duration / $max_requests);
|
||||
$this->wait_seconds = $tmp - ($micronow - $last_time) % $tmp;
|
||||
return false;
|
||||
}
|
||||
$this->cur_requests = $max_requests - $token_left;
|
||||
$cache::set($key, $micronow, $duration);
|
||||
$cache::set($assist_key, $token_left - 1, $duration);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user