Flatboard5管理员登录后没多久就失效再次登录直接密码错误
自从重构以后
他的问题太多了 以及弃用 最不能忍的是太慢 前端动不动几十秒是常事还会预载入一些帖子但没有内容 都是些不知所云的开发 更新还一个劲的不修复只知道埋头更新
问题
/app/Middleware/AuthMiddleware.php
/app/Middleware/VisitorTrackingMiddleware.php
❌ 它会和 VisitorTrackingMiddleware 再次产生登录态分歧
❌ 它绕开了我们要统一的 AuthContext
❌ 正是“限流间隔异常”的根源之一
登录态被算了两次
用了两种不同标准
发生在两个不同时间点
还影响了限流 key / 间隔的选择
👉 这是经典的「身份判定漂移」问题
VisitorTrackingMiddleware
├─ isAuthenticatedUser() ← 用“宽松定义”
├─ 决定是否访客
├─ 生成/更新 Visitor
└─ (访客限流 key / 间隔 在这里已隐式确定)
AuthMiddleware
├─ isAuthenticated() ← 用“较严格定义”
├─ 决定是否允许访问
VisitorTrackingMiddleware 是“隐形限流前置器”
用户状态 是否进入 VisitorTracking 访客模型
isAuthenticatedUser=true ❌ 登录用户
false ✅ 匿名访客
时刻 VisitorTracking 看到 AuthMiddleware 看到
刚登录 游客 登录
刚退出 登录 游客
并发请求 登录 游客
Session 切换 随机 稳定
解决
保留 Flatboard 5 原有所有功能
✅ 保留 Translator / UrlHelper / Logger
✅ 不依赖你是否已经写好 AuthContextMiddleware
✅ 即使中间件顺序没调好,也不会白屏
【最终可用版】/app/Middleware/AuthMiddleware.php
<?php
/*
* Project name: Flatboard 5
* Project URL: https://flatboard.org
* Author: Frédéric Kaplon and contributors
* All Flatboard code is released under the GPL3 license.
*
* Flatboard 5 - Middleware d'authentification (AuthContext-based)
*/
namespace App\Middleware;
use App\Core\Request;
use App\Core\Response;
use App\Core\Session;
use App\Helpers\Translator;
use App\Helpers\UrlHelper;
class AuthMiddleware implements MiddlewareInterface
{
private const DEFAULT_ERROR_MESSAGE = 'You must be logged in to access this page';
private const DEFAULT_AJAX_ERROR = 'Unauthorized access';
// Cache des traductions
private static $translations = [];
public function handle(Request $request, Response $response): bool
{
/**
* 🛟 Sécurité :
* Si l'AuthContext n'existe pas encore (ordre des middlewares),
* on le crée ici pour éviter tout crash.
*/
if (!isset($request->auth)) {
$request->auth = (object) [
'isAuthenticated' => false,
'userId' => null
];
$userId = Session::get('user_id');
if (!empty($userId) && is_numeric($userId)) {
$request->auth->isAuthenticated = true;
$request->auth->userId = (int) $userId;
}
}
// ✅ Lecture unique de l'état d'authentification
if ($request->auth->isAuthenticated) {
return true;
}
// ❌ Non authentifié
$this->handleUnauthenticated($request, $response);
return false;
}
/**
* Gère une tentative d'accès non authentifié
*/
private function handleUnauthenticated(Request $request, Response $response): void
{
if (defined('DEBUG') && DEBUG) {
\App\Core\Logger::info('Unauthorized access attempt', [
'url' => $request->getUrl(),
'method' => $request->getMethod(),
'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown'
]);
}
if ($request->isAjax()) {
$this->handleAjaxUnauthorized($response);
} else {
$this->handleWebUnauthorized($response);
}
}
/**
* Gère une requête AJAX non autorisée
*/
private function handleAjaxUnauthorized(Response $response): void
{
$errorMessage = $this->getTranslation(
'http.403.title',
'errors',
self::DEFAULT_AJAX_ERROR
);
$response->json([
'success' => false,
'error' => $errorMessage,
'redirect' => UrlHelper::to('/login')
], 401);
}
/**
* Gère une requête web non autorisée
*/
private function handleWebUnauthorized(Response $response): void
{
$errorMessage = $this->getTranslation(
'auth.loginRequired',
'errors',
self::DEFAULT_ERROR_MESSAGE
);
Session::flash('error', $errorMessage);
$response->redirect(UrlHelper::to('/login'));
}
/**
* Récupère une traduction avec cache
*/
private function getTranslation(string $key, string $domain, string $default): string
{
$cacheKey = $domain . '.' . $key;
if (isset(self::$translations[$cacheKey])) {
return self::$translations[$cacheKey];
}
$translation = Translator::trans($key, [], $domain);
$result = !empty($translation) ? $translation : $default;
self::$translations[$cacheKey] = $result;
return $result;
}
/**
* Vide le cache des traductions (tests)
*/
public static function clearCache(): void
{
self::$translations = [];
}
}
这是【已经修复 + 整合好】的完整
/app/Middleware/VisitorTrackingMiddleware.php
isAuthenticatedUser() 只在 middleware 内部使用
✅ 不会和 AuthMiddleware 冲突
✅ 不会重复追踪
所有判断、变量、逻辑
👉 必须写在方法里(handle / private function 里)
<?php
/*
* Project name: Flatboard 5
* Project URL: https://flatboard.org
* Author: Frédéric Kaplon and contributors
*
* Flatboard 5 - Visitor tracking middleware (safe & throttled)
*/
namespace App\Middleware;
use App\Core\Request;
use App\Core\Response;
use App\Core\Session;
use App\Models\Visitor;
class VisitorTrackingMiddleware implements MiddlewareInterface
{
/**
* Paths that should never be tracked
*/
private const IGNORED_PATHS = [
'/api/',
'/favicon.ico',
'/robots.txt',
'/health',
'/ping',
'/presence/update',
];
/**
* Static file extensions to ignore
*/
private const IGNORED_EXTENSIONS = [
'.css', '.js', '.jpg', '.jpeg', '.png', '.gif',
'.svg', '.ico', '.woff', '.woff2', '.ttf', '.eot',
'.map', '.json', '.xml',
];
/**
* Prevent multiple tracking calls during the same request
*/
private static bool $tracked = false;
public function handle(Request $request, Response $response): bool
{
/**
* 1️⃣ Avoid double execution in the same request
*/
if (self::$tracked) {
return true;
}
/**
* 2️⃣ Logged-in users are handled elsewhere (presence system)
*/
if ($this->isAuthenticatedUser()) {
return true;
}
/**
* 3️⃣ Ignore useless / noisy requests
*/
if ($this->shouldIgnore($request)) {
return true;
}
/**
* 4️⃣ Track anonymous visitor
*/
$this->trackVisitor($request);
self::$tracked = true;
return true;
}
/**
* Check if a user is authenticated
*/
private function isAuthenticatedUser(): bool
{
return !empty(Session::get('user_id'));
}
/**
* Decide whether the request should be ignored
*/
private function shouldIgnore(Request $request): bool
{
$url = $request->getUrl();
// Ignore AJAX calls (except presence ping)
if ($request->isAjax() && strpos($url, '/presence/update') === false) {
return true;
}
// Ignore known paths
foreach (self::IGNORED_PATHS as $path) {
if (strpos($url, $path) === 0) {
return true;
}
}
// Ignore static files
foreach (self::IGNORED_EXTENSIONS as $ext) {
if (str_ends_with($url, $ext)) {
return true;
}
}
return false;
}
/**
* Create or update anonymous visitor entry
*/
private function trackVisitor(Request $request): void
{
try {
Visitor::updateOrCreate([
'ip_address' => $request->getIp(),
'user_agent' => $request->getUserAgent(),
'page' => $request->getUrl(),
'user_id' => null,
]);
} catch (\Throwable $e) {
// Never block the request because of tracking
\App\Core\Logger::error('Visitor tracking failed', [
'error' => $e->getMessage(),
]);
}
}
/**
* Cleanup old visitors (CRON)
*/
public static function cleanup(int $minutes = 60): int
{
try {
return Visitor::cleanup($minutes);
} catch (\Throwable $e) {
\App\Core\Logger::error('Visitor cleanup failed', [
'error' => $e->getMessage(),
]);
return 0;
}
}
/**
* Get visitor statistics
*/
public static function getStats(int $minutes = 15): ?array
{
try {
return [
'active_visitors' => count(Visitor::getActive($minutes)),
'anonymous_visitors' => count(Visitor::getActiveAnonymousVisitors($minutes)),
'bots' => count(Visitor::getActiveBotsDetailed($minutes)),
'visitors_list' => Visitor::getActiveAnonymousVisitors($minutes),
'bots_list' => Visitor::getActiveBotsDetailed($minutes),
];
} catch (\Throwable $e) {
\App\Core\Logger::error('Visitor stats failed', [
'error' => $e->getMessage(),
]);
return null;
}
}
/**
* Reset internal state (tests only)
*/
public static function reset(): void
{
self::$tracked = false;
}
}
------ 分页符-------
管理员被自己系统限流,体验直接炸 💥
👉 Flatboard 又没有用户组限流逻辑,只能靠 RateLimiter
改掉限流
'login' => ['attempts' => 1000000, 'window' => 1],
'discussion_create' => ['attempts' => 1000000, 'window' => 1],
'post_create' => ['attempts' => 1000000, 'window' => 1],
含义:
1 秒 100 万次(≈ 没有限制)
逻辑还在,但永远不会 hit
👉 不会破坏任何调用点
其他改逻辑方案
IP白名单
public function __construct()
{
$this->cache = new Cache();
$this->addToWhitelist('ip:你的真实IP');
}
ID白名单
按用户 ID(更干净,稍微改一点代码)
前提:登录后 identifier 能拿到 user id
$this->addToWhitelist('user:1'); // 管理员 ID = 1
然后在调用 check() 的地方,把:
$rateLimiter->check('post_create', $ip);
改成:
$identifier = $user->isLogged() ? 'user:' . $user->id() : 'ip:' . $ip;
$rateLimiter->check('post_create', $identifier);
其他方式
登录成功后自动解除限制
在 登录成功的地方加一行:
$rateLimiter->reset('login', $ip);
效果:
输错 5 次 → 仍然限制
一旦成功登录 → 立刻清空 login 限流
不影响安全
专业一点
'login'=> ['attempts'=> 50, 'window'=> 60], // 防爆破但不误伤
'discussion_create'=> ['attempts'=> 1000000, 'window'=> 1],
'post_create'=> ['attempts'=> 1000000, 'window'=> 1],