Skip to content

Instantly share code, notes, and snippets.

@Gummibeer
Last active September 2, 2020 03:57
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save Gummibeer/47cbf85d9d9eeca82670103f6cb5e86f to your computer and use it in GitHub Desktop.
Save Gummibeer/47cbf85d9d9eeca82670103f6cb5e86f to your computer and use it in GitHub Desktop.
Laravel ThrottlesRequests trait
<?php
if(!function_exists('clamp')) {
function clamp(float $min, float $value, float $max) {
return min(max($min, $value), $max);
}
}
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Concerns\ThrottlesRequests;
use App\Http\Requests\Auth\SignInRequest;
use App\Http\Requests\Auth\SignUpRequest;
use App\Models\User;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;
use Illuminate\View\View;
class SignInController
{
use ThrottlesRequests;
public function form(): View
{
return view('auth.signin');
}
public function __invoke(SignInRequest $request): RedirectResponse
{
$this->throttled($request);
if (Auth::attempt(
$request->only('email', 'password'),
$request->get('remember_me', false)
)) {
$request->session()->regenerate();
$this->clearAttempts($request);
return redirect()->intended(route('app.dashboard'));
}
throw ValidationException::withMessages([
'email' => [trans('auth.failed')],
]);
}
}
<?php
namespace App\Http\Controllers\Concerns;
use Carbon\CarbonInterval;
use Illuminate\Auth\Events\Lockout;
use Illuminate\Cache\RateLimiter;
use Illuminate\Contracts\Cache\Repository as CacheContract;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
trait ThrottlesRequests
{
protected function throttled(Request $request): void
{
if ($this->hasTooManyAttempts($request)) {
$this->lockout($request);
}
$this->incrementAttempts($request);
}
protected function hasTooManyAttempts(Request $request): bool
{
return $this->limiter()->tooManyAttempts(
$this->throttleKey($request),
$this->maxAttempts()
);
}
protected function incrementAttempts(Request $request): void
{
$hits = $this->limiter()->hit(
$this->throttleKey($request), $this->decayMinutes($request) * 60
);
if ($hits >= $this->maxAttempts()) {
$this->cache()->add($this->throttleKey($request, 'long'), 0, CarbonInterval::day()->totalSeconds);
$this->cache()->increment($this->throttleKey($request, 'long'));
}
}
protected function clearAttempts(Request $request): void
{
$this->limiter()->clear($this->throttleKey($request));
$this->cache()->forget($this->throttleKey($request, 'long'));
}
protected function lockout(Request $request)
{
event(new Lockout($request));
throw ValidationException::withMessages([
'_throttle' => $this->limiter()->availableIn($this->throttleKey($request)),
])->status(Response::HTTP_TOO_MANY_REQUESTS);
}
protected function throttleKey(Request $request, ?string $suffix = null): string
{
return collect([
$request->path(),
$request->ip(),
$suffix,
])
->filter()
->map(fn(string $part): string => Str::lower($part))
->implode('|');
}
protected function limiter(): RateLimiter
{
return app(RateLimiter::class);
}
protected function cache(): CacheContract
{
return app('cache')->store();
}
protected function maxAttempts(): int
{
return property_exists($this, 'maxAttempts') ? $this->maxAttempts : 3;
}
protected function decayExponent(): int
{
return property_exists($this, 'decayExponent') ? $this->decayExponent : 1.25;
}
protected function decayMinutes(Request $request): int
{
$minutes = property_exists($this, 'decayMinutes') ? $this->decayMinutes : 10;
$multiplier = $this->cache()->get($this->throttleKey($request, 'long'), 0) + 1;
return clamp(
$minutes,
round(pow($multiplier, $this->decayExponent()) * $minutes),
CarbonInterval::day()->totalMinutes
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment