v1.0.51: 批次1架构筑基 - 1)版本迁移(common/migrate.php+schema_versions+natsort迁移文件);2)登录安全(bcrypt平滑迁移login/user_add/user_update旧sha1命中自动重写+system_login_attempts限流+session加固httponly/samesite/secure);3)CSRF Token(服务端checkCsrf+auth/csrf.php登录页取token+前端common.js/login.js统一携带X-CSRF-Token);4)统一基座common/Api.php并存量强制迁移全部70个endpoint(Api::boot按public/super/module/permissions分流,写接口强制checkAjax+checkCsrf,全局异常处理);5)前端收敛(common.js新增esc转义别名);6)REV-4硬数据来源渠道可编辑(hard_update+补全弹窗下拉);7)prepared卫生(soft_update.php修复+analysis.php表名白名单REV-9)

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
qianqiuwanzi
2026-09-17 14:48:17 +08:00
parent 69fd4246c2
commit c86f08c325
91 changed files with 1072 additions and 463 deletions
+32 -13
View File
@@ -3,18 +3,21 @@
* 登录接口 POST /api/auth/login.php
* 入参:username / password / slider_token(滑块验证通过后的token)
* 出参(成功):{ code:0, msg:'登录成功', data:{ real_name, role_name, permissions } }
*
* 批次1 安全加固(REV-1/REV-8):
* - 密码 bcrypt(password_hash)优先校验;存量 sha1 用户登录命中后平滑升级
* - 登录限流(15分钟内同 IP+账号失败≥5 次锁定)
* - 登录成功后 session_regenerate_id 防会话固定
* - 经 Api::boot 统一走 CSRF + 安全 Session
*/
require_once __DIR__ . '/../common/db.php';
require_once __DIR__ . '/../common/response.php';
require_once __DIR__ . '/../common/logger.php';
require_once __DIR__ . '/../common/Api.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
Response::error('请求方式错误', 400);
}
$pdo = Api::boot(['public' => true]);
$username = trim($_POST['username'] ?? '');
$password = (string)($_POST['password'] ?? '');
$sliderToken = trim($_POST['slider_token'] ?? '');
$ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
// 滑块验证:前端 slider.js 拖动通过后生成的 token,服务端校验非空且格式合法
if ($sliderToken === '' || strlen($sliderToken) < 10) {
@@ -24,7 +27,9 @@ if ($username === '' || $password === '') {
Response::error('请输入账号和密码');
}
$pdo = DB::getInstance()->getPdo();
// 登录限流(fail-open)
checkLoginThrottle($username, $ip);
$stmt = $pdo->prepare(
"SELECT u.*, r.role_name, r.permissions
FROM system_users u
@@ -34,22 +39,35 @@ $stmt = $pdo->prepare(
$stmt->execute([$username]);
$user = $stmt->fetch();
if (!$user || !hash_equals($user['password'], sha1($password))) {
// 密码校验:bcrypt 优先;旧版无盐 sha1 校验通过后即时平滑升级为 bcrypt
$authOk = false;
if ($user) {
if (password_verify($password, (string)$user['password'])) {
$authOk = true;
} elseif (hash_equals((string)$user['password'], sha1($password))) {
$authOk = true;
$pdo->prepare("UPDATE system_users SET password = ? WHERE id = ?")
->execute([password_hash($password, PASSWORD_DEFAULT), (int)$user['id']]);
}
}
if (!$authOk) {
recordLoginAttempt($username, $ip, false);
Response::error('账号或密码错误');
}
if ((int)$user['is_active'] !== 1) {
recordLoginAttempt($username, $ip, false);
Response::error('账号已被禁用,请联系管理员');
}
recordLoginAttempt($username, $ip, true);
// 更新最后登录时间/IP
$ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
$upd = $pdo->prepare("UPDATE system_users SET last_login_time = NOW(), last_login_ip = ? WHERE id = ?");
$upd->execute([$ip, $user['id']]);
// 写入 Session
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
// 会话固定防护:登录成功后重新生成 session id(保留已有 CSRF Token)
session_regenerate_id(true);
$_SESSION['user_id'] = (int)$user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['real_name'] = $user['real_name'] ?? $user['username'];
@@ -64,4 +82,5 @@ Response::success([
'real_name' => $_SESSION['real_name'],
'role_name' => $_SESSION['role_name'],
'permissions' => $_SESSION['permissions'],
'csrf_token' => csrfToken(),
], '登录成功');