124 lines
5.3 KiB
PHP
124 lines
5.3 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace app\adminapi\service\iam;
|
|
|
|
use app\adminapi\logic\LoginLogic;
|
|
use app\common\enum\AdminTerminalEnum;
|
|
use app\common\model\auth\Admin;
|
|
use RuntimeException;
|
|
use think\facade\Config;
|
|
|
|
final class IamLoginService
|
|
{
|
|
private array $config;
|
|
private IamOidcClient $client;
|
|
private IamLoginTransactionStore $transactions;
|
|
|
|
public static function settings(): array
|
|
{
|
|
$config = (array) Config::get('iam', []);
|
|
$enabled = in_array(strtolower((string) ($config['enabled'] ?? '')), ['1', 'true', 'yes', 'on'], true);
|
|
if (!$enabled) {
|
|
return ['enabled' => 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;
|
|
}
|
|
}
|