Skip to content

Commit dcc8f0f

Browse files
committed
配置微调,代码结构调整
1 parent 257a4a1 commit dcc8f0f

File tree

10 files changed

+165
-119
lines changed

10 files changed

+165
-119
lines changed

README.md

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,19 @@ return [
2727
'key' => true,
2828
// 要被限制的请求类型, eg: GET POST PUT DELETE HEAD
2929
'visit_method' => ['GET'],
30-
// 设置访问频率,例如 '10/m' 指的是允许每分钟请求10次。值 null 表示不限制, eg: null 10/m 20/h 300/d 200/300
30+
// 设置访问频率,例如 '10/m' 指的是允许每分钟请求10次;'10/60'指允许每60秒请求10次。空字符串表示不限制, eg: '', '10/m', '20/h', '300/d', '200/300'
3131
'visit_rate' => '100/m',
3232
// 访问受限时返回的响应
3333
'visit_fail_response' => function (Throttle $throttle, Request $request, int $wait_seconds) {
34-
return Response::create('Too many requests, try again after ' . $wait_seconds . ' seconds.')->code(429);
34+
$content = str_replace('__WAIT__', (string)$wait_seconds, $throttle->getFailMessage());
35+
return Response::create($content)->code(429);
3536
},
3637
];
3738
```
3839

3940
当配置项满足以下条件任何一个时,不会限制访问频率:
40-
1. `key` 值为 `false``null`
41-
2. `visit_rate` 值为 `null`
41+
1. `key` 值为 `false``''`
42+
2. `visit_rate` 值为 `''`
4243

4344
其中 `key` 用来设置缓存键的;而 `visit_rate` 用来设置访问频率,单位可以是秒,分,时,天,例如:`1/s`, `10/m`, `98/h`, `100/d` , 也可以是 `100/600` (600 秒内最多 100 次请求)。
4445

@@ -81,20 +82,62 @@ Route::group(function() {
8182
'key' => '__CONTROLLER__/__ACTION__/__IP__',
8283
]);
8384
```
85+
86+
示例五:使用注解配置(3.0.x版本支持)
87+
需要开启路由中间件,它有多种启用方式,这里以全局路由中间件为例,在 `config/route.php` 配置文件中添加:
88+
```
89+
'middleware' => [
90+
\think\middleware\Throttle::class,
91+
],
92+
```
93+
控制器中使用:
94+
```
95+
<?php
96+
97+
namespace app\controller;
98+
99+
use app\BaseController;
100+
use think\middleware\annotation\RateLimit;
101+
102+
class User extends BaseController
103+
{
104+
105+
#[RateLimit(rate: "10/m")]
106+
public function index(): string
107+
{
108+
// 默认为IP限流,默认单位时间为1秒
109+
return '每个ip每秒最多10个请求';
110+
}
111+
112+
#[RateLimit(rate: "1/d", key: RateLimit::SESSION, message: '每个用户每天只能领取一次优惠券')]
113+
#[RateLimit(rate: '100/d', key: 'coupon', message: '今天的优惠券已经发完,请明天再来')]
114+
public function coupon(): string
115+
{
116+
return '优惠券发送成功';
117+
}
118+
119+
}
120+
```
84121
## 版本与 TP 适配关系
85122
```
123+
3.0.x -> thinkphp 8.0
86124
2.0.x -> thinkphp 8.0
87125
1.x.x -> thinkphp 6.0/6.1
88126
0.5.x -> thinkphp 5.1
89127
```
90128

91129
## 更新日志
130+
版本 3.0.x 配置微调,不支持无缝升级;
131+
92132
版本 2.0.x 的可从 1.x 无缝升级;
93133

94134
版本 1.3.x 的配置形式完全兼容版本 1.2.x 内容,可以无缝升级;
95135

96136
版本 1.2.x 的配置形式完全兼容版本 1.1.x 内容,可以无缝升级;
97137

138+
### 3.0.x 更新
139+
- 支持注解方式;
140+
98141
### 2.0.x 更新
99142
- 适配 thinkphp 8.0;
100143
- 所有 `php` 文件都采用 `declare(strict_types=1);` 强类型约束;

src/Throttle.php

Lines changed: 67 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use think\middleware\throttle\ThrottleAbstract;
1717
use think\Request;
1818
use think\Response;
19+
use think\Session;
1920
use TypeError;
2021
use function sprintf;
2122

