feat(auth): 统一身份 - 增加可选IAM快捷登录并保留原业务权限

This commit is contained in:
2026-09-10 10:33:03 +08:00
parent 27fbef9321
commit aa0d22bbe2
518 changed files with 5919 additions and 13 deletions
@@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace app\adminapi\controller;
use app\adminapi\service\iam\IamLoginService;
use think\facade\Log;
/** Dedicated optional endpoints: existing account and WeCom actions are untouched. */
class IamController extends BaseAdminController
{
public array $notNeedLogin = ['config', 'start', 'callback', 'exchange'];
private const COOKIE = 'ZYT_IAM_BROWSER';
public function config()
{
return $this->data(IamLoginService::settings())->header(['Cache-Control' => 'no-store']);
}
public function start()
{
try {
$service = new IamLoginService();
$browser = $this->browser();
if ($browser === '') {
$browser = bin2hex(random_bytes(32));
}
setcookie(self::COOKIE, $browser, ['expires' => time() + 600, 'path' => '/adminapi/iam', 'secure' => true, 'httponly' => true, 'samesite' => 'Lax']);
return redirect($service->start($browser, $this->request->ip()))->header($this->privateHeaders());
} catch (\Throwable $error) {
return $this->back(['iam_error' => $this->message($error)]);
}
}
public function callback()
{
try {
if ($this->browser() === '' || $this->request->get('error', '') !== '') {
throw new \RuntimeException('授权已取消或浏览器状态过期,请重新登录');
}
$ticket = (new IamLoginService())->callback($this->browser(), (string) $this->request->get('state', ''), (string) $this->request->get('code', ''));
return $this->back(['iam_ticket' => $ticket]);
} catch (\Throwable $error) {
return $this->back(['iam_error' => $this->message($error)]);
}
}
public function exchange()
{
if (!$this->request->isPost()) {
return $this->fail('请使用 POST 兑换登录状态')->code(405);
}
try {
if ($this->browser() === '') {
throw new \RuntimeException('浏览器登录状态已过期,请重新登录');
}
$payload = (new IamLoginService())->exchange($this->browser(), (string) $this->request->post('ticket', ''), (string) $this->request->header('origin', ''));
return $this->data($payload)->header($this->privateHeaders());
} catch (\Throwable $error) {
return $this->fail($this->message($error))->header($this->privateHeaders());
}
}
private function browser(): string
{
$value = (string) ($_COOKIE[self::COOKIE] ?? '');
return preg_match('/^[a-f0-9]{64}$/D', $value) ? $value : '';
}
private function back(array $query)
{
// Relative, fixed path: never derive the return origin from request headers or query strings.
return redirect('/admin/login?' . http_build_query($query, '', '&', PHP_QUERY_RFC3986))->header($this->privateHeaders());
}
private function privateHeaders(): array
{
return ['Cache-Control' => 'no-store', 'Referrer-Policy' => 'no-referrer'];
}
private function message(\Throwable $error): string
{
Log::warning('IAM login rejected: ' . get_class($error));
$message = $error->getMessage();
return preg_match('/^[\x{4e00}-\x{9fff}]/u', $message) ? $message : '统一账号登录失败,请重试或使用原账号登录';
}
}