根因:ny_save_config 只写不校验,Db::settingsSet 内部吞异常且返回 void。 data/ 目录不可写时登录页照常显示登录成功,但下一请求读到的账号仍是空的 → 抛未配置卷王登录账号 → 前端 auth 打标 → 跳登录页 → 无限循环。 修复: - Db::settingsSet 改返回 bool 并回读校验;新增 settingsDelete - ny_save_config 双写后均做回读校验,失败写入 ny_config_save_error() - 新增 ny_config_diag()/ny_session_usable(),暴露来源/可写性/是否已存账号 - ny_load_config 把 null 账号强制成空字符串(脏数据不再伪装成未配置) - login.php 存凭据后回读确认,存不住/会话不可写直接报错而不是假成功 - login.php 账号为空时页面直接给出 data 目录写权限提示 - config_save 合并逻辑抽成 ny_apply_config_input:账号/密码不许被改空 - 鉴权失败响应带 extra.diag,前端 401 也算 auth - 前端防死循环:刚登录 120s 内或已跳 2 次则停止自动跳转,弹窗给出真实原因+诊断 测试:新增 tests/test-config-persist.php(29) 与 tools/test-login-loop.js(19); run-all.sh 纳入两者。全套 148+48=196 项通过。
145 lines
6.8 KiB
PHP
145 lines
6.8 KiB
PHP
<?php
|
||
/**
|
||
* 独立登录页:连接信息(系统地址/接口前缀/账号)已在 data/config.json 中预置,
|
||
* 本页只需输入密码(账号可改)完成卷王登录,成功后进入主系统。
|
||
* 连接信息有变请在【设置】中修改。
|
||
*/
|
||
declare(strict_types=1);
|
||
require __DIR__ . '/bootstrap.php';
|
||
|
||
// 先加载配置:下面已登录分支要用 $cfg 构造 SurveyKingClient 探活,必须在前面就准备好
|
||
$cfg = ny_load_config();
|
||
|
||
$notice = ''; // 非阻断提示(例如"上次会话已失效,请重新登录")
|
||
|
||
if (ny_is_auth()) {
|
||
// 已登录但卷王 token 可能已过期:用当前 token 探一次卷王,能通即视为有效,否则清掉本地会话强制重新登录
|
||
try {
|
||
$client = new SurveyKingClient($cfg);
|
||
$client->ping();
|
||
} catch (Throwable $e) {
|
||
// 卷王 token 已失效或配置错误 → 清会话 + 强制显示登录页(避免 login.php 把人踢回 index.php 形成死循环)
|
||
// 把失败原因带出来,避免用户只看到登录框却不知道为什么被踢回来
|
||
$notice = '已退出之前的登录(' . $e->getMessage() . ')';
|
||
$_SESSION = [];
|
||
@session_destroy();
|
||
}
|
||
if (ny_is_auth()) {
|
||
header('Location: index.php');
|
||
exit;
|
||
}
|
||
}
|
||
|
||
$err = '';
|
||
$ok = false;
|
||
|
||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||
$user = trim((string)($_POST['username'] ?? $cfg['username']));
|
||
$pwd = (string)($_POST['password'] ?? '');
|
||
// 允许用户在本页调整账号;密码必须输入
|
||
if ($user === '' || $pwd === '') {
|
||
$err = '请输入账号与密码。';
|
||
} else {
|
||
// 用当前配置的连接信息 + 本页输入的账密做一次卷王登录
|
||
$test = $cfg;
|
||
$test['username'] = $user;
|
||
$test['password'] = $pwd;
|
||
try {
|
||
$client = new SurveyKingClient($test);
|
||
$client->getToken(true);
|
||
|
||
// 登录成功:把账号与密码写回配置(本地单用户工具,持久化密码以便 token
|
||
// 失效时平台能自动重新登录卷王,避免后台刷新陷入"未配置账号"的死状态)
|
||
$cfg['username'] = $user;
|
||
$cfg['password'] = $pwd;
|
||
$saved = ny_save_config($cfg);
|
||
|
||
// 关键:必须回读确认真的存住了。以前这里不看返回值,data 目录不可写时
|
||
// 会显示"登录成功",但下一个请求读到的账号仍是空的 → 无限跳回登录页。
|
||
$back = ny_load_config();
|
||
$persistOk = $saved
|
||
&& trim((string)($back['username'] ?? '')) === $user
|
||
&& (string)($back['password'] ?? '') === $pwd;
|
||
|
||
if (!$persistOk) {
|
||
$d = ny_config_diag();
|
||
$err = "登录卷王成功,但凭据没能保存到本机配置,继续进入会立刻又提示“未配置账号”。\n"
|
||
. "请检查 data 目录写权限后重试:\n"
|
||
. "· 配置来源:" . $d['storage'] . "(可写=" . ($d['cfg_writable'] ? '是' : '否') . ")\n"
|
||
. "· 数据目录:" . $d['data_dir'] . "(可写=" . ($d['dir_writable'] ? '是' : '否') . ")\n"
|
||
. "· 数据库:" . ($d['db_connected'] ? '已连接(可写=' . ($d['db_writable'] ? '是' : '否') . ')' : '未连接 ' . $d['db_error']) . "\n"
|
||
. "· 保存错误:" . ($d['save_error'] !== '' ? $d['save_error'] : '(无)');
|
||
} elseif (!ny_session_usable($sessWhy)) {
|
||
// 会话存不住时同样会"登录成功→立刻被踢回登录页",必须在这一步拦住
|
||
$err = "凭据已保存,但 PHP 会话不可用,登录状态无法保持:\n" . $sessWhy
|
||
. "\n请在 php.ini 中把 session.save_path 指向一个可写目录后重试。";
|
||
} else {
|
||
ny_login();
|
||
$ok = true;
|
||
}
|
||
} catch (Throwable $e) {
|
||
$err = '登录失败:' . $e->getMessage();
|
||
}
|
||
}
|
||
}
|
||
?>
|
||
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>登录 · <?= htmlspecialchars(NY_APP_NAME) ?></title>
|
||
<link rel="stylesheet" href="assets/style.css">
|
||
</head>
|
||
<body>
|
||
<div class="login-gate<?= $ok ? ' hidden' : '' ?>">
|
||
<div class="login-box">
|
||
<div class="login-brand">
|
||
<span class="logo">NY</span>
|
||
<div class="brand-text">
|
||
<strong><?= htmlspecialchars(NY_APP_NAME) ?></strong>
|
||
<small>实时数据统计</small>
|
||
</div>
|
||
</div>
|
||
<p class="login-tip">系统连接信息已配置(<?= htmlspecialchars(parse_url($cfg['base_url'], PHP_URL_HOST)) ?>/<?= htmlspecialchars(trim($cfg['api_prefix'], '/')) ?>)。请输入账号密码完成登录。</p>
|
||
<?php if (trim((string)$cfg['username']) === ''): ?>
|
||
<div class="alert" style="white-space:pre-wrap">⚠️ 本机尚未保存卷王账号(配置来源:<?= htmlspecialchars(ny_config_source()) ?>)。
|
||
如果每次登录成功都回到本页,多半是 data 目录不可写导致账号没存住——
|
||
请让 data/ 目录(<?= htmlspecialchars(NY_DATA) ?>)对 PHP 进程可写后重试。</div>
|
||
<?php endif; ?>
|
||
|
||
<?php if ($err !== ''): ?>
|
||
<div class="alert" style="white-space:pre-wrap"><?= htmlspecialchars($err) ?></div>
|
||
<?php elseif ($notice !== ''): ?>
|
||
<div class="alert" style="white-space:pre-wrap"><?= htmlspecialchars($notice) ?></div>
|
||
<?php endif; ?>
|
||
|
||
<form method="post">
|
||
<div class="fi"><label>账号</label><input type="text" name="username" value="<?= htmlspecialchars($cfg['username']) ?>" autocomplete="username"></div>
|
||
<div class="fi"><label>密码</label><input type="password" name="password" placeholder="请输入密码" autocomplete="current-password" autofocus></div>
|
||
<div class="login-actions">
|
||
<button class="btn btn-primary" type="submit">登录</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<?php if ($ok): ?>
|
||
<div class="login-gate">
|
||
<div class="login-box" style="text-align:center">
|
||
<div class="login-brand" style="justify-content:center"><span class="logo">NY</span>
|
||
<div class="brand-text"><strong>登录成功</strong><small>正在进入数据平台…</small></div>
|
||
</div>
|
||
<script>
|
||
// 记录"本次登录成功"的时间戳:前端据此判断"刚登录完又报鉴权失败",
|
||
// 这种情况不再自动跳回登录页(否则会形成死循环),而是把真实错误显示出来。
|
||
try { sessionStorage.setItem('ny_login_at', String(Date.now())); sessionStorage.removeItem('ny_auth_jumps'); } catch (e) {}
|
||
setTimeout(function(){ location.href='index.php'; }, 600);
|
||
</script>
|
||
</div>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
</body>
|
||
</html>
|