160 lines
6.6 KiB
PHP
160 lines
6.6 KiB
PHP
<?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、隐私模式等)
|
||
// 导致每次请求创建新 Session,CSRF 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;
|
||
}
|
||
}
|