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(); } }