Flatboard5管理员登录后没多久就失效再次登录直接密码错误

Avatar
Posts 142

问题
/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

TEXT


<?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 里)

TEXT


<?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;
    }
}

Edited on  Jul 04, 2026  By  sytbbt .

Avatar
Posts 142

管理员被自己系统限流,体验直接炸 💥
👉 Flatboard 又没有用户组限流逻辑,只能靠 RateLimiter

改掉限流

TEXT

'login' => ['attempts' => 1000000, 'window' => 1],
'discussion_create' => ['attempts' => 1000000, 'window' => 1],
'post_create'       => ['attempts' => 1000000, 'window' => 1],

含义:
1 秒 100 万次(≈ 没有限制)
逻辑还在,但永远不会 hit
👉 不会破坏任何调用点

其他改逻辑方案
IP白名单

TEXT

public function __construct()
{
    $this->cache = new Cache();

    $this->addToWhitelist('ip:你的真实IP');
}

ID白名单
按用户 ID(更干净,稍微改一点代码)

前提:登录后 identifier 能拿到 user id

TEXT

$this->addToWhitelist('user:1'); // 管理员 ID = 1

然后在调用 check() 的地方,把:

$rateLimiter->check('post_create', $ip);

改成:

TEXT

$identifier = $user->isLogged() ? 'user:' . $user->id() : 'ip:' . $ip;
$rateLimiter->check('post_create', $identifier);

其他方式
登录成功后自动解除限制
在 登录成功的地方加一行:

$rateLimiter->reset('login', $ip);
效果:

输错 5 次 → 仍然限制

一旦成功登录 → 立刻清空 login 限流

不影响安全

专业一点

TEXT

'login'=> ['attempts'=> 50, 'window'=> 60], // 防爆破但不误伤
'discussion_create'=> ['attempts'=> 1000000, 'window'=> 1],
'post_create'=> ['attempts'=> 1000000, 'window'=> 1],

Edited on  May 13, 2026  By  sytbbt .

Avatar
Posts 142

后续跟踪
这个问题依然没有解决 也许是主机环境问题阻止了文件执行和生成
所以为了跟踪这个问题 我发了几个帖子后 直接在后台清空所有缓存 多次
然后直接数据库备份和全站备份
之后退出登录再次登录 - 成功
再次清空缓存

目前等到几小时后再回来看

等到下一步状态 有可能直接挂了 无法登录 即使账号是正确的也不行

Avatar
Posts 142

升级为5.51后 在后台已经看不到清缓存界面了,而且前端还限制管理员 发帖发不出 需要删除

/up/stockage/cache/*

Avatar
Posts 142

间隔7小时候的结果
1.登录已经失效
2.再次登录 没有失败

总结:大概率是安装之后需要清除所有缓存 但不应该只是此单一原因 应该还有其他原因 比如上面的限流和会话等问题导致缓存错乱或丢失
后续还会进行用户注册跟踪 看看普通用户是不是一样在登录后再次登录会失败或直接不存在了

问题列表
/bbs/users 列出的用户依然显示为空
首先我注册一个普通用户试试,然后将其邮件验证状态为ture 但不清理任何缓存试试
usernameisuser/usernameisuser@msn.com

结论是成立,用户列表已经不报错

sytbbt 登录
去普通用户也登录
看来刚安装后的缓存管理是关键

两个相同的程序 不同子目录 还是会直接覆盖 cookie 无法登录

Log in to reply
Navigation
5 Posts
post #1
13 May 2026
By Utilisateur