|
|
|
@ -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; |
|
|
|
} |
|
|
|
} |
|
|
|
|