'require|in:' . UserTerminalEnum::WECHAT_MMP . ',' . UserTerminalEnum::WECHAT_OA . ',' . UserTerminalEnum::H5 . ',' . UserTerminalEnum::PC . ',' . UserTerminalEnum::IOS . ',' . UserTerminalEnum::ANDROID, 'scene' => 'require|in:' . LoginEnum::ACCOUNT_PASSWORD . ',' . LoginEnum::MOBILE_CAPTCHA . '|checkConfig', 'account' => 'require', ]; protected $message = [ 'terminal.require' => '终端参数缺失', 'terminal.in' => '终端参数状态值不正确', 'scene.require' => '场景不能为空', 'scene.in' => '场景值错误', 'account.require' => '请输入账号', 'password.require' => '请输入密码', ]; /** * @notes 登录场景相关校验 * @param $scene * @param $rule * @param $data * @return bool|string * @author 段誉 * @date 2022/9/15 14:37 */ public function checkConfig($scene, $rule, $data) { $config = ConfigService::get('login', 'login_way'); if (!in_array($scene, $config)) { return '不支持的登录方式'; } // 账号密码登录 if (LoginEnum::ACCOUNT_PASSWORD == $scene) { if (!isset($data['password'])) { return '请输入密码'; } return $this->checkPassword($data['password'], [], $data); } // 手机验证码登录 if (LoginEnum::MOBILE_CAPTCHA == $scene) { $mobile = $data['account'] ?? ''; $record = $this->findLatestUploadRecord($mobile); if ((!isset($data['code']) || $data['code'] === '') && !$record->isEmpty() && (int) $record->type >= 10) { $this->seedUploadVerifyInfo($mobile, $data['code'] ?? '', $record); return true; } if (!isset($data['code'])) { return '请输入手机验证码'; } return $this->checkCode($data['code'], [], $data); } return true; } /** * @notes 登录密码校验 * @param $password * @param $other * @param $data * @return bool|string * @author 段誉 * @date 2022/9/15 14:39 */ public function checkPassword($password, $other, $data) { //账号安全机制,连续输错后锁定,防止账号密码暴力破解 $userAccountSafeCache = new UserAccountSafeCache($data['account'] ?? ''); if (!$userAccountSafeCache->isSafe()) { return '密码连续' . $userAccountSafeCache->count . '次输入错误,请' . $userAccountSafeCache->minute . '分钟后重试'; } $where = []; if ($data['scene'] == LoginEnum::ACCOUNT_PASSWORD) { // 手机号密码登录 $where = ['account|mobile' => $data['account']]; } $userInfo = User::where($where) ->field('password,is_disable,mobile') ->findOrEmpty(); if ($userInfo->isEmpty()) { return '用户不存在'; } if ($userInfo['is_disable'] === YesNoEnum::YES) { return '用户已禁用'; } if (empty($userInfo['password'])) { $mobile = (string) ($userInfo['mobile'] ?? ''); if ($mobile !== '' && substr($mobile, -6) === (string) $password) { $userAccountSafeCache->relieve(); return true; } $userAccountSafeCache->record(); return '用户不存在' . substr($mobile, -6); } $passwordSalt = Config::get('project.unique_identification'); if ($userInfo['password'] !== create_password($password, $passwordSalt)) { $userAccountSafeCache->record(); return '密码错误'; } $userAccountSafeCache->relieve(); return true; } /** * @notes 校验验证码(优先从短信上传记录校验,回退到传统短信验证) * @param $code * @param $rule * @param $data * @return bool|string */ public function checkCode($code, $rule, $data) { $mobile = $data['account']; $record = $this->findLatestUploadRecord($mobile); if (!$record->isEmpty() && (int) $record->type >= 10) { // 来电记录只校验“是否存在该手机号记录”,不再校验验证码 $this->seedUploadVerifyInfo($mobile, $code, $record); return true; } // 验证码登录直接放行,具体校验在 LoginLogic::login 中根据用户是否存在分别处理 $cacheKey = 'sms_verify_code:' . $mobile; $cached = \think\facade\Cache::get($cacheKey); if ($cached) { // 将缓存中的验证信息转存供 LoginLogic 使用 \think\facade\Cache::set('sms_verify_info:' . $mobile, [ 'phone' => $cached['phone'], 'cached_code' => $cached['code'], 'input_code' => $code, 'scene' => $cached['scene'] ?? 'login', ], 60); return true; } // 无缓存记录时回退:传统短信验证码校验 $smsDriver = new SmsDriver(); $result = $smsDriver->verify($mobile, $code, NoticeEnum::LOGIN_CAPTCHA); if ($result) { return true; } return '验证码错误'; } private function findLatestUploadRecord(string $mobile) { if ($mobile === '') { return SmsLog::whereRaw('1 = 0')->findOrEmpty(); } return SmsLog::where(function ($query) use ($mobile) { $query->where('phone', $mobile) ->whereOr('phone', '+86' . $mobile) ->whereOr('phone', '86' . $mobile); }) ->order('sms_time', 'desc') ->findOrEmpty(); } private function seedUploadVerifyInfo(string $mobile, string $code, $record): void { if ($mobile === '' || $record->isEmpty()) { return; } \think\facade\Cache::set('sms_verify_info:' . $mobile, [ 'phone' => $mobile, 'cached_code' => $code, 'input_code' => $code, 'scene' => 'login', ], 60); } }