'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, // 限流算法驱动 ]; 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(Cache $cache, Config $config) { $this->cache = $cache; $this->config = array_merge(static::$default_config, $config->get('throttle', [])); } /** * 请求是否允许 * @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::getInstance()->invokeClass($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 Closure $next * @param array $params * @return Response */ public function handle(Request $request, Closure $next, array $params=[]): Response { if ($params) { $this->config = array_merge($this->config, $params); } $allow = $this->allowRequest($request); if (!$allow) { // 访问受限 throw $this->buildLimitException($this->wait_seconds, $request); } $response = $next($request); if (200 <= $response->getCode() && 300 > $response->getCode() && $this->config['visit_enable_show_rate_limit']) { // 将速率限制 headers 添加到响应中 $response->header($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 = Container::getInstance()->invokeFunction($key, [$this, $request]); } if ($key === null || $key === false || $this->config['visit_rate'] === null) { // 关闭当前限制 return null; } if ($key === true) { $key = $request->ip(); } elseif (false !== strpos($key, '__')) { $key = str_replace(['__CONTROLLER__', '__ACTION__', '__IP__'], [$request->controller(), $request->action(), $request->ip()], $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' => $this->remaining < 0 ? 0 : $this->remaining, 'X-Rate-Limit-Reset' => $this->now + $this->expire, ]; } /** * 构建 Response Exception * @param int $wait_seconds * @param Request $request * @return HttpResponseException */ public function buildLimitException(int $wait_seconds, Request $request): HttpResponseException { $visitFail = $this->config['visit_fail_response'] ?? null; if ($visitFail instanceof \Closure) { $response = Container::getInstance()->invokeFunction($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 = Response::create($content)->code($this->config['visit_fail_code']); } if ($this->config['visit_enable_show_rate_limit']) { $response->header(['Retry-After' => $wait_seconds]); } return new HttpResponseException($response); } }