实现一个中间件

以下内容将以身份认证中间件为例,概述实现中间件的关键步骤和概念。

目录

  1. 理解中间件

  2. 定义中间件接口

  3. 创建认证中间件

  4. 更新路由器以支持中间件

  5. 应用中间件到路由

  6. 处理认证逻辑

  7. 总结


理解中间件

中间件(Middleware) 是一种在请求被路由到控制器方法之前或响应返回客户端之前执行的代码。它通常用于处理通用任务,如身份验证、日志记录、CORS 处理等。

在本例中,认证中间件将检查用户是否已登录,若未登录,则重定向到登录页面。


定义中间件接口

首先,定义一个中间件接口,确保所有中间件类都实现该接口。这有助于保持中间件的一致性和可扩展性。

步骤:

  1. 创建 Middleware 接口

    src/Middleware/ 目录下创建 Middleware.php 文件,并定义接口:

    <?php
    // src/Middleware/Middleware.php
    
    namespace MyFramework\Middleware;
    
    interface Middleware
    {
        /**
         * 处理请求
         *
         * @param array $params 路由参数
         * @return bool 返回 `true` 继续执行,`false` 中止执行
         */
        public function handle($params);
    }
    ?>
    

    说明:

    • handle 方法:接收路由参数,执行中间件逻辑。返回 true 表示请求可以继续执行,返回 false 则中止执行。


创建认证中间件

接下来,创建具体的认证中间件,实现上述接口。

步骤:

  1. 创建 AuthenticationMiddleware 类

    src/Middleware/ 目录下创建 AuthenticationMiddleware.php 文件:

    <?php
    // src/Middleware/AuthenticationMiddleware.php
    
    namespace MyFramework\Middleware;
    
    class AuthenticationMiddleware implements Middleware
    {
        public function handle($params)
        {
            session_start();
            if (isset($_SESSION['user'])) {
                // 已认证,允许继续
                return true;
            } else {
                // 未认证,重定向到登录页面
                header('Location: /login');
                exit();
            }
        }
    }
    ?>
    

    说明:

    • 认证逻辑:检查 $_SESSION['user'] 是否存在,判断用户是否已登录。

    • 未认证处理:若未登录,使用 header 函数重定向到 /login 路由,并使用 exit() 终止脚本执行。


更新路由器以支持中间件

为了让路由器能够识别和执行中间件,需要对现有的 Router 类进行修改。

步骤:

  1. 扩展路由定义以包含中间件

    修改 Router 类,使其能够接受中间件参数。

  2. 更新 Router.php 文件

    src/Router.php 中进行如下修改:

    <?php
    // src/Router.php
    
    namespace MyFramework;
    
    use MyFramework\Middleware\Middleware;
    
    class Router
    {
        private $routes = [];
    
        /**
         * 添加路由规则
         *
         * @param string $method HTTP 方法(GET, POST, etc.)
         * @param string $uri 请求的 URI,支持参数,例如 '/user/{id}/profile'
         * @param string $action 控制器和方法,例如 'UsersController@showProfile'
         * @param array $middlewares 中间件列表
         */
        public function add($method, $uri, $action, $middlewares = [])
        {
            // 转换 URI 模式为正则表达式,并提取参数名称
            $pattern = preg_replace_callback('/\{([a-zA-Z0-9_]+)(\?)?\}/', function ($matches) {
                $param = $matches[1];
                $optional = isset($matches[2]) && $matches[2] === '?';
                if ($optional) {
                    return '(?P<' . $param . '>[a-zA-Z0-9_-]+)?';
                } else {
                    return '(?P<' . $param . '>[a-zA-Z0-9_-]+)';
                }
            }, $uri);
    
            // 支持可选参数后的斜杠
            $pattern = preg_replace('#//+#', '/', $pattern);
            $pattern = '#^' . $pattern . '(/)?$#';
    
            // 提取参数名称
            $params = $this->extractParams($uri);
    
            $this->routes[] = [
                'method'      => strtoupper($method),
                'pattern'     => $pattern,
                'action'      => $action,
                'params'      => $params,
                'middlewares' => $middlewares
            ];
        }
    
        /**
         * 分发请求到相应的控制器方法
         *
         * @param string $requestMethod HTTP 方法
         * @param string $requestUri 请求的 URI
         */
        public function dispatch($requestMethod, $requestUri)
        {
            foreach ($this->routes as $route) {
                if ($route['method'] === strtoupper($requestMethod)) {
                    if (preg_match($route['pattern'], $requestUri, $matches)) {
                        // 提取命名参数
                        $params = [];
                        foreach ($route['params'] as $param) {
                            if (isset($matches[$param]) && $matches[$param] !== '') {
                                $params[$param] = $matches[$param];
                            }
                        }
    
                        // 执行中间件
                        foreach ($route['middlewares'] as $middlewareClass) {
                            $middleware = new $middlewareClass();
                            if ($middleware instanceof Middleware) {
                                if (!$middleware->handle($params)) {
                                    // 中间件中止请求
                                    return;
                                }
                            }
                        }
    
                        $this->executeAction($route['action'], $params);
                        return;
                    }
                }
            }
            // 如果没有匹配的路由,返回 404
            $this->sendNotFound();
        }
    
        // ... 其他方法保持不变 ...
    }
    ?>
    

    说明:

    • 路由定义扩展add 方法现在接受一个可选的 $middlewares 数组,用于指定该路由需要执行的中间件。

    • 中间件执行:在匹配路由后,遍历中间件列表,实例化并调用其 handle 方法。如果任何中间件返回 false,则中止请求处理。


