初始化
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user