first
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
/.idea
|
||||
/.vscode
|
||||
/vendor
|
||||
|
||||
# OS X
|
||||
.DS_Store
|
||||
# OS X Thumbnails
|
||||
._*
|
||||
|
||||
# Windows image file caches
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
Desktop.ini
|
||||
|
||||
#-------------------------
|
||||
# Environment Files
|
||||
#-------------------------
|
||||
# These should never be under version control,
|
||||
# as it poses a security risk.
|
||||
.env
|
||||
.vagrant
|
||||
Vagrantfile
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 webman
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
# webman-throttler
|
||||
限流类(Throttler)提供了一种非常简单的方法,可以将用户要执行的活动限制为在设定的时间段内只能进行一定次数的尝试。这最常用于对 API 进行速率限制,或限制用户针对表单进行的尝试次数,以帮助防止暴力攻击。 该类可用于你根据设置的时间来进行限制的操作。
|
||||
|
||||
|
||||
|
||||
|
||||
限流类
|
||||
|
||||
* 全局中间件,整个应用接口限流,
|
||||
* 路由中间件,某些功能接口请求速率限制
|
||||
|
||||
缓存依据的是`Support\Cache的 instance()`, 其他类只要是实现 `get($key, $default = null)`, `set($key, $value, $ttl = null)`, `delete($key)` funtion就行.
|
||||
|
||||
|
||||
> 项目地址:https://github.com/nsp-team/webman-throttler
|
||||
|
||||
## 安装
|
||||
`composer require nsp-team/webman-throttler`
|
||||
|
||||
## 基本用法
|
||||
> 默认 开启全局中间件限流
|
||||
```php
|
||||
return [
|
||||
'' => [
|
||||
\NspTeam\WebmanThrottler\Middleware\ThrottlerMiddleware::class,
|
||||
]
|
||||
];
|
||||
```
|
||||
|
||||
> 你也可以启用路由中间件,控制接口请求速率限制
|
||||
例如:
|
||||
```php
|
||||
Route::group('/sys/user', static function () {
|
||||
Route::post('/test', [User::class, 'test']);
|
||||
})->middleware([
|
||||
\NspTeam\WebmanThrottler\Middleware\ThrottlerMiddleware::class
|
||||
]);
|
||||
```
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "nsp-team/webman-throttler",
|
||||
"description": "very Very easy to use a current limiting component, the code is very simple, based on the webman framework.",
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": ">=7.2",
|
||||
"workerman/webman-framework": "^1.2.1"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"NspTeam\\WebmanThrottler\\": "src/"
|
||||
}
|
||||
},
|
||||
"authors": [
|
||||
{
|
||||
"name": "maoxp",
|
||||
"email": "maoxingpei8686@163.com"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
namespace NspTeam\WebmanThrottler;
|
||||
|
||||
class Install
|
||||
{
|
||||
const WEBMAN_PLUGIN = true;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected static $pathRelation = array (
|
||||
'config/plugin/nsp-team/webman-throttler' => 'config/plugin/nsp-team/webman-throttler',
|
||||
);
|
||||
|
||||
/**
|
||||
* Install
|
||||
* @return void
|
||||
*/
|
||||
public static function install()
|
||||
{
|
||||
static::installByRelation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Uninstall
|
||||
* @return void
|
||||
*/
|
||||
public static function uninstall()
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace NspTeam\WebmanThrottler\Middleware;
|
||||
|
||||
|
||||
use NspTeam\WebmanThrottler\Throttle\Throttler;
|
||||
use support\Cache;
|
||||
use support\Container;
|
||||
use Webman\Http\Request;
|
||||
use Webman\Http\Response;
|
||||
use Webman\MiddlewareInterface;
|
||||
|
||||
|
||||
/**
|
||||
* ThrottlerMiddleware
|
||||
*/
|
||||
class ThrottlerMiddleware implements MiddlewareInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @param Request $request
|
||||
* @param callable $handler
|
||||
* @return Response
|
||||
*/
|
||||
public function process(Request $request, callable $handler): Response
|
||||
{
|
||||
/**
|
||||
* @var Throttler
|
||||
*/
|
||||
$throttler = Container::make(Throttler::class, [Cache::instance()]);
|
||||
|
||||
$config = config('plugin.nsp-team.webman-throttler.app');
|
||||
$capacity = $config['capacity'] ?? 60;
|
||||
$seconds = $config['seconds'] ?? 60;
|
||||
$cost = $config['cost'] ?? 1;
|
||||
$customerHandle = $config['customer_handle'];
|
||||
|
||||
if ($throttler->check($request->getRemoteIp(), $capacity, $seconds, $cost) === false) {
|
||||
$newClass = $customerHandle['class'];
|
||||
return new $newClass(... \array_values($customerHandle['constructor']));
|
||||
//return new Response(429, ['Content-Type' => 'application/json'], json_encode(['success' => false, 'msg' => '请求此时太频繁'], JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
|
||||
return $handler($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
|
||||
namespace NspTeam\WebmanThrottler\Throttle;
|
||||
|
||||
|
||||
/**
|
||||
* Class Throttler
|
||||
*
|
||||
* Uses an implementation of the Token Bucket algorithm to implement a
|
||||
* "rolling window" type of throttling that can be used for rate limiting
|
||||
* an API or any other request.
|
||||
*
|
||||
* Each "token" in the "bucket" is equivalent to a single request
|
||||
* for the purposes of this implementation.
|
||||
*
|
||||
* @see https://en.wikipedia.org/wiki/Token_bucket
|
||||
*/
|
||||
class Throttler implements ThrottlerInterface
|
||||
{
|
||||
/**
|
||||
* Container for throttle counters.
|
||||
*
|
||||
* @var object
|
||||
*/
|
||||
protected $cache;
|
||||
|
||||
/**
|
||||
* The number of seconds until the next token is available.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $tokenTime = 0;
|
||||
|
||||
/**
|
||||
* The prefix applied to all keys to
|
||||
* minimize potential conflicts.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $prefix = 'throttler_';
|
||||
|
||||
/**
|
||||
* Timestamp to use (during testing)
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $testTime;
|
||||
|
||||
public function __construct(object $cache)
|
||||
{
|
||||
$this->cache = $cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key The name of the bucket
|
||||
* @return $this
|
||||
*/
|
||||
public function remove(string $key): self
|
||||
{
|
||||
$tokenName = $this->prefix . $key;
|
||||
|
||||
$this->cache->delete($tokenName);
|
||||
$this->cache->delete($tokenName . 'Time');
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the test time, defaulting to current.
|
||||
*/
|
||||
private function time(): int
|
||||
{
|
||||
return $this->testTime ?? time();
|
||||
}
|
||||
|
||||
/**
|
||||
* Used during testing to set the current timestamp to use.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setTestTime(int $time): self
|
||||
{
|
||||
$this->testTime = $time;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restricts the number of requests made by a single IP address within
|
||||
* a set number of seconds.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* if (! $throttler->check($request->getRemoteIp(), 60, MINUTE)) {
|
||||
* die('You submitted over 60 requests within a minute.');
|
||||
* }
|
||||
*
|
||||
* @param string $key The name to use as the "bucket" name.
|
||||
* @param int $capacity The number of requests the "bucket" can hold
|
||||
* @param int $seconds The time it takes the "bucket" to completely refill
|
||||
* @param int $cost The number of tokens this action uses.
|
||||
*
|
||||
* @internal param int $maxRequests
|
||||
*/
|
||||
public function check(string $key, int $capacity, int $seconds, int $cost): bool
|
||||
{
|
||||
$tokenName = $this->prefix . $key;
|
||||
|
||||
// Number of tokens to add back per second
|
||||
$rate = $capacity / $seconds;
|
||||
// Number of seconds to get one token
|
||||
$refresh = 1 / $rate;
|
||||
|
||||
// Check to see if the bucket has even been created yet.
|
||||
if (($tokens = $this->cache->get($tokenName)) === null) {
|
||||
// If it hasn't been created, then we'll set it to the maximum
|
||||
// capacity - 1, and save it to the cache.
|
||||
$tokens = $capacity - $cost;
|
||||
$this->cache->set($tokenName, $tokens, $seconds);
|
||||
$this->cache->set($tokenName . 'Time', $this->time(), $seconds);
|
||||
|
||||
$this->tokenTime = 0;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// If $tokens > 0, then we need to replenish the bucket
|
||||
// based on how long it's been since the last update.
|
||||
$throttleTime = $this->cache->get($tokenName . 'Time');
|
||||
$elapsed = $this->time() - $throttleTime;
|
||||
|
||||
// Add tokens based up on number per second that
|
||||
// should be refilled, then checked against capacity
|
||||
// to be sure the bucket didn't overflow.
|
||||
$tokens += $rate * $elapsed;
|
||||
$tokens = $tokens > $capacity ? $capacity : $tokens;
|
||||
|
||||
// If $tokens >= 1, then we are safe to perform the action, but
|
||||
// we need to decrement the number of available tokens.
|
||||
if ($tokens >= 1) {
|
||||
$tokens -= $cost;
|
||||
$this->cache->set($tokenName, $tokens, $seconds);
|
||||
$this->cache->set($tokenName . 'Time', $this->time(), $seconds);
|
||||
|
||||
$this->tokenTime = 0;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// How many seconds till a new token is available.
|
||||
// We must have a minimum wait of 1 second for a new token.
|
||||
// Primarily stored to allow devs to report back to users.
|
||||
$newTokenAvailable = (int)($refresh - $elapsed - $refresh * $tokens);
|
||||
$this->tokenTime = max(1, $newTokenAvailable);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of seconds until the next available token will
|
||||
* be released for usage.
|
||||
* @return int
|
||||
*/
|
||||
public function getTokenTime(): int
|
||||
{
|
||||
return $this->tokenTime;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace NspTeam\WebmanThrottler\Throttle;
|
||||
|
||||
/**
|
||||
* Expected behavior of a Throttler
|
||||
* @package NspTeam\WebmanThrottler\Throttle
|
||||
*/
|
||||
interface ThrottlerInterface
|
||||
{
|
||||
/**
|
||||
* Restricts the number of requests made by a single key within
|
||||
* a set number of seconds.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* if (! $throttler->checkIPAddress($request->ipAddress(), 60, MINUTE))
|
||||
* {
|
||||
* die('You submitted over 60 requests within a minute.');
|
||||
* }
|
||||
*
|
||||
* @param string $key The name to use as the "bucket" name.
|
||||
* @param int $capacity The number of requests the "bucket" can hold
|
||||
* @param int $seconds The time it takes the "bucket" to completely refill
|
||||
* @param int $cost The number of tokens this action uses.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function check(string $key, int $capacity, int $seconds, int $cost): bool;
|
||||
|
||||
/**
|
||||
* Returns the number of seconds until the next available token will
|
||||
* be released for usage.
|
||||
* @return int
|
||||
*/
|
||||
public function getTokenTime(): int;
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
return [
|
||||
'enable' => true,
|
||||
|
||||
'capacity' => 60, // The number of requests the "bucket" can hold
|
||||
'seconds' => 60, // The time it takes the "bucket" to completely refill
|
||||
'cost' => 1, // The number of tokens this action uses.
|
||||
'customer_handle' => [
|
||||
'class' => \support\Response::class,
|
||||
'constructor' => [
|
||||
429,
|
||||
array(),
|
||||
json_encode(['success' => false, 'msg' => '请求次数太频繁'], 256),
|
||||
],
|
||||
],
|
||||
];
|
||||
Vendored
+19
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
return [
|
||||
'' => [
|
||||
\NspTeam\WebmanThrottler\Middleware\ThrottlerMiddleware::class,
|
||||
]
|
||||
];
|
||||
Reference in New Issue
Block a user