4096 || str_contains($account, "\0") || !is_string($password) || $password === '' || strlen($password) > 4096 || str_contains($password, "\0")) { JsonService::throw('登录参数不合法'); } return Db::transaction(static function () use ($account, $password): array { $admin = Admin::where('account', $account)->lock(true)->findOrEmpty(); $salt = (string) Config::get('project.unique_identification'); if ($admin->isEmpty() || (int) $admin['disable'] !== 0 || !hash_equals((string) $admin['password'], create_password($password, $salt))) { JsonService::throw('账号或密码错误,或账号已停用', [], -1); } return ['id' => (int) $admin['id'], 'account' => (string) $admin['account'], 'name' => (string) ($admin['name'] ?? ''), 'disable' => 0, 'requires_password_change' => (int) ($admin['is_paw'] ?? 1) === 0, 'requires_wecom_binding' => LoginLogic::adminMustBindWorkWechat([ 'root' => (int) ($admin['root'] ?? 0), 'work_wechat_userid' => (string) ($admin['work_wechat_userid'] ?? ''), ])]; }); } /** 与个人改密共用账号行锁,绝不信任事务前验证器的密码快照或调试免密结果。 */ public static function login(array $params): array { if (array_diff(array_keys($params), ['account', 'password', 'terminal']) !== []) { JsonService::throw('请求包含不支持的字段'); } $account = $params['account'] ?? null; $password = $params['password'] ?? null; $terminal = $params['terminal'] ?? null; if (!is_string($account) || $account === '' || strlen($account) > 4096 || str_contains($account, "\0") || !is_string($password) || $password === '' || strlen($password) > 4096 || str_contains($password, "\0") || (!is_int($terminal) && !is_string($terminal)) || !in_array((string) $terminal, ['1', '2', '7', '9', (string) AdminTerminalEnum::GROUP_IDENTITY], true)) { JsonService::throw('登录参数不合法'); } return Db::transaction(static function () use ($account, $password, $params): array { $admin = Admin::where('account', $account)->lock(true)->findOrEmpty(); $salt = (string) Config::get('project.unique_identification'); if ($admin->isEmpty() || (int) $admin['disable'] !== 0 || !hash_equals((string) $admin['password'], create_password($password, $salt))) { JsonService::throw('账号或密码错误,或账号已停用', [], -1); } // 原登录的资料更新和会话签发均在该账号锁释放前完成。 return (new LoginLogic())->login($params); }); } public static function changePassword(string $token, array $params): void { if (array_diff(array_keys($params), ['old_password', 'password', 'password_confirm']) !== []) { JsonService::throw('请求包含不支持的字段'); } $password = $params['password'] ?? null; if (!is_string($password) || strlen($password) < 6 || strlen($password) > 32 || str_contains($password, "\0")) { JsonService::throw('密码长度须在6-32字节'); } if ($password !== ($params['password_confirm'] ?? null)) { JsonService::throw('两次输入的密码不一致'); } $tokens = Db::transaction(static function () use ($token, $params, $password): array { [$admin] = self::lockContext($token); $firstPassword = (int) ($admin['is_paw'] ?? 1) === 0; if (!$firstPassword && LoginLogic::adminMustBindWorkWechat([ 'root' => (int) $admin['root'], 'work_wechat_userid' => (string) ($admin['work_wechat_userid'] ?? ''), ])) { JsonService::throw('请先在原后台绑定企业微信', [], LoginLogic::CODE_NEED_BIND_WORK_WECHAT); } $salt = (string) Config::get('project.unique_identification'); if (!$firstPassword) { $oldPassword = $params['old_password'] ?? null; if (!is_string($oldPassword) || strlen($oldPassword) > 1024 || !hash_equals((string) $admin['password'], create_password($oldPassword, $salt))) { JsonService::throw('当前密码错误'); } } $id = (int) $admin['id']; Admin::where('id', $id)->update([ 'password' => create_password($password, $salt), 'is_paw' => 1, 'update_time' => time(), ]); return self::expire($id, null); }); self::clearTokens($tokens); } public static function logout(string $token, bool $all = false): void { $tokens = Db::transaction(static function () use ($token, $all): array { [$admin] = self::lockContext($token); return self::expire((int) $admin['id'], $all ? null : $token); }); self::clearTokens($tokens); } /** 先锁账号再锁会话,避免两个会话同时改密时互相等待。 */ private static function lockContext(string $token): array { if ($token === '' || strlen($token) > 512 || preg_match('/[\x00-\x20\x7f]/', $token)) { JsonService::throw('请先登录', [], -1); } $candidate = AdminSession::where('token', $token)->findOrEmpty(); if ($candidate->isEmpty()) { JsonService::throw('登录已失效,请重新登录', [], -1); } $admin = Admin::where('id', (int) $candidate['admin_id'])->lock(true)->findOrEmpty(); if ($admin->isEmpty() || (int) $admin['disable'] !== 0) { JsonService::throw('登录已失效,请重新登录', [], -1); } $session = AdminSession::where('admin_id', (int) $admin['id'])->where('token', $token) ->where('expire_time', '>', time())->lock(true)->findOrEmpty(); if ($session->isEmpty()) { JsonService::throw('登录已失效,请重新登录', [], -1); } return [$admin, $session]; } private static function expire(int $adminId, ?string $token): array { $query = static function () use ($adminId, $token) { $builder = AdminSession::where('admin_id', $adminId); return $token === null ? $builder : $builder->where('token', $token); }; $tokens = $query()->column('token'); $query()->update(['expire_time' => time() - 1, 'update_time' => time()]); return $tokens; } private static function clearTokens(array $tokens): void { $cache = new AdminTokenCache(); foreach ($tokens as $token) { $cache->deleteAdminInfo((string) $token); } } }