应用中间件到路由

现在,指定哪些路由需要执行认证中间件。

步骤:

  1. 添加需要认证的路由

    index.php 中,定义需要认证的路由,并为其指定认证中间件。例如:

    <?php
    // index.php
    
    use MyFramework\Router;
    use MyFramework\Middleware\AuthenticationMiddleware;
    
    // ... 之前的代码 ...
    
    // 定义带有中间件的路由
    $router->add('GET', '/users', 'UsersController@list', [AuthenticationMiddleware::class]);
    $router->add('GET', '/user/{id}', 'UsersController@show', [AuthenticationMiddleware::class]);
    $router->add('GET', '/user/{id}/info/{info?}', 'UsersController@info', [AuthenticationMiddleware::class]);
    
    // 定义无需认证的登录路由
    $router->add('GET', '/login', 'AuthController@showLoginForm');
    $router->add('POST', '/login', 'AuthController@login');
    
    // 其他无需认证的路由
    $router->add('GET', '/', 'HomeController@index');
    $router->add('GET', '/about', 'HomeController@about');
    $router->add('GET', '/contact', 'HomeController@contact');
    $router->add('POST', '/submit', 'HomeController@submit');
    
    // 处理请求
    $router->dispatch($requestMethod, $requestUri);
    ?>
    

    说明:

    • 指定中间件:为需要认证的路由添加 [AuthenticationMiddleware::class],这将确保这些路由在执行控制器方法前进行身份验证。

    • 登录路由例外:登录相关的路由(如 /login)无需认证,因此不指定中间件。

  2. 创建认证控制器

    需要确保有一个 AuthController,包含 showLoginFormlogin 方法,用于显示登录表单和处理登录逻辑。

    <?php
    // src/Controllers/AuthController.php
    
    namespace MyFramework\Controllers;
    
    use MyFramework\Controller;
    use Monolog\Logger;
    use Monolog\Handler\StreamHandler;
    
    class AuthController extends Controller
    {
        private $logger;
    
        public function __construct()
        {
            $this->logger = new Logger('auth');
            $this->logger->pushHandler(new StreamHandler(__DIR__ . '/../../logs/app.log', Logger::DEBUG));
        }
    
        public function showLoginForm()
        {
            // 显示登录表单
            echo '<form method="POST" action="/login">
                    <label>用户名: <input type="text" name="username" required></label><br>
                    <label>密码: <input type="password" name="password" required></label><br>
                    <button type="submit">登录</button>
                  </form>';
        }
    
        public function login()
        {
            // 处理登录逻辑
            $username = $_POST['username'] ?? '';
            $password = $_POST['password'] ?? '';
    
            // 简单示例:假设用户名和密码均为 'admin'
            if ($username === 'admin' && $password === 'admin') {
                session_start();
                $_SESSION['user'] = $username;
                $this->logger->info("用户登录成功", ['username' => $username]);
                header('Location: /users');
                exit();
            } else {
                $this->logger->warning("用户登录失败", ['username' => $username]);
                echo "<p>登录失败,请重试。</p>";
                $this->showLoginForm();
            }
        }
    
        public function logout()
        {
            session_start();
            unset($_SESSION['user']);
            session_destroy();
            header('Location: /login');
            exit();
        }
    }
    ?>
    

    说明:

    • 登录逻辑:在 login 方法中验证用户凭证,成功后将用户信息存入 $_SESSION,并重定向到受保护的路由。

    • 登出功能:可选,提供 logout 方法以允许用户登出。


处理认证逻辑

在认证中间件和认证控制器中正确处理用户会话和认证状态。

关键点:

  1. 会话管理

    • 在中间件和控制器中使用 session_start() 来管理会话。

    • 确保在每个需要访问 $_SESSION 的地方调用 session_start()

  2. 保护受限路由

    • 使用认证中间件保护所有需要认证的路由。

    • 确保登录路由不受认证中间件保护,以防止死循环重定向。

  3. 重定向逻辑

    • 未认证用户访问受限路由时,中间件将其重定向到登录页面。

    • 登录成功后,用户被重定向到之前尝试访问的受限页面,或默认的受限页面(如 /users)。


总结

通过以上步骤,在自定义的 PHP 框架中成功添加了身份认证的中间件,实现了以下功能:

  1. 中间件接口:定义了一个通用的中间件接口,确保所有中间件的一致性。

  2. 认证中间件:实现了 AuthenticationMiddleware,用于检查用户是否已登录。

  3. 路由器增强:更新了 Router 类,使其能够识别和执行路由中指定的中间件。

  4. 路由定义更新:在路由定义中为需要认证的路由指定了认证中间件,确保这些路由在访问前进行身份验证。

  5. 认证控制器:创建了 AuthController,包含显示登录表单和处理登录逻辑的方法。

扩展建议:

  1. 中间件堆栈:支持为路由指定多个中间件,按顺序执行。

  2. 全局中间件:实现全局中间件功能,对所有路由统一应用某些中间件(如日志记录)。

  3. 中间件参数:允许为中间件传递参数,以增强中间件的灵活性。

  4. 更复杂的认证机制:集成更安全的认证机制,如密码哈希、令牌验证等。

  5. 错误处理:增强中间件中的错误处理,提供更友好的错误信息和页面。