Files

119 lines
5.4 KiB
PHP

<?php
declare(strict_types=1);
function portalEnabled(): bool { return getenv('PORTAL_SSO_ENABLED') === '1'; }
function portalPublic(): string
{
$url = rtrim((string)getenv('PORTAL_PUBLIC_URL'), '/');
if (!preg_match('#^https?://[a-zA-Z0-9.:-]+$#D', $url)) {
throw new RuntimeException('Portal URL unavailable');
}
return $url;
}
function portalFail(int $status, string $message): never
{
http_response_code($status);
header('Content-Type: text/html; charset=utf-8');
header('Cache-Control: no-store');
header('Referrer-Policy: no-referrer');
echo '<!doctype html><meta charset="utf-8"><title>金牛贷款档案</title><p>' . htmlspecialchars($message) . '</p>';
try { echo '<a href="' . htmlspecialchars(portalPublic(), ENT_QUOTES) . '">返回统一门户</a>'; } catch (Throwable $e) {}
exit;
}
function portalCall(string $operation, array $payload): array
{
$base = rtrim((string)getenv('PORTAL_INTERNAL_URL'), '/');
$secret = (string)getenv('PORTAL_SSO_SECRET');
if (!preg_match('#^https?://[a-zA-Z0-9.:-]+$#D', $base) || strlen($secret) < 32) {
throw new RuntimeException('SSO configuration unavailable');
}
$context = stream_context_create(['http' => [
'method' => 'POST', 'header' => "Content-Type: application/json\r\nAuthorization: Bearer " . $secret,
'content' => json_encode($payload), 'timeout' => 5, 'follow_location' => 0,
'ignore_errors' => true,
]]);
$body = @file_get_contents($base . '/api/sso/apps/loan-archive/' . $operation, false, $context, 0, 32769);
if ($body === false || strlen($body) > 32768 || !isset($http_response_header[0])
|| !preg_match('#^HTTP/\S+ 200\b#', $http_response_header[0])) {
throw new RuntimeException('Portal verification unavailable');
}
$result = json_decode($body, true, 32, JSON_THROW_ON_ERROR);
if (!is_array($result)) throw new RuntimeException('Invalid identity response');
return $result;
}
function portalValidate(): void
{
static $checked = false;
if (!portalEnabled() || $checked) return;
$handle = $_SESSION['portal_handle'] ?? '';
if (!is_string($handle) || !preg_match('/^[a-f0-9]{64}$/D', $handle)) {
portalFail(401, '请从统一门户进入贷款档案系统。');
}
try { $result = portalCall('introspect', ['session_handle' => $handle]); }
catch (Throwable $e) { portalFail(503, '暂时无法验证登录,请稍后重试。'); }
$identity = $result['identity'] ?? [];
if (empty($result['active']) || ($identity['empid'] ?? '') !== ($_SESSION['portal_empid'] ?? '')
|| ($identity['local_uid'] ?? 0) !== ($_SESSION['uid'] ?? 0)) {
$_SESSION = [];
session_regenerate_id(true);
portalFail(401, '登录或授权已失效,请重新进入统一门户。');
}
$checked = true;
}
function portalCallback(): never
{
if (!portalEnabled()) portalFail(404, '统一登录未启用。');
header('Cache-Control: no-store');
header('Referrer-Policy: no-referrer');
if ($_SERVER['REQUEST_METHOD'] !== 'GET') portalFail(405, 'Method not allowed');
if (!isset($_GET['ticket'])) {
session_regenerate_id(true);
$verifier = bin2hex(random_bytes(32));
$_SESSION['portal_verifier'] = $verifier;
$_SESSION['portal_started'] = time();
$_SESSION['portal_theme'] = ($_GET['theme'] ?? '') === 'night' ? 'night' : 'day';
header('Location: ' . portalPublic() . '/api/sso/apps/loan-archive/authorize?challenge=' . hash('sha256', $verifier), true, 303);
exit;
}
$ticket = $_GET['ticket'];
$verifier = $_SESSION['portal_verifier'] ?? '';
if (!is_string($ticket) || !preg_match('/^[a-f0-9]{64}$/D', $ticket)
|| !preg_match('/^[a-f0-9]{64}$/D', $verifier)
|| time() - (int)($_SESSION['portal_started'] ?? 0) > 120) {
portalFail(401, '登录请求已失效,请从门户重新进入。');
}
unset($_SESSION['portal_verifier'], $_SESSION['portal_started']);
try {
$result = portalCall('exchange', ['ticket' => $ticket, 'verifier' => $verifier]);
$who = $result['identity'] ?? [];
$uid = $who['local_uid'] ?? 0;
$empid = $who['empid'] ?? '';
$handle = $result['session_handle'] ?? '';
if (!is_int($uid) || $uid < 1 || !preg_match('/^[a-f0-9-]{36}$/D', $empid)
|| !preg_match('/^[a-f0-9]{64}$/D', $handle)) throw new RuntimeException('Invalid identity');
$db = db();
$stmt = $db->prepare('SELECT id FROM users WHERE id=?');
$stmt->execute([$uid]);
if (!$stmt->fetch()) throw new DomainException('Local account missing');
$stmt = $db->prepare('INSERT OR IGNORE INTO portal_identity(uid, empid) VALUES (?,?)');
$stmt->execute([$uid, $empid]);
$stmt = $db->prepare('SELECT empid FROM portal_identity WHERE uid=?');
$stmt->execute([$uid]);
if ($stmt->fetchColumn() !== $empid) throw new DomainException('Binding mismatch');
$theme = $_SESSION['portal_theme'] ?? 'day';
$_SESSION = ['uid' => $uid, 'portal_empid' => $empid, 'portal_handle' => $handle];
session_regenerate_id(true);
header('Location: /index.php?action=workbench&theme=' . $theme, true, 303);
exit;
} catch (Throwable $e) {
$_SESSION = [];
session_regenerate_id(true);
portalFail(401, '无法进入,请检查门户授权、账号绑定及子系统可用状态。');
}
}