false, 'loginUrl' => '']; } try { new IamOidcClient($config); $origin = self::publicOrigin($config); if (($config['redirect_uri'] ?? '') !== $origin . '/adminapi/iam/callback') { return ['enabled' => false, 'loginUrl' => '']; } return ['enabled' => true, 'loginUrl' => $origin . '/adminapi/iam/start']; } catch (\Throwable $error) { return ['enabled' => false, 'loginUrl' => '']; } } public function __construct() { if (!self::settings()['enabled']) { throw new RuntimeException('统一账号快捷登录尚未启用,请使用原账号登录'); } $this->config = (array) Config::get('iam'); $this->client = new IamOidcClient($this->config); $this->transactions = new IamLoginTransactionStore(app()->getRuntimePath() . 'iam-login'); } public function start(string $browser, string $ip): string { if (!$this->transactions->allowStart($ip)) { throw new RuntimeException('快捷登录请求过于频繁,请稍后重试'); } $state = bin2hex(random_bytes(32)); $nonce = bin2hex(random_bytes(32)); $verifier = bin2hex(random_bytes(32)); $target = $this->client->authorizationUrl($state, $nonce, $verifier); $this->transactions->put('state', $state, $browser, ['nonce' => $nonce, 'verifier' => $verifier], 600); return $target; } public function callback(string $browser, string $state, string $code): string { $transaction = $this->transactions->consume('state', $state, $browser); if ($code === '' || strlen($code) > 4096) { throw new RuntimeException('快捷登录回调无效,请重新登录'); } $claims = $this->client->exchange($code, $transaction['verifier'], $transaction['nonce']); $this->verifiedAdmin($claims['sub']); $ticket = bin2hex(random_bytes(32)); $this->transactions->put('ticket', $ticket, $browser, ['subject' => $claims['sub']], 60); return $ticket; } public function exchange(string $browser, string $ticket, string $origin): array { if ($origin !== '' && $origin !== self::publicOrigin($this->config)) { throw new RuntimeException('快捷登录来源无效,请重新登录'); } $transaction = $this->transactions->consume('ticket', $ticket, $browser); // Recheck entitlement, binding and local status immediately before issuing the existing business token. $admin = $this->verifiedAdmin($transaction['subject']); return (array) (new LoginLogic())->login(['account' => $admin->account, 'terminal' => AdminTerminalEnum::PC]); } public static function boundAdminId(array $context, string $subject, string $applicationId): int { if (($context['applicationId'] ?? '') !== $applicationId || ($context['localIdentityKey'] ?? '') !== $subject || ($context['employee']['oidcSubject'] ?? '') !== $subject || ($context['employee']['status'] ?? '') !== 'active') { throw new RuntimeException('当前统一账号未获得甄养堂访问权限'); } $rawId = $context['externalAccountId'] ?? ''; if (!is_string($rawId) && !is_int($rawId)) { throw new RuntimeException('统一账号绑定格式无效,请联系管理员'); } $id = (string) $rawId; if (!preg_match('/^[1-9][0-9]{0,9}$/D', $id) || ($context['shouldCreateLocalAccount'] ?? true) !== false) { throw new RuntimeException('统一账号尚未开通甄养堂账号,请联系管理员'); } return (int) $id; } private function verifiedAdmin(string $subject): Admin { $id = (new IamAccountProvisioner())->resolve($this->client, $this->config, $subject); $admin = Admin::findOrEmpty($id); if ($admin->isEmpty() || (int) $admin->disable !== 0 || !empty($admin->getData('delete_time'))) { throw new RuntimeException('甄养堂账号已停用,请联系管理员'); } return $admin; } private static function publicOrigin(array $config): string { $value = rtrim((string) ($config['public_url'] ?? ''), '/'); $parts = parse_url($value); if (!is_array($parts) || ($parts['scheme'] ?? '') !== 'https' || empty($parts['host']) || isset($parts['user']) || isset($parts['pass']) || isset($parts['query']) || isset($parts['fragment']) || !empty($parts['path']) || preg_match('/[\x00-\x20\x7f\\\\]/', $value)) { throw new RuntimeException('IAM public origin must be fixed HTTPS'); } return $value; } }