Created
January 19, 2023 00:50
-
-
Save OSDDQD/0b0472a926ca15b9b2d01eabd4662232 to your computer and use it in GitHub Desktop.
Prevent Concurrent Requests
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Http\Middleware; | |
use Closure; | |
use Illuminate\Http\Exceptions\HttpResponseException; | |
use Illuminate\Support\Facades\Cache; | |
use RuntimeException; | |
use Symfony\Component\HttpFoundation\Response; | |
class ThrottleMiddleware | |
{ | |
/** | |
* Amount of time (in seconds) the last request will be stored, | |
* in the cache, in the case the middleware never terminates. | |
* | |
* @var int | |
*/ | |
protected $cacheForSeconds = 3600; | |
/** | |
* The limit of concurrent requests the current user can run. | |
* | |
* @var int | |
*/ | |
protected $limit; | |
/** | |
* The current user's signature. | |
* | |
* @var string | |
*/ | |
protected $signature; | |
/** | |
* Prefix to be on the request signature. | |
* | |
* @var string | |
*/ | |
protected $prefix = 'concurrent:'; | |
/** | |
* Handle the incoming request. | |
* | |
* @param \Illuminate\Http\Request $request | |
* @param \Closure $next | |
* @param int $limit | |
* @return mixed | |
*/ | |
public function handle($request, Closure $next, $limit = 1) | |
{ | |
$this->limit = (int) $limit; | |
$this->setRequestSignature($request); | |
if ($this->limit <= Cache::get($this->signature)) { | |
throw new HttpResponseException(response()->json([ | |
'error' => 'Too Many Attempts', | |
], Response::HTTP_FORBIDDEN)); | |
} | |
$this->increment(); | |
return $next($request); | |
} | |
/** | |
* Handle the outgoing response. | |
* | |
* @param \Illuminate\Http\Request $request | |
* @param \Illuminate\Http\Response $response | |
* @return mixed | |
*/ | |
public function terminate($request, $response) | |
{ | |
$this->setRequestSignature($request); | |
$this->decrement(); | |
return $response; | |
} | |
/** | |
* Get the number of remaining concurrent requests the user can run. | |
*/ | |
protected function getRemainingRequests(int $limit): int | |
{ | |
return max(0, $limit - Cache::get($this->signature)); | |
} | |
/** | |
* Manually set the signature for the current request. | |
* | |
* @param \Illuminate\Http\Request $request | |
* @param string|null $signature | |
* @return string | |
*/ | |
public function setRequestSignature($request, $signature = null) | |
{ | |
if (! empty($this->signature)) { | |
return $signature; | |
} | |
$signature = $this->prefix.sha1($signature ?? $this->resolveRequestSignature($request)); | |
$this->signature = $signature; | |
return $signature; | |
} | |
/** | |
* Resolve the request signature for the current requesting user. | |
* | |
* @param \Illuminate\Http\Request $request | |
* @return string | |
* | |
* @throws \RuntimeException | |
*/ | |
protected function resolveRequestSignature($request) | |
{ | |
if (! empty($this->signature)) { | |
return $this->signature; | |
} | |
if ($user = $request->user()) { | |
return $user->getAuthIdentifier().'|'.$request->route()->uri(); | |
} | |
if ($route = $request->route()) { | |
return $route->getDomain().'|'.$request->ip(); | |
} | |
throw new RuntimeException('Unable to generate the request signature. Route unavailable.'); | |
} | |
/** | |
* Increment the count of currently running requests for the current user by 1. | |
* | |
* @return int | |
*/ | |
protected function increment(): int | |
{ | |
$value = 1; | |
if (Cache::has($this->signature)) { | |
$value = Cache::get($this->signature) + 1; | |
} | |
Cache::put($this->signature, $value, $this->cacheForSeconds); | |
return $value; | |
} | |
/** | |
* Decrement the count of currently running requests for the current user by 1. | |
*/ | |
protected function decrement(): int | |
{ | |
if (! Cache::has($this->signature)) { | |
return 0; | |
} | |
$value = Cache::get($this->signature) - 1; | |
if ($value === 0) { | |
Cache::forget($this->signature); | |
return 0; | |
} | |
Cache::put($this->signature, $value); | |
return $value; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment