Files
zyt/server/app/common/service/iam/IamAdminIdentityService.php
T

132 lines
5.1 KiB
PHP

<?php
declare(strict_types=1);
namespace app\common\service\iam;
use app\adminapi\logic\LoginLogic;
use app\adminapi\service\AdminTokenService;
use app\common\model\auth\Admin;
use app\common\model\auth\AdminRole;
use app\common\service\FileService;
use RuntimeException;
use think\facade\Config;
use think\facade\Db;
class IamAdminIdentityService
{
private array $config;
private ?IamHubClient $client;
public function __construct(?array $config = null, ?IamHubClient $client = null)
{
$this->config = $config ?? (array) config('iam_hub');
$this->client = $client;
}
/** @return array<string,mixed> */
public function login(array $claims, int $terminal): array
{
$subject = trim((string) ($claims['sub'] ?? ''));
if ($subject === '') {
throw new RuntimeException('IAM subject is missing');
}
$client = $this->client ?? new IamHubClient($this->config);
$context = $client->provisioningContext($subject);
$employee = $context['employee'] ?? null;
if (!is_array($employee) || !hash_equals($subject, (string) ($employee['oidcSubject'] ?? ''))) {
throw new RuntimeException('IAM provisioning context does not match the login identity');
}
Db::startTrans();
try {
$admin = Admin::where('iam_subject', '=', $subject)->find();
$externalId = trim((string) ($context['externalAccountId'] ?? ''));
if (!$admin && ctype_digit($externalId) && (int) $externalId > 0) {
$admin = Admin::find((int) $externalId);
if ($admin && trim((string) ($admin->iam_subject ?? '')) !== ''
&& !hash_equals($subject, (string) $admin->iam_subject)) {
throw new RuntimeException('本地管理员账号已绑定其他统一身份');
}
}
if (!$admin) {
$account = self::stableAccount($subject);
$collision = Admin::where('account', '=', $account)->find();
if ($collision) {
throw new RuntimeException('统一身份账号映射冲突');
}
$admin = Admin::create([
'account' => $account,
'name' => (string) ($employee['displayName'] ?? $claims['name'] ?? $claims['preferred_username'] ?? $account),
'password' => '',
'avatar' => Config::get('project.default_image.admin_avatar'),
'root' => 0,
'disable' => 0,
'multipoint_login' => 1,
'is_paw' => 1,
'iam_subject' => $subject,
'iam_managed' => 1,
'iam_revoked_at' => 0,
]);
foreach ($this->defaultRoleIds() as $roleId) {
AdminRole::create(['admin_id' => $admin->id, 'role_id' => $roleId]);
}
} else {
if ((int) ($admin->disable ?? 0) === 1 && (int) ($admin->iam_revoked_at ?? 0) === 0) {
throw new RuntimeException('本地管理员账号已被平台禁用');
}
$admin->iam_subject = $subject;
$admin->iam_managed = 1;
if ((int) ($admin->iam_revoked_at ?? 0) > 0) {
$admin->disable = 0;
$admin->iam_revoked_at = 0;
}
$admin->save();
}
$admin->login_time = time();
$admin->login_ip = request()->ip();
$admin->save();
Db::commit();
} catch (\Throwable $error) {
Db::rollback();
throw $error;
}
$client->bindAccount($subject, (int) $admin->id);
$adminInfo = AdminTokenService::setToken($admin->id, $terminal, $admin->multipoint_login);
if (!is_array($adminInfo) || empty($adminInfo['token'])) {
throw new RuntimeException('本地管理员会话创建失败');
}
$avatar = $admin->avatar ?: Config::get('project.default_image.admin_avatar');
return [
'name' => $adminInfo['name'],
'avatar' => FileService::getFileUrl($avatar),
'role_name' => $adminInfo['role_name'],
'token' => $adminInfo['token'],
'is_paw' => 1,
'need_bind_work_wechat' => LoginLogic::adminMustBindWorkWechat([
'root' => (int) ($admin->root ?? 0),
'work_wechat_userid' => (string) ($admin->work_wechat_userid ?? ''),
]),
];
}
public static function stableAccount(string $subject): string
{
return 'iam_' . substr(hash('sha256', $subject), 0, 20);
}
/** @return int[] */
private function defaultRoleIds(): array
{
$values = explode(',', (string) ($this->config['default_role_ids'] ?? ''));
$roles = array_values(array_unique(array_filter(
array_map('intval', $values),
static fn (int $value): bool => $value > 0
)));
sort($roles, SORT_NUMERIC);
return $roles;
}
}