@@ -37,7 +38,7 @@ class Throttle
3738
'visit_rate' => '', // 节流频率, 空字符串表示不限制 eg: '', '10/m', '20/h', '300/d'
3839
'visit_enable_show_rate_limit' => true, // 在响应体中设置速率限制的头部信息
3940
'visit_fail_code' => 429, // 访问受限时返回的 http 状态码,当没有 visit_fail_response 时生效
40-
'visit_fail_text' => 'Too Many Requests', // 访问受限时访问的文本信息,当没有 visit_fail_response 时生效
41+
'visit_fail_text' => 'Too many requests, try again after __WAIT__ seconds.', // 访问受限时访问的文本信息
4142
'visit_fail_response' => null, // 访问受限时的响应信息闭包回调
4243
'driver_name' => CounterFixed::class, // 限流算法驱动
4344
];
@@ -55,6 +56,7 @@ class Throttle
5556
*/
5657
protected CacheInterface $cache;
5758
protected App $app;
59+
protected Session $session;
5860

5961
/**
6062
* 配置参数
@@ -74,12 +76,13 @@ class Throttle
7476
* @param Cache $cache
7577
* @param Config $config
7678
*/
77-
public function __construct(Cache $cache, Config $config, App $app)
79+
public function __construct(Cache $cache, Config $config, App $app, Session $session)
7880
{
7981
$this->cache = $cache;
8082
$this->config = array_merge(static::$default_config, $config->get('throttle', []));
8183
$this->app = $app;
8284
$this->config_instance = $config;
85+
$this->session = $session;
8386
}
8487

