162 lines
5.3 KiB
PHP
162 lines
5.3 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Http\Controllers\Admin;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use Illuminate\Contracts\View\View;
|
|
use Illuminate\Http\RedirectResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Http\Response;
|
|
use Illuminate\Support\Facades\RateLimiter;
|
|
use Illuminate\Support\Str;
|
|
use Illuminate\Validation\ValidationException;
|
|
|
|
class AuthController extends Controller
|
|
{
|
|
private const LOGIN_MAX_ATTEMPTS = 5;
|
|
|
|
private const LOGIN_DECAY_SECONDS = 120;
|
|
|
|
public function showLogin(): View
|
|
{
|
|
return view('admin.auth.login');
|
|
}
|
|
|
|
/**
|
|
* @throws ValidationException
|
|
*/
|
|
public function login(Request $request): RedirectResponse
|
|
{
|
|
$payload = $request->validate([
|
|
'username' => ['required', 'string', 'max:100'],
|
|
'password' => ['required', 'string', 'max:255'],
|
|
'captcha' => ['required', 'string', 'size:5'],
|
|
], [
|
|
'username.required' => '请输入后台账号。',
|
|
'password.required' => '请输入后台密码。',
|
|
'captcha.required' => '请输入验证码。',
|
|
'captcha.size' => '验证码格式不正确。',
|
|
]);
|
|
|
|
$rateLimiterKey = $this->loginRateLimiterKey($request, $payload['username']);
|
|
|
|
if (RateLimiter::tooManyAttempts($rateLimiterKey, self::LOGIN_MAX_ATTEMPTS)) {
|
|
$seconds = RateLimiter::availableIn($rateLimiterKey);
|
|
|
|
throw ValidationException::withMessages([
|
|
'username' => "尝试次数过多,请在 {$seconds} 秒后重试。",
|
|
]);
|
|
}
|
|
|
|
if (! $this->isCaptchaValid($request, $payload['captcha'])) {
|
|
RateLimiter::hit($rateLimiterKey, self::LOGIN_DECAY_SECONDS);
|
|
|
|
throw ValidationException::withMessages([
|
|
'captcha' => '验证码错误,请重试。',
|
|
]);
|
|
}
|
|
|
|
if (! $this->isCredentialValid($payload['username'], $payload['password'])) {
|
|
RateLimiter::hit($rateLimiterKey, self::LOGIN_DECAY_SECONDS);
|
|
|
|
throw ValidationException::withMessages([
|
|
'username' => '账号或密码错误。',
|
|
]);
|
|
}
|
|
|
|
RateLimiter::clear($rateLimiterKey);
|
|
|
|
$request->session()->regenerate();
|
|
$request->session()->put('admin_authenticated', true);
|
|
$request->session()->put('admin_username', $payload['username']);
|
|
$request->session()->forget('admin_captcha_hash');
|
|
|
|
return redirect()->route('admin.dashboard');
|
|
}
|
|
|
|
public function logout(Request $request): RedirectResponse
|
|
{
|
|
$request->session()->forget(['admin_authenticated', 'admin_username', 'admin_captcha_hash']);
|
|
$request->session()->invalidate();
|
|
$request->session()->regenerateToken();
|
|
|
|
return redirect()->route('admin.login')->with('status', '已安全退出后台。');
|
|
}
|
|
|
|
public function captcha(Request $request): Response
|
|
{
|
|
$captchaText = strtoupper(Str::random(5));
|
|
$request->session()->put('admin_captcha_hash', hash('sha256', strtolower($captchaText)));
|
|
|
|
$imageWidth = 140;
|
|
$imageHeight = 46;
|
|
|
|
$image = imagecreatetruecolor($imageWidth, $imageHeight);
|
|
|
|
$background = imagecolorallocate($image, 241, 245, 249);
|
|
$textColor = imagecolorallocate($image, 15, 23, 42);
|
|
$lineColor = imagecolorallocate($image, 148, 163, 184);
|
|
$noiseColor = imagecolorallocate($image, 100, 116, 139);
|
|
|
|
imagefilledrectangle($image, 0, 0, $imageWidth, $imageHeight, $background);
|
|
|
|
for ($index = 0; $index < 4; $index++) {
|
|
imageline(
|
|
$image,
|
|
random_int(0, $imageWidth),
|
|
random_int(0, $imageHeight),
|
|
random_int(0, $imageWidth),
|
|
random_int(0, $imageHeight),
|
|
$lineColor,
|
|
);
|
|
}
|
|
|
|
for ($index = 0; $index < 90; $index++) {
|
|
imagesetpixel($image, random_int(0, $imageWidth - 1), random_int(0, $imageHeight - 1), $noiseColor);
|
|
}
|
|
|
|
imagestring($image, 5, 24, 14, $captchaText, $textColor);
|
|
|
|
ob_start();
|
|
imagepng($image);
|
|
$binary = (string) ob_get_clean();
|
|
imagedestroy($image);
|
|
|
|
return response($binary, 200, [
|
|
'Content-Type' => 'image/png',
|
|
'Cache-Control' => 'no-store, no-cache, must-revalidate, max-age=0',
|
|
'Pragma' => 'no-cache',
|
|
'Expires' => '0',
|
|
]);
|
|
}
|
|
|
|
private function isCredentialValid(string $username, string $password): bool
|
|
{
|
|
$configuredUser = (string) config('app.admin_user', 'admin');
|
|
$configuredPassword = (string) config('app.admin_password', 'change-me');
|
|
|
|
return hash_equals($configuredUser, $username)
|
|
&& hash_equals($configuredPassword, $password);
|
|
}
|
|
|
|
private function isCaptchaValid(Request $request, string $input): bool
|
|
{
|
|
$storedHash = (string) $request->session()->get('admin_captcha_hash', '');
|
|
|
|
$request->session()->forget('admin_captcha_hash');
|
|
|
|
if ($storedHash === '') {
|
|
return false;
|
|
}
|
|
|
|
return hash_equals($storedHash, hash('sha256', strtolower(trim($input))));
|
|
}
|
|
|
|
private function loginRateLimiterKey(Request $request, string $username): string
|
|
{
|
|
return Str::lower($username).'|'.$request->ip();
|
|
}
|
|
}
|