*/ namespace app\controller\admin; use app\model\AdminAuthGroupAccess; use app\model\AdminAuthRule; use app\model\AdminMenu; use app\model\AdminUser; 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 * @throws \think\db\exception\DataNotFoundException * @throws \think\db\exception\DbException * @throws \think\db\exception\ModelNotFoundException * @author zhaoxiang */ 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, '缺少用户名!'); } if (!$password) { return $this->buildFailed(ReturnCode::LOGIN_ERROR, '缺少密码!'); } 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']) { //更新用户数据 $userData = $userInfo->userData; $data = []; if ($userData) { $userData->login_times++; $userData->last_login_ip = sprintf("%u", ip2long($this->request->ip())); $userData->last_login_time = time(); $userData->save(); } else { $data['login_times'] = 1; $data['uid'] = $userInfo['id']; $data['last_login_ip'] = sprintf("%u", ip2long($this->request->ip())); $data['last_login_time'] = time(); $data['head_img'] = ''; AdminUserData::create($data); $userInfo['userData'] = $data; } } else { return $this->buildFailed(ReturnCode::LOGIN_ERROR, '用户已被封禁,请联系管理员'); } } else { $count = $this->setFailKey($username); return $this->buildFailed( ReturnCode::LOGIN_ERROR, '用户名密码不正确', ['need_captcha' => $count >= $this->captchaThreshold] ); } $userInfo['access'] = $this->getAccess($userInfo['id']); $userInfo['menu'] = $this->getAccessMenuData($userInfo['id']); $apiAuth = md5(uniqid() . time()); cache('Login:' . $apiAuth, json_encode($userInfo), config('apiadmin.ONLINE_TIME')); cache('Login:' . $userInfo['id'], $apiAuth, config('apiadmin.ONLINE_TIME')); $userInfo['apiAuth'] = $apiAuth; return $this->buildSuccess($userInfo->toArray(), '登录成功'); } /** * 获取用户信息 * @return Response * @author zhaoxiang */ public function getUserInfo(): Response { return $this->buildSuccess($this->userInfo); } /** * 用户登出 * @return Response * @author zhaoxiang */ public function logout(): Response { $ApiAuth = $this->request->header('Api-Auth'); cache('Login:' . $ApiAuth, null); cache('Login:' . $this->userInfo['id'], null); return $this->buildSuccess([], '登出成功'); } /** * 获取当前用户的允许菜单 * @return Response * @throws \think\db\exception\DataNotFoundException * @throws \think\db\exception\DbException * @throws \think\db\exception\ModelNotFoundException * @author zhaoxiang */ public function getAccessMenu(): Response { return $this->buildSuccess($this->getAccessMenuData($this->userInfo['id'])); } /** * 获取当前用户的允许菜单 * @param int $uid * @return array * @throws \think\db\exception\DataNotFoundException * @throws \think\db\exception\DbException * @throws \think\db\exception\ModelNotFoundException * @author zhaoxiang */ public function getAccessMenuData(int $uid): array { $returnData = []; $isSupper = Tools::isAdministrator($uid); if ($isSupper) { $access = (new AdminMenu())->where('router', '<>', '')->select(); $returnData = Tools::listToTree(Tools::buildArrFromObj($access)); } else { $groups = (new AdminAuthGroupAccess())->where('uid', $uid)->find(); if (isset($groups) && $groups->group_id) { $access = (new AdminAuthRule())->whereIn('group_id', $groups->group_id)->select(); $access = array_unique(array_column(Tools::buildArrFromObj($access), 'url')); array_push($access, ""); $menus = (new AdminMenu())->whereIn('url', $access)->where('show', 1)->select(); $returnData = Tools::listToTree(Tools::buildArrFromObj($menus)); RouterTool::buildVueRouter($returnData); } } return array_values($returnData); } /** * 获取用户权限数据 * @param $uid * @return array * @throws \think\db\exception\DataNotFoundException * @throws \think\db\exception\DbException * @throws \think\db\exception\ModelNotFoundException * @author zhaoxiang */ public function getAccess(int $uid): array { $isSupper = Tools::isAdministrator($uid); if ($isSupper) { $access = (new AdminMenu())->select(); $access = Tools::buildArrFromObj($access); return array_values(array_filter(array_column($access, 'url'))); } else { $groups = (new AdminAuthGroupAccess())->where('uid', $uid)->find(); if (isset($groups) && $groups->group_id) { $access = (new AdminAuthRule())->whereIn('group_id', $groups->group_id)->select(); $access = Tools::buildArrFromObj($access); return array_values(array_unique(array_column($access, 'url'))); } else { return []; } } } 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 setFailKey($username) { $failKey = "login:fail:{$username}"; $failCount = (int) Cache::get($failKey, 0); $failCount += 1; Cache::set($failKey, $failCount, $this->lockMinutes * 60); return $failCount; } /** * 原子增加失败次数 */ protected function incrementFailCount(string $key): int { $count = Cache::inc($key); if ($count == 1) { Cache::expire($key, $this->lockMinutes * 60); } return $count; } }