diff --git a/app/controller/admin/Login.php b/app/controller/admin/Login.php index be169ec..5077958 100644 --- a/app/controller/admin/Login.php +++ b/app/controller/admin/Login.php @@ -16,10 +16,16 @@ use app\model\AdminUserData; use app\util\ReturnCode; use app\util\RouterTool; use app\util\Tools; +use think\captcha\facade\Captcha; +use think\facade\Cache; use think\Response; class Login extends Base { + protected $maxAttempts = 5; // 最大失败次数 + protected $lockMinutes = 1; // 锁定分钟数 + protected $captchaThreshold = 3; // 失败多少次后需要验证码 + /** * 用户登录【账号密码登录】 * @return Response @@ -31,6 +37,8 @@ class Login extends Base { public function index(): Response { $username = $this->request->post('username'); $password = $this->request->post('password'); + $captchaToken = $this->request->post('captcha_token'); + $captchaInput = $this->request->post('captcha'); if (!$username) { return $this->buildFailed(ReturnCode::LOGIN_ERROR, '缺少用户名!'); } @@ -39,6 +47,30 @@ class Login extends Base { } else { $password = Tools::userMd5($password); } + // 2. 【硬拦截】检查是否已被锁定 + $failKey = "login:fail:{$username}"; + $failCount = (int) Cache::get($failKey, 0); + if ($failCount >= $this->maxAttempts) { + return $this->buildFailed(ReturnCode::LOGIN_ERROR, "账户已被临时锁定,请{$this->lockMinutes}分钟后再试"); + } + + // 3. 判断是否需要验证码(失败 ≥ 3 次) + $needCaptcha = $failCount >= $this->captchaThreshold; + + // 4. 如需验证码,进行校验 + if ($needCaptcha) { + if (empty($captchaToken) || empty($captchaInput)) { + return $this->buildFailed(ReturnCode::LOGIN_ERROR, "请输入验证码", ['need_captcha' => true]); + } + $storedCode = Cache::get($captchaToken); + if (!$storedCode || strtolower($storedCode) !== strtolower($captchaInput)) { + // 验证码错误也记一次失败[reference:8] + $this->incrementFailCount($failKey); + return $this->buildFailed(ReturnCode::LOGIN_ERROR, "验证码错误", ['need_captcha' => true]); + } + // 验证码校验通过后立即删除,防止重放攻击[reference:9] + Cache::delete($captchaToken); + } $userInfo = (new AdminUser())->where('username', $username)->where('password', $password)->find(); if (!empty($userInfo)) { if ($userInfo['status']) { @@ -64,6 +96,7 @@ class Login extends Base { return $this->buildFailed(ReturnCode::LOGIN_ERROR, '用户已被封禁,请联系管理员'); } } else { + $this->setFailKey($username); return $this->buildFailed(ReturnCode::LOGIN_ERROR, '用户名密码不正确'); } $userInfo['access'] = $this->getAccess($userInfo['id']); @@ -170,4 +203,44 @@ class Login extends Base { } } } + + public function getCaptcha(): Response + { + // 生成验证码(api模式下返回数组:['code' => 验证码文本, 'img' => base64图片]) + $captcha = Captcha::create(); + + // 生成唯一 token 作为 Redis key + $token = 'captcha_' . md5(uniqid() . microtime(true)); + + // 存入 Redis,有效期 300 秒 + Cache::set($token, $captcha['code'], 300); + + return $this->buildSuccess([ + 'token' => $token, + 'img' => $captcha['img'], // 已是完整的 base64 data:image/png;base64,... + ]); + } + + protected function captchaCheck($username, $captchaToken, $captchaInput) + { + } + + protected function setFailKey($username) + { + $failKey = "login:fail:{$username}"; + $failCount = (int) Cache::get($failKey, 0); + Cache::set($failKey, $failCount + 1, $this->lockMinutes * 60); + } + + /** + * 原子增加失败次数 + */ + protected function incrementFailCount(string $key): int + { + $count = Cache::inc($key); + if ($count == 1) { + Cache::expire($key, $this->lockMinutes * 60); + } + return $count; + } } diff --git a/route/app.php b/route/app.php index a1ad4ae..98984ec 100644 --- a/route/app.php +++ b/route/app.php @@ -7,6 +7,7 @@ Route::group('admin', function() { Route::rule('ThirdLogin/wx', 'admin.ThirdLogin/wx', 'get'); // end Route::rule('Login/index', 'admin.Login/index', 'post')->middleware([app\middleware\AdminResponse::class]); + Route::rule('Login/captcha', 'admin.Login/getCaptcha', 'get')->middleware([app\middleware\AdminResponse::class]); Route::rule('Login/logout', 'admin.Login/logout', 'get')->middleware([app\middleware\AdminAuth::class, app\middleware\AdminLog::class, app\middleware\AdminResponse::class]); Route::rule('Menu/changeStatus', 'admin.Menu/changeStatus', 'get')->middleware([app\middleware\AdminAuth::class, app\middleware\AdminPermission::class, app\middleware\AdminLog::class, app\middleware\AdminResponse::class]); Route::rule('Menu/add', 'admin.Menu/add', 'post')->middleware([app\middleware\AdminAuth::class, app\middleware\AdminPermission::class, app\middleware\AdminLog::class, app\middleware\AdminResponse::class]);