8588
/**
@@ -95,7 +98,7 @@ public function handle(Request $request, Closure $next, array $params = []): Res
9598
$this->config = array_merge($this->config, $params);
9699
}
97100

98-
$allow = $this->allowRequestByConfig($request) && $this->allowRequestByAnnotation($request);
101+
$allow = $this->allowRequestByAnnotation($request) && $this->allowRequestByConfig($request);
99102
if (!$allow) {
100103
// 访问受限
101104
throw $this->buildLimitException($this->wait_seconds, $request);
@@ -113,43 +116,73 @@ public function handle(Request $request, Closure $next, array $params = []): Res
113116
}
114117

115118
/**
116-
* 根据**配置**信息是否允许请求通过
119+
* 根据**注解**信息是否允许请求通过
117120
* @param Request $request
118121
* @return bool
119122
*/
120-
protected function allowRequestByConfig(Request $request): bool
123+
protected function allowRequestByAnnotation(Request $request): bool
121124
{
122-
// 若请求类型不在限制内
123-
if (!in_array($request->method(), $this->config['visit_method'])) {
124-
return true;
125+
// 处理注解
126+
$controller = $this->getFullController($request);
127+
if ($controller) {
128+
$action = $request->action();
129+
if (method_exists($controller, $action)) {
130+
$reflectionMethod = new ReflectionMethod($controller, $action);
131+
$attributes = $reflectionMethod->getAttributes(RateLimitAnnotation::class);
132+
foreach ($attributes as $attribute) {
133+
$annotation = $attribute->newInstance();
134+
$key = $this->getCacheKey($request, $annotation->key, $annotation->driver, true);
135+
if (!$this->allowRequest($key, $annotation->rate, $annotation->driver)) {
136+
$this->config['visit_fail_text'] = $annotation->message;
137+
return false;
138+
}
139+
}
140+
}
125141
}
126-
$driver = $this->config['driver_name'];
127-
$key = $this->getCacheKey($request, $this->config['key'], $driver);
128-
return $this->allowRequest($key, $this->config['visit_rate'], $driver);
142+
return true;
143+
}
144+
145+
private function getFullController(Request $request): string
146+
{
147+
$controller = $request->controller();
148+
if (empty($controller)) {
149+
return '';
150+
}
151+
$suffix = $this->config_instance->get('route.controller_suffix') ? 'Controller' : '';
152+
$layer = $this->config_instance->get('route.controller_layer') ?: 'controller';
153+
$controllerClassName = $this->app->parseClass($layer, $controller . $suffix);
154+
return $controllerClassName;
129155
}
130156

131157
/**
132158
* 生成缓存的 key
133159
* @param Request $request
134-
* @param string|bool|Closure|null $key
160+
* @param string|bool|Closure $key
135161
* @param string $driver
136162
* @return string
137163
*/
138-
protected function getCacheKey(Request $request, string|bool|Closure|null $key, string $driver): string
164+
protected function getCacheKey(Request $request, string|bool|Closure $key, string $driver, bool $annotation = false): string
139165
{
140166
if ($key instanceof Closure) {
141167
$key = Container::getInstance()->invokeFunction($key, [$this, $request]);
142168
}
143169

144-
if ($key === null || $key === false) {
145-
// 关闭当前限制
170+
if ($key === false || $key === '') {
171+
// 不做限制
146172
return '';
147173
}
148174

149175
if ($key === true) {
150176
$key = $request->ip();
151177
} elseif (is_string($key) && str_contains($key, '__')) {
152-
$key = str_replace(['__CONTROLLER__', '__ACTION__', '__IP__'], [$request->controller(), $request->action(), $request->ip()], $key);
178+
$key = str_replace(['__CONTROLLER__', '__ACTION__', '__IP__', '__SESSION__'],
179+
[$request->controller(), $request->action(), $request->ip(), $this->session->getId()],
180+
$key);
181+
}
182+
183+
if ($annotation) {
184+
// 注解需要以实际方法作为前缀
185+
$key = $request->controller() . $request->action() . $key;
153186
}
154187

155188
return md5($this->config['prefix'] . $key . $driver);
@@ -206,44 +239,19 @@ protected function parseRate(string $rate): array
206239
}
207240

208241
/**
209-
* 根据**注解**信息是否允许请求通过
242+
* 根据**配置**信息是否允许请求通过
210243
* @param Request $request
211244
* @return bool
212245
*/
213-
protected function allowRequestByAnnotation(Request $request): bool
214-
{
215-
// 处理注解
216-
$controller = $this->getFullController($request);
217-
if ($controller) {
218-
$action = $request->action();
219-
if (method_exists($controller, $action)) {
220-
$reflectionMethod = new ReflectionMethod($controller, $action);
221-
$attributes = $reflectionMethod->getAttributes(RateLimitAnnotation::class);
222-
foreach ($attributes as $attribute) {
223-
$annotation = $attribute->newInstance();
224-
$key = $this->getCacheKey($request, $annotation->key, $annotation->driver);
225-
$key = $controller . $action . $key; // 注解需要以实际方法作为前缀
226-
227-
if (!$this->allowRequest($key, $annotation->rate, $annotation->driver)) {
228-
$this->config['visit_fail_text'] = $annotation->message;
229-
return false;
230-
}
231-
}
232-
}
233-
}
234-
return true;
235-
}
236-
237-
private function getFullController(Request $request): string
246+
protected function allowRequestByConfig(Request $request): bool
238247
{
239-
$controller = $request->controller();
240-
if (empty($controller)) {
241-
return '';
248+
// 若请求类型不在限制内
249+
if (!in_array($request->method(), $this->config['visit_method'])) {
250+
return true;
242251
}
243-
$suffix = $this->config_instance->get('route.controller_suffix') ? 'Controller' : '';
244-
$layer = $this->config_instance->get('route.controller_layer') ?: 'controller';
245-
$controllerClassName = $this->app->parseClass($layer, $controller . $suffix);
246-
return $controllerClassName;
252+
$driver = $this->config['driver_name'];
253+
$key = $this->getCacheKey($request, $this->config['key'], $driver);
254+
return $this->allowRequest($key, $this->config['visit_rate'], $driver);
247255
}
248256

249257
/**
@@ -261,7 +269,7 @@ public function buildLimitException(int $wait_seconds, Request $request): HttpRe
261269
throw new TypeError(sprintf('The closure must return %s instance', Response::class));
262270
}
263271
} else {
264-
$content = str_replace('__WAIT__', (string)$wait_seconds, $this->config['visit_fail_text']);
272+
$content = str_replace('__WAIT__', (string)$wait_seconds, $this->getFailMessage());
265273
$response = Response::create($content)->code($this->config['visit_fail_code']);
266274
}
267275
if ($this->config['visit_enable_show_rate_limit']) {
@@ -270,6 +278,15 @@ public function buildLimitException(int $wait_seconds, Request $request): HttpRe
270278
return new HttpResponseException($response);
271279
}
272280

281+
/**
282+
* 获取受限时的信息
283+
* @return string
284+
*/
285+
public function getFailMessage(): string
286+
{
287+
return $this->config['visit_fail_text'];
288+
}
289+
273290
/**
274291
* 设置速率
275292
* @param string $rate '10/m' '20/300'

src/annotation/RateLimit.php

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@ class RateLimit
1212
{
1313
const AUTO = true;
1414
const IP = '__IP__';
15+
const SESSION = '__SESSION__';
1516

16-
public function __construct(public string $rate,
17-
public string|bool|Closure|null $key = RateLimit::AUTO,
18-
public string $driver = CounterFixed::class,
19-
public string $message = 'Too Many Requests')
17+
public function __construct(public string $rate,
18+
public string|bool|Closure $key = RateLimit::AUTO,
19+
public string $driver = CounterFixed::class,
20+
public string $message = 'Too Many Requests')
2021
{
2122

2223
}

src/config.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@
1010
return [
1111
// 缓存键前缀,防止键值与其他应用冲突
1212
'prefix' => 'throttle_',
13-
// 缓存的键,true 表示使用来源ip
13+
// 缓存的键,true 表示使用来源 ip
1414
'key' => true,
1515
// 要被限制的请求类型, eg: GET POST PUT DELETE HEAD 等
1616
'visit_method' => ['GET', 'HEAD'],
17-
// 设置访问频率,例如 '10/m' 指的是允许每分钟请求10次;'10/60'指允许每60秒请求10次。空值表示不限制, eg: '', '10/m', '20/h', '300/d', '200/300'
17+
// 设置访问频率,例如 '10/m' 指的是允许每分钟请求10次;'10/60'指允许每60秒请求10次。空字符串表示不限制, eg: '', '10/m', '20/h', '300/d', '200/300'
1818
'visit_rate' => '100/m',
1919
/*
2020
* 设置节流算法,组件提供了四种算法:
@@ -28,6 +28,7 @@
2828
'visit_enable_show_rate_limit' => true,
2929
// 访问受限时返回的响应
3030
'visit_fail_response' => function (Throttle $throttle, Request $request, int $wait_seconds) {
31-
return Response::create('Too many requests, try again after ' . $wait_seconds . ' seconds.')->code(429);
31+
$content = str_replace('__WAIT__', (string)$wait_seconds, $throttle->getFailMessage());
32+
return Response::create($content)->code(429);
3233
},
3334
];

tests/Base.php

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,10 +107,10 @@ function set_throttle_config(array $config): void
107107
$this->throttle_config = $config;
108108
}
109109

110-
protected function tearDown(): void
110+
protected function setUp(): void
111111
{
112-
parent::tearDown();
113-
// 每次测试完毕都需要清理 runtime cache 目录,避免影响其他单元测试
112+
parent::setUp();
113+
// 每次单元测试都需要清理 runtime cache 目录,避免影响其他单元测试
114114
$cache_dir = GCApp::RUNTIME_PATH . "cache";
115115
$dirs = glob($cache_dir . '/*', GLOB_ONLYDIR);
116116
foreach ($dirs as $dir) {
@@ -125,7 +125,6 @@ protected function tearDown(): void
125125
}
126126
unset($cache_dir);
127127
unset($dirs);
128-
gc_collect_cycles(); // 进行垃圾回收
129128
}
130129

131130
}

tests/app/controller/User.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,11 @@ public function coupon(): string
3434
return '优惠券发送成功';
3535
}
3636

37+
#[RateLimit(rate: "1/d", key: RateLimit::SESSION, message: '每个用户每天只能领取一次优惠券')]
38+
#[RateLimit(rate: '100/d', key: 'coupon', message: '今天的优惠券已经发完,请明天再来')]
39+
public function coupon2(): string
40+
{
41+
return '优惠券发送成功';
42+
}
43+
3744
}

tests/config/throttle.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@
1010
return [
1111
// 缓存键前缀,防止键值与其他应用冲突
1212
'prefix' => 'throttle_',
13-
// 缓存的键,true 表示使用来源ip
13+
// 缓存的键,true 表示使用来源 ip
1414
'key' => true,
1515
// 要被限制的请求类型, eg: GET POST PUT DELETE HEAD 等
1616
'visit_method' => ['GET', 'HEAD'],
17-
// 设置访问频率,例如 '10/m' 指的是允许每分钟请求10次;'10/60'指允许每60秒请求10次。值 null 表示不限制, eg: null 10/m 20/h 300/d 200/300
17+
// 设置访问频率,例如 '10/m' 指的是允许每分钟请求10次;'10/60'指允许每60秒请求10次。空值表示不限制, eg: '', '10/m', '20/h', '300/d', '200/300'
1818
'visit_rate' => '100/m',
1919
/*
2020
* 设置节流算法,组件提供了四种算法:
@@ -28,6 +28,7 @@
2828
'visit_enable_show_rate_limit' => true,
2929
// 访问受限时返回的响应
3030
'visit_fail_response' => function (Throttle $throttle, Request $request, int $wait_seconds) {
31-
return Response::create('Too many requests, try again after ' . $wait_seconds . ' seconds.')->code(429);
31+
$content = str_replace('__WAIT__', (string)$wait_seconds, $throttle->getFailMessage());
32+
return Response::create($content)->code(429);
3233
},
3334
];

0 commit comments

Comments
 (0)