init
This commit is contained in:
161
app/Http/Controllers/Admin/AuthController.php
Normal file
161
app/Http/Controllers/Admin/AuthController.php
Normal file
@@ -0,0 +1,161 @@
|
||||
<?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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user