tickets = $tickets ?? new IdentityHospitalSsoTicketStore(app()->getRuntimePath() . 'identity-hospital-sso'); } public function issue(string $sourceToken, array $data): array { self::only($data, ['code_challenge', 'code_challenge_method']); if (($data['code_challenge_method'] ?? null) !== 'S256' || !is_string($data['code_challenge'] ?? null) || !preg_match('/^[A-Za-z0-9_-]{43}$/D', $data['code_challenge'])) { JsonService::throw('登录验证参数不合法'); } if ($sourceToken === '' || strlen($sourceToken) > 512 || preg_match('/[\x00-\x20\x7f]/', $sourceToken)) { JsonService::throw('请先登录集团工作台', [], -1); } return Db::transaction(function () use ($sourceToken, $data): array { $candidate = AdminSession::where('token', $sourceToken)->findOrEmpty(); [$admin, $session] = self::lockSource($candidate, hash('sha256', $sourceToken)); self::ready($admin); // 交接源仍受原 IP 绑定保护;目标 exchange 才允许来自医院浏览器的不同 IP。 $info = (new AdminTokenCache())->getAdminInfo($sourceToken); $ip = request()->ip(); if (!$info || (int) ($info['admin_id'] ?? 0) !== (int) $admin['id'] || (($info['login_ip'] ?? '') !== $ip && !AuthMiddleware::isTrustedRpaServerRequest( (array) $info, $ip, env('wecom_rpa.server_ips', '')))) { JsonService::throw('ip地址发生变化,请重新登录', [], -1); } return ['ticket' => $this->tickets->issue((int) $session['id'], hash('sha256', $sourceToken), $data['code_challenge'])]; }); } public function exchange(array $data): array { self::only($data, ['ticket', 'code_verifier']); if (!is_string($data['ticket'] ?? null) || !is_string($data['code_verifier'] ?? null)) { JsonService::throw('登录验证参数不合法'); } try { $record = $this->tickets->consume($data['ticket'], $data['code_verifier']); } catch (\RuntimeException $error) { JsonService::throw('登录票据已失效,请重新从集团工作台进入'); } $result = Db::transaction(static function () use ($record): array { $candidate = AdminSession::where('id', $record['session_id'])->findOrEmpty(); [$admin] = self::lockSource($candidate, $record['token_hash']); self::ready($admin); $now = time(); $target = AdminSession::where('admin_id', (int) $admin['id']) ->where('terminal', AdminTerminalEnum::GROUP_HOSPITAL)->lock(true)->findOrEmpty(); $oldToken = $target->isEmpty() ? '' : (string) $target['token']; $token = $oldToken; if ($target->isEmpty() || (int) $target['expire_time'] <= $now || (int) $admin['multipoint_login'] === 0) { // 原 admin_session.token 为 varchar(32),保持128位随机性且不修改业务表。 $token = bin2hex(random_bytes(16)); } $values = ['token' => $token, 'expire_time' => $now + (int) Config::get('project.admin_token.expire_duration'), 'update_time' => $now]; if ($target->isEmpty()) { AdminSession::create($values + ['admin_id' => (int) $admin['id'], 'terminal' => AdminTerminalEnum::GROUP_HOSPITAL]); } else { $target->save($values); } // Persist the independent session under the same account/source locks. Publish its cache only after commit. return ['token' => $token, 'old_token' => $oldToken, 'name' => (string) $admin['name'], 'avatar' => (string) $admin['avatar']]; }); $cache = new AdminTokenCache(); if ($result['old_token'] !== '' && $result['old_token'] !== $result['token']) { $cache->deleteAdminInfo($result['old_token']); } // This executes in the real hospital browser request, retaining the original per-token IP protection. $info = $cache->setAdminInfo($result['token']); if (!$info) { JsonService::throw('登录已失效,请重新进入', [], -1); } return ['name' => $result['name'], 'avatar' => $result['avatar'], 'role_name' => $info['role_name'], 'token' => $result['token'], 'is_paw' => 1, 'need_bind_work_wechat' => false]; } private static function lockSource(AdminSession $candidate, string $tokenHash): array { if ($candidate->isEmpty()) { JsonService::throw('集团登录已失效,请重新登录', [], -1); } // Same order as IdentitySessionService and account changes: account row, then the exact session row. $admin = Admin::where('id', (int) $candidate['admin_id'])->lock(true)->findOrEmpty(); if ($admin->isEmpty() || (int) $admin['disable'] !== 0 || !empty($admin->getData('delete_time'))) { JsonService::throw('集团登录已失效,请重新登录', [], -1); } $session = AdminSession::where('id', (int) $candidate['id'])->where('admin_id', (int) $admin['id']) ->where('expire_time', '>', time())->lock(true)->findOrEmpty(); if ($session->isEmpty() || !hash_equals($tokenHash, hash('sha256', (string) $session['token'])) || (int) $session['terminal'] === AdminTerminalEnum::GROUP_HOSPITAL) { JsonService::throw('集团登录已失效,请重新登录', [], -1); } return [$admin, $session]; } private static function ready(Admin $admin): void { if ((int) $admin['is_paw'] !== 1) { JsonService::throw('请先在集团工作台完成首次密码修改'); } if (LoginLogic::adminMustBindWorkWechat(['root' => (int) $admin['root'], 'work_wechat_userid' => (string) ($admin['work_wechat_userid'] ?? '')])) { JsonService::throw('请先在原后台完成企业微信绑定', [], LoginLogic::CODE_NEED_BIND_WORK_WECHAT); } } private static function only(array $data, array $allowed): void { if (array_diff(array_keys($data), $allowed) !== []) { JsonService::throw('请求包含不支持的字段'); } } }