Files
ny-exam-platform/login.php
T
nxxy335 f661862d46 fix(auth): 修复登录后反复报未配置账号并跳回登录页的死循环
根因: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 项通过。
2026-09-14 08:44:09 +08:00

145 lines
6.8 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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>