让Lumen支持请求控制

Lumen 是你构建微服务架构和 API 应用的完美解决方案,事实上,她是全宇宙最快的框架之一,甚至要快过以速度著称的 SilexSlim,现在,为你的 Laravel 应用程序编写微服务架构变得再简单不过了。

但是你在使用的过程中,你会发现很多 Laravel 中好用的功能都被精简了,比如说请求控制中间件 Throttle。这个中间件能简单的实现请求控制。那么接下来跟我一起为 Lumen 重新添加这么好用的功能吧。

从 Laravel 移植

GitHub地址

保存为 app/Http/Middleware/ThrottleRequests.php,别忘了修改 namespaceApp\Http\Middleware,完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
<?php
namespace App\Http\Middleware;

use Closure;
use Illuminate\Cache\RateLimiter;
use Symfony\Component\HttpFoundation\Response;

class ThrottleRequests
{
/**
* The rate limiter instance.
*
* @var \Illuminate\Cache\RateLimiter
*/
protected $limiter;

/**
* Create a new request throttler.
*
* @param \Illuminate\Cache\RateLimiter $limiter
* @return void
*/
public function __construct(RateLimiter $limiter)
{
$this->limiter = $limiter;
}

/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param int $maxAttempts
* @param int $decayMinutes
* @return mixed
*/
public function handle($request, Closure $next, $maxAttempts = 60, $decayMinutes = 1)
{
$key = $this->resolveRequestSignature($request);

if ($this->limiter->tooManyAttempts($key, $maxAttempts, $decayMinutes)) {
return $this->buildResponse($key, $maxAttempts);
}

$this->limiter->hit($key, $decayMinutes);

$response = $next($request);

return $this->addHeaders(
$response, $maxAttempts,
$this->calculateRemainingAttempts($key, $maxAttempts)
);
}

/**
* Resolve request signature.
*
* @param \Illuminate\Http\Request $request
* @return string
*/
protected function resolveRequestSignature($request)
{
// return $request->fingerprint();
// Lumen 版本缺少 Request::fingerprint 方法
return sha1(
$request->method() .
'|' . $request->server('SERVER_NAME') .
'|' . $request->path() .
'|' . $request->ip()
);
}

/**
* Create a 'too many attempts' response.
*
* @param string $key
* @param int $maxAttempts
* @return \Illuminate\Http\Response
*/
protected function buildResponse($key, $maxAttempts)
{
$response = new Response('Too Many Attempts.', 429);

$retryAfter = $this->limiter->availableIn($key);

return $this->addHeaders(
$response, $maxAttempts,
$this->calculateRemainingAttempts($key, $maxAttempts, $retryAfter),
$retryAfter
);
}

/**
* Add the limit header information to the given response.
*
* @param \Symfony\Component\HttpFoundation\Response $response
* @param int $maxAttempts
* @param int $remainingAttempts
* @param int|null $retryAfter
* @return \Illuminate\Http\Response
*/
protected function addHeaders(Response $response, $maxAttempts, $remainingAttempts, $retryAfter = null)
{
$headers = [
'X-RateLimit-Limit' => $maxAttempts,
'X-RateLimit-Remaining' => $remainingAttempts,
];

if (!is_null($retryAfter)) {
$headers['Retry-After'] = $retryAfter;
}

$response->headers->add($headers);

return $response;
}

/**
* Calculate the number of remaining attempts.
*
* @param string $key
* @param int $maxAttempts
* @param int|null $retryAfter
* @return int
*/
protected function calculateRemainingAttempts($key, $maxAttempts, $retryAfter = null)
{
if (!is_null($retryAfter)) {
return 0;
}

return $this->limiter->retriesLeft($key, $maxAttempts);
}
}

注册中间件

找到 bootstrap/app.php 文件,添加:

1
2
3
$app->routeMiddleware([
'throttle' => App\Http\Middleware\ThrottleRequests::class,
]);

更新 composer

1
composer dump-autoload

测试

找到 routes/web.php,修改 or 添加

1
2
3
4
5
6
7
8
9
10
11
12
// 默认速率是60请求/分钟
$router->group(['prefix' => 'api', 'middleware'=>'throttle'], function () use ($router) {
$router->get('/', function () {
return $router->app->version();
});
});
// 自定义速率是2请求/分钟
$router->group(['prefix' => 'home', 'middleware'=>'throttle:2,1'], function () use ($router) {
$router->get('/', function () {
return $router->app->version();
});
});