Files
MES/app/controllers/LoginController.php
T
2026-08-08 18:28:49 +08:00

160 lines
6.6 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
namespace app\controllers;
use core\base\Controller;
use app\models\Employee;
class LoginController extends Controller
{
// 暴力破解防护配置
const MAX_LOGIN_ATTEMPTS = 5; // 最大尝试次数
const LOCKOUT_DURATION = 900; // 锁定时间(秒),15分钟
const ATTEMPT_WINDOW = 300; // 计数窗口(秒),5分钟
public function index()
{
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// 登录页不强制 CSRF 验证,原因:
// 1. 部分设备/浏览器 Session Cookie 不稳定(SameSite=Lax、隐私模式等)
// 导致每次请求创建新 SessionCSRF Token 永远对不上
// 2. 登录本身无敏感副作用(不修改数据),攻击者 CSRF 登录成功
// 也无法获知密码,且暴力破解防护仍在生效
// 3. 登录成功后的所有操作仍受 CSRF 保护
$emp_no = isset($_POST['emp_no']) ? trim($_POST['emp_no']) : '';
$password = isset($_POST['password']) ? $_POST['password'] : '';
if (empty($emp_no) || empty($password)) {
$ua = isset($_SERVER['HTTP_USER_AGENT']) ? substr($_SERVER['HTTP_USER_AGENT'], 0, 150) : 'unknown';
$sess = session_id() ? hash('sha256', session_id()) : 'none';
error_log("[LOGIN_DIAG] POST_EMPTY emp_no_len=" . strlen($emp_no) . " pwd_len=" . strlen($password) . " ua={$ua} sess_hash={$sess}");
$error = '请输入员工编号和密码';
} else {
// 暴力破解检查
$lockError = $this->checkBruteForce($emp_no);
if ($lockError) {
$error = $lockError;
} else {
$employee = new Employee();
$user = $employee->validateLogin($emp_no, $password);
if ($user) {
// 登录成功 — 清除失败记录
$this->clearLoginAttempts($emp_no);
// 重新生成 Session ID 防止会话固定攻击
session_regenerate_id(true);
$_SESSION['user_id'] = $user['id'];
$_SESSION['emp_no'] = $user['emp_no'];
$_SESSION['emp_name'] = $user['emp_name'];
$_SESSION['role'] = $user['role'];
$_SESSION['category'] = $user['category'] ?? self::CATEGORY_MES;
// 根据角色和类目跳转到不同页面
if ($user['role'] === self::ROLE_SUPER_ADMIN) {
// 超级管理员:默认进入 MES 后台,可通过顶部切换类目
header('Location: ' . BASE_URL . '/Admin/index');
} elseif ($user['role'] === self::ROLE_ADMIN) {
// 管理员:根据分配的类目跳转到对应管理界面
$cat = $user['category'] ?? self::CATEGORY_MES;
$homeRoute = self::CATEGORY_HOME[$cat] ?? 'Admin/index';
header('Location: ' . BASE_URL . '/' . $homeRoute);
} else {
// 操作员:进入前台工位界面
header('Location: ' . BASE_URL . '/Front/index');
}
exit;
} else {
// 记录失败
$this->recordLoginAttempt($emp_no);
$remaining = $this->getRemainingAttempts($emp_no);
// 诊断日志:validateLogin 返回 false(详细日志已在 Employee 中记录)
$ua = isset($_SERVER['HTTP_USER_AGENT']) ? substr($_SERVER['HTTP_USER_AGENT'], 0, 150) : 'unknown';
$sess = session_id() ? hash('sha256', session_id()) : 'none';
error_log("[LOGIN_DIAG] RESULT=FAIL emp_no_len=" . strlen($emp_no) . " remaining={$remaining} ua={$ua} sess_hash={$sess}");
$error = '员工编号或密码错误' . ($remaining > 0 ? "(还可尝试 {$remaining} 次)" : '(账号已临时锁定)');
}
}
}
}
$this->assign('title', 'MES 系统登录');
$this->assign('error', $error);
$this->assign('csrf_token', $this->csrfToken());
$this->render();
}
/**
* 检查暴力破解状态
*/
private function checkBruteForce($emp_no)
{
$key = 'login_attempts_' . md5($emp_no);
$attempts = $_SESSION[$key] ?? [];
// 清理过期记录
$now = time();
$attempts = array_filter($attempts, function($t) use ($now) {
return $t > ($now - self::ATTEMPT_WINDOW);
});
if (count($attempts) >= self::MAX_LOGIN_ATTEMPTS) {
$lastAttempt = max($attempts);
$waitTime = self::LOCKOUT_DURATION - ($now - $lastAttempt);
if ($waitTime > 0) {
$minutes = ceil($waitTime / 60);
return "登录尝试次数过多,请 {$minutes} 分钟后再试";
}
// 锁定时间已过,清除记录
unset($_SESSION[$key]);
return null;
}
$_SESSION[$key] = array_values($attempts);
return null;
}
/**
* 记录失败登录尝试
*/
private function recordLoginAttempt($emp_no)
{
$key = 'login_attempts_' . md5($emp_no);
$attempts = $_SESSION[$key] ?? [];
$now = time();
$attempts = array_filter($attempts, function($t) use ($now) {
return $t > ($now - self::ATTEMPT_WINDOW);
});
$attempts[] = $now;
$_SESSION[$key] = array_values($attempts);
}
/**
* 清除登录尝试记录
*/
private function clearLoginAttempts($emp_no)
{
$key = 'login_attempts_' . md5($emp_no);
unset($_SESSION[$key]);
}
/**
* 获取剩余尝试次数
*/
private function getRemainingAttempts($emp_no)
{
$key = 'login_attempts_' . md5($emp_no);
$attempts = $_SESSION[$key] ?? [];
return max(0, self::MAX_LOGIN_ATTEMPTS - count($attempts));
}
public function logout()
{
session_destroy();
header('Location: ' . BASE_URL . '/');
exit;
}
}