目录

  1. 理解中间件与认证中间件

  2. 常见的认证方式

  3. 实现 认证中间件

  4. 生成和管理 Bearer Token

  5. 集成中间件到框架中

  6. 安全性考虑

  7. 总结


理解中间件与认证中间件

中间件(Middleware)

中间件 是一种在 HTTP 请求到达最终处理程序(如控制器)之前对其进行处理的机制。它们通常用于执行任务,如日志记录、认证、授权、CORS 处理等。

认证中间件

认证中间件 专门用于验证请求的来源和身份。例如,确保只有经过认证的用户才能访问某些受保护的路由。


常见的认证方式

在实现认证中间件时,选择合适的认证方式至关重要。以下是几种常见的认证方式:

Bearer Token 认证

Bearer Token 是一种基于令牌的认证机制,常用于 API 认证。客户端在每次请求中携带一个访问令牌,服务器验证该令牌以确认用户身份。

Basic Authentication

Basic Authentication 使用用户名和密码进行认证。客户端在请求头中发送编码后的凭证。

API Key 认证

API Key 是一种简单的认证方式,客户端在请求中包含一个预先分配的密钥,服务器验证该密钥以确认请求合法性。

Session-Based 认证

Session-Based 认证 使用服务器端会话来跟踪用户登录状态。客户端通过 Cookie 发送会话标识符,服务器验证会话以确认用户身份。


实现 认证中间件

下面将详细介绍如何实现一个支持 Bearer Token 认证的 AuthenticationMiddleware。为了增强安全性和功能性,我们还将介绍如何使用 JWT(JSON Web Token)

Bearer Token 简单认证示例

假设希望使用 Bearer Token 来保护 API 路由,确保只有携带有效令牌的请求才能访问受保护的资源。

步骤:

  1. 创建 AuthenticationMiddleware 类

  2. 实现 Token 验证逻辑

  3. 集成中间件到框架

1. 创建 AuthenticationMiddleware 类

首先,在 src/Middleware/ 目录下创建 AuthenticationMiddleware.php 文件:

<?php
// src/Middleware/AuthenticationMiddleware.php

namespace MyFrameworkMiddleware;

use MyFrameworkContainer;

interface Middleware
{
    /**
     * 处理请求
     *
     * @param array $params 路由参数
     * @return bool 返回 true 继续请求,false 中止
     */
    public function handle(array $params): bool;
}

class AuthenticationMiddleware implements Middleware
{
    protected $container;
    protected $secretKey;

    public function __construct(Container $container)
    {
        $this->container = $container;
        // 在实际应用中,应从配置文件中获取密钥
        $this->secretKey = 'your_secret_key';
    }

    public function handle(array $params): bool
    {
        // 从请求头中获取 Authorization 字段
        $authHeader = isset($_SERVER['HTTP_AUTHORIZATION']) ? $_SERVER['HTTP_AUTHORIZATION'] : '';

        if (!$authHeader) {
            $this->unauthorizedResponse('Authorization header missing');
            return false;
        }

        // 检查是否为 Bearer Token
        if (strpos($authHeader, 'Bearer ') !== 0) {
            $this->unauthorizedResponse('Invalid authorization header format');
            return false;
        }

        // 获取 Token
        $token = substr($authHeader, 7);

        if (!$token) {
            $this->unauthorizedResponse('Token missing');
            return false;
        }

        // 验证 Token
        if (!$this->validateToken($token)) {
            $this->unauthorizedResponse('Invalid or expired token');
            return false;
        }

        // Token 验证成功,继续请求
        return true;
    }

    /**
     * 验证 Bearer Token
     *
     * @param string $token
     * @return bool
     */
    protected function validateToken(string $token): bool
    {
        // 简单示例:检查 Token 是否等于预定义值
        // 在实际应用中,应实现更复杂的验证,如 JWT 解析
        return $token === 'valid_token_example';
    }

    /**
     * 发送 401 Unauthorized 响应
     *
     * @param string $message
     */
    protected function unauthorizedResponse(string $message)
    {
        header('HTTP/1.1 401 Unauthorized');
        header('Content-Type: application/json');
        echo json_encode(['error' => $message]);
        exit();
    }
}
?>

2. 实现 Token 验证逻辑

在上述示例中,validateToken 方法进行了简单的 Token 验证,检查 Token 是否等于预定义值 valid_token_example。在实际应用中,需要使用更安全和灵活的验证机制,如 JWT

3. 集成中间件到框架

确保中间件接口与框架中其他中间件的接口一致。前面的代码示例中,Middleware 接口定义了 handle 方法。确保路由器在分发请求时正确调用中间件。


使用 JWT(JSON Web Token)扩展 认证示例

为了提供更强大的认证机制,建议使用 JWT。JWT 是一种紧凑且独立的方式,用于在各方之间安全地传递信息。它由三个部分组成:头部(Header)、载荷(Payload)和签名(Signature),具有以下优点:

  • 自包含:包含了用户信息和声明(claims),无需查询数据库。

  • 安全性:可以使用签名(如 HMAC SHA256)确保 Token 不被篡改。

  • 灵活性:支持自定义声明,适用于各种认证和授权需求。

安装 JWT 库

使用 Composer 安装 firebase/php-jwt 库:

composer require firebase/php-jwt

更新 AuthenticationMiddleware 以支持 JWT

修改 AuthenticationMiddleware 类以使用 JWT 进行 Token 验证:

<?php
// src/Middleware/AuthenticationMiddleware.php

namespace MyFrameworkMiddleware;

use MyFrameworkContainer;
use FirebaseJWTJWT;
use FirebaseJWTKey;

class AuthenticationMiddleware implements Middleware
{
    protected $container;
    protected $secretKey;

    public function __construct(Container $container)
    {
        $this->container = $container;
        // 在实际应用中,应从配置文件中获取密钥
        $this->secretKey = 'your_secret_key';
    }

    public function handle(array $params): bool
    {
        // 从请求头中获取 Authorization 字段
        $authHeader = isset($_SERVER['HTTP_AUTHORIZATION']) ? $_SERVER['HTTP_AUTHORIZATION'] : '';

        if (!$authHeader) {
            $this->unauthorizedResponse('Authorization header missing');
            return false;
        }

        // 检查是否为 Bearer Token
        if (strpos($authHeader, 'Bearer ') !== 0) {
            $this->unauthorizedResponse('Invalid authorization header format');
            return false;
        }

        // 获取 Token
        $token = substr($authHeader, 7);

        if (!$token) {
            $this->unauthorizedResponse('Token missing');
            return false;
        }

        // 验证 Token
        $payload = $this->validateToken($token);

        if (!$payload) {
            $this->unauthorizedResponse('Invalid or expired token');
            return false;
        }

        // 可选:将用户信息存储到全局或请求上下文中
        // $_SERVER['user'] = $payload->sub;

        // Token 验证成功,继续请求
        return true;
    }

    /**
     * 验证 JWT Token
     *
     * @param string $token
     * @return object|null
     */
    protected function validateToken(string $token)
    {
        try {
            $decoded = JWT::decode($token, new Key($this->secretKey, 'HS256'));
            return $decoded;
        } catch (Exception $e) {
            // 可以记录错误日志
            return null;
        }
    }

    /**
     * 发送 401 Unauthorized 响应
     *
     * @param string $message
     */
    protected function unauthorizedResponse(string $message)
    {
        header('HTTP/1.1 401 Unauthorized');
        header('Content-Type: application/json');
        echo json_encode(['error' => $message]);
        exit();
    }
}
?>

生成 JWT Token

在实际应用中,需要生成 JWT Token,例如在用户登录时生成 Token 并返回给客户端。

<?php
// src/Controllers/AuthController.php

namespace MyFrameworkControllers;

use MyFrameworkController;
use FirebaseJWTJWT;

class AuthController extends Controller
{
    protected $secretKey;

    public function __construct(TwigEnvironment $twig, MonologLogger $logger, MyFrameworkRouter $router)
    {
        parent::__construct($twig, $logger, $router);
        // 应从配置文件中获取密钥
        $this->secretKey = 'your_secret_key';
    }

    /**
     * 用户登录,生成 JWT Token
     */
    public function login()
    {
        // 获取 JSON 请求体
        $input = json_decode(file_get_contents('php://input'), true);

        if (!$input || !isset($input['username']) || !isset($input['password'])) {
            $this->jsonResponse(['error' => '无效的输入'], 400);
            return;
        }

        $username = $input['username'];
        $password = $input['password'];

        // 示例验证逻辑,实际应用中应从数据库验证
        if ($username === 'admin' && $password === 'password') {
            $payload = [
                'iss' => 'yourdomain.com',      // 签发者
                'aud' => 'yourdomain.com',      // 接收者
                'iat' => time(),                // 签发时间
                'nbf' => time(),                // 生效时间
                'exp' => time() + (60 * 60),    // 过期时间(1小时)
                'sub' => 1,                      // 用户ID
            ];

            $jwt = JWT::encode($payload, $this->secretKey, 'HS256');

            $this->jsonResponse(['token' => $jwt], 200);
        } else {
            $this->jsonResponse(['error' => '用户名或密码错误'], 401);
        }
    }
}
?>

生成和管理 Bearer Token

为了使用 Bearer Token(尤其是 JWT)进行认证,需要实现以下功能:

  1. 生成 JWT Token:在用户登录时生成并返回 JWT Token。

  2. 验证 JWT Token:在中间件中验证 Token 的有效性。

  3. 刷新 Token(可选):为长时间会话提供 Token 刷新机制。

使用 JWT

在前面的 AuthenticationMiddleware 示例中,我们已经展示了如何使用 JWT 来验证 Bearer Token。以下是更详细的步骤和示例。

1. 生成 JWT Token

在用户登录成功后,生成 JWT Token 并返回给客户端。

<?php
// src/Controllers/AuthController.php

namespace MyFrameworkControllers;

use MyFrameworkController;
use FirebaseJWTJWT;

class AuthController extends Controller
{
    protected $secretKey;

    public function __construct(TwigEnvironment $twig, MonologLogger $logger, MyFrameworkRouter $router)
    {
        parent::__construct($twig, $logger, $router);
        // 应从配置文件中获取密钥
        $this->secretKey = 'your_secret_key';
    }

    /**
     * 用户登录,生成 JWT Token
     */
    public function login()
    {
        // 获取 JSON 请求体
        $input = json_decode(file_get_contents('php://input'), true);

        if (!$input || !isset($input['username']) || !isset($input['password'])) {
            $this->jsonResponse(['error' => '无效的输入'], 400);
            return;
        }

        $username = $input['username'];
        $password = $input['password'];

        // 示例验证逻辑,实际应用中应从数据库验证
        if ($username === 'admin' && $password === 'password') {
            $payload = [
                'iss' => 'yourdomain.com',      // 签发者
                'aud' => 'yourdomain.com',      // 接收者
                'iat' => time(),                // 签发时间
                'nbf' => time(),                // 生效时间
                'exp' => time() + (60 * 60),    // 过期时间(1小时)
                'sub' => 1,                      // 用户ID
            ];

            $jwt = JWT::encode($payload, $this->secretKey, 'HS256');

            $this->jsonResponse(['token' => $jwt], 200);
        } else {
            $this->jsonResponse(['error' => '用户名或密码错误'], 401);
        }
    }
}
?>

2. 验证 JWT Token

AuthenticationMiddleware 中,已经展示了如何使用 firebase/php-jwt 库来验证 Token 的有效性。以下是关键部分的复述:

<?php
// src/Middleware/AuthenticationMiddleware.php

namespace MyFrameworkMiddleware;

use MyFrameworkContainer;
use FirebaseJWTJWT;
use FirebaseJWTKey;

class AuthenticationMiddleware implements Middleware
{
    protected $container;
    protected $secretKey;

    public function __construct(Container $container)
    {
        $this->container = $container;
        // 应从配置文件中获取密钥
        $this->secretKey = 'your_secret_key';
    }

    public function handle(array $params): bool
    {
        // 从请求头中获取 Authorization 字段
        $authHeader = isset($_SERVER['HTTP_AUTHORIZATION']) ? $_SERVER['HTTP_AUTHORIZATION'] : '';

        if (!$authHeader) {
            $this->unauthorizedResponse('Authorization header missing');
            return false;
        }

        // 检查是否为 Bearer Token
        if (strpos($authHeader, 'Bearer ') !== 0) {
            $this->unauthorizedResponse('Invalid authorization header format');
            return false;
        }

        // 获取 Token
        $token = substr($authHeader, 7);

        if (!$token) {
            $this->unauthorizedResponse('Token missing');
            return false;
        }

        // 验证 Token
        $payload = $this->validateToken($token);

        if (!$payload) {
            $this->unauthorizedResponse('Invalid or expired token');
            return false;
        }

        // 可选:将用户信息存储到全局或请求上下文中
        // $_SERVER['user'] = $payload->sub;

        // Token 验证成功,继续请求
        return true;
    }

    /**
     * 验证 JWT Token
     *
     * @param string $token
     * @return object|null
     */
    protected function validateToken(string $token)
    {
        try {
            $decoded = JWT::decode($token, new Key($this->secretKey, 'HS256'));
            return $decoded;
        } catch (Exception $e) {
            // 可以记录错误日志
            return null;
        }
    }

    /**
     * 发送 401 Unauthorized 响应
     *
     * @param string $message
     */
    protected function unauthorizedResponse(string $message)
    {
        header('HTTP/1.1 401 Unauthorized');
        header('Content-Type: application/json');
        echo json_encode(['error' => $message]);
        exit();
    }
}
?>

3. 刷新 Token(可选)

对于需要长期会话的应用,可以实现 Token 刷新机制。通常,客户端会持有一个刷新令牌(Refresh Token),用于获取新的访问令牌(Access Token)。


集成中间件到框架中

确保中间件在请求处理过程中被正确调用。以下是如何在路由器中集成 AuthenticationMiddleware 的示例。

更新 Router 类

假设框架中已经有一个 Router 类,并且它支持中间件。以下是一个简化的 Router 类示例,展示如何集成中间件。

<?php
// src/Router.php

namespace MyFramework;

use MyFrameworkMiddlewareMiddleware;
use ReflectionException;

class Router
{
    private $routes = [];
    private $namedRoutes = [];
    private $container;

    /**
     * 构造函数
     *
     * @param Container $container 依赖注入容器
     */
    public function __construct(Container $container)
    {
        $this->container = $container;
        $this->loadCache(); // 如果实现了路由缓存
    }

    /**
     * 添加路由规则
     *
     * @param string $method HTTP 方法
     * @param string $uri 请求的 URI
     * @param string $action 控制器和方法,例如 'UsersController@show'
     * @param array $middlewares 中间件列表
     * @param string|null $name 路由名称
     */
    public function add(string $method, string $uri, string $action, array $middlewares = [], string $name = null)
    {
        // 转换 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,
            'name'        => $name
        ];

        if ($name) {
            $this->namedRoutes[$name] = end($this->routes);
        }
    }

    /**
     * 分发请求到相应的控制器方法
     *
     * @param string $requestMethod HTTP 方法
     * @param string $requestUri 请求的 URI
     */
    public function dispatch(string $requestMethod, string $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) {
                        /** @var Middleware $middleware */
                        $middleware = $this->container->make($middlewareClass);
                        if (!$middleware->handle($params)) {
                            // 中间件中止请求
                            return;
                        }
                    }

                    // 解析控制器和方法
                    list($controllerName, $method) = explode('@', $route['action']);
                    $fullControllerName = "MyFramework\Controllers\$controllerName";

                    // 通过容器获取控制器实例
                    if ($this->container->make($fullControllerName)) {
                        $controller = $this->container->make($fullControllerName);
                        if (method_exists($controller, $method)) {
                            // 调用方法并传递参数
                            call_user_func_array([$controller, $method], $params);
                            return;
                        }
                    }

                    // 如果控制器或方法不存在,返回 404
                    $this->sendNotFound();
                }
            }
        }
        // 如果没有匹配的路由,返回 404
        $this->sendNotFound();
    }

    /**
     * 发送 404 响应
     */
    private function sendNotFound()
    {
        header("HTTP/1.0 404 Not Found");
        echo json_encode(['error' => '资源未找到']);
        exit();
    }

    /**
     * 提取路由中的参数名称
     *
     * @param string $uri 路由 URI
     * @return array 参数名称列表
     */
    private function extractParams(string $uri): array
    {
        preg_match_all('/{([a-zA-Z0-9_]+)(?)?}/', $uri, $matches);
        return $matches[1];
    }

    // ... 路由缓存相关方法 ...
}
?>

示例控制器

以下是一个示例控制器,展示如何使用经过认证的用户信息(例如,从 JWT Payload 获取用户 ID)。

<?php
// src/Controllers/UsersController.php

namespace MyFrameworkControllers;

use MyFrameworkController;
use MyFrameworkModelsUserModel;
use FirebaseJWTJWT;
use FirebaseJWTKey;

class UsersController extends Controller
{
    protected $userModel;
    protected $secretKey;

    public function __construct(TwigEnvironment $twig, MonologLogger $logger, MyFrameworkRouter $router, UserModel $userModel)
    {
        parent::__construct($twig, $logger, $router);
        $this->userModel = $userModel;
        $this->secretKey = 'your_secret_key'; // 应从配置文件中获取
    }

    /**
     * 获取用户列表
     */
    public function index()
    {
        $this->logger->info("获取用户列表");

        $users = $this->userModel->getAllUsers();

        $this->jsonResponse($users, 200);
    }

    /**
     * 获取单个用户详情
     *
     * @param int $id 用户ID
     */
    public function show($id)
    {
        $this->logger->info("获取用户详情", ['id' => $id]);

        $user = $this->userModel->getUserById($id);

        if ($user) {
            $this->jsonResponse($user, 200);
        } else {
            $this->jsonResponse(['error' => '用户未找到'], 404);
        }
    }

    /**
     * 创建新用户
     */
    public function store()
    {
        $this->logger->info("创建新用户");

        // 获取 JSON 请求体
        $input = json_decode(file_get_contents('php://input'), true);

        if (!$input || !isset($input['name']) || !isset($input['email'])) {
            $this->jsonResponse(['error' => '无效的输入'], 400);
            return;
        }

        $name = htmlspecialchars($input['name']);
        $email = htmlspecialchars($input['email']);

        $newUser = $this->userModel->createUser($name, $email);

        if ($newUser) {
            $this->jsonResponse($newUser, 201);
        } else {
            $this->jsonResponse(['error' => '创建用户失败'], 500);
        }
    }

    /**
     * 更新用户信息
     *
     * @param int $id 用户ID
     */
    public function update($id)
    {
        $this->logger->info("更新用户信息", ['id' => $id]);

        // 获取 JSON 请求体
        $input = json_decode(file_get_contents('php://input'), true);

        if (!$input) {
            $this->jsonResponse(['error' => '无效的输入'], 400);
            return;
        }

        $updateData = array_map('htmlspecialchars', $input);

        $updatedUser = $this->userModel->updateUser($id, $updateData);

        if ($updatedUser) {
            $this->jsonResponse($updatedUser, 200);
        } else {
            $this->jsonResponse(['error' => '用户未找到或更新失败'], 404);
        }
    }

    /**
     * 删除用户
     *
     * @param int $id 用户ID
     */
    public function destroy($id)
    {
        $this->logger->info("删除用户", ['id' => $id]);

        $deleted = $this->userModel->deleteUser($id);

        if ($deleted) {
            $this->jsonResponse(['message' => '用户已删除'], 200);
        } else {
            $this->jsonResponse(['error' => '用户未找到'], 404);
        }
    }

    /**
     * 发送 JSON 响应
     *
     * @param mixed $data 数据
     * @param int $status HTTP 状态码
     */
    protected function jsonResponse($data, $status = 200)
    {
        header_remove();
        http_response_code($status);
        header("Content-Type: application/json");
        echo json_encode($data);
        exit();
    }
}
?>

更新路由定义

确保在路由中正确指定中间件。例如:

<?php
// index.php

use MyFrameworkRouter;
use MyFrameworkControllersUsersController;
use MyFrameworkControllersAuthController;
use MyFrameworkMiddlewareAuthenticationMiddleware;

// ... 之前的代码 ...

// 定义认证路由
$router->add('POST', '/login', 'AuthController@login', [], 'auth.login');

// 定义受保护的 RESTful 用户路由
$router->add('GET', '/users', 'UsersController@index', [AuthenticationMiddleware::class], 'users.index');
$router->add('GET', '/users/{id}', 'UsersController@show', [AuthenticationMiddleware::class], 'users.show');
$router->add('POST', '/users', 'UsersController@store', [AuthenticationMiddleware::class], 'users.store');
$router->add('PUT', '/users/{id}', 'UsersController@update', [AuthenticationMiddleware::class], 'users.update');
$router->add('DELETE', '/users/{id}', 'UsersController@destroy', [AuthenticationMiddleware::class], 'users.destroy');

// 处理请求
$router->dispatch($requestMethod, $requestUri);
?>

安全性考虑

在实现认证中间件时,安全性是首要考虑因素。以下是一些关键的安全性最佳实践:

  1. 使用强密钥

    • 确保 JWT 使用强大的密钥进行签名,避免使用易猜测的字符串。

    • 将密钥存储在安全的位置(如环境变量),而非代码库中。

  2. Token 过期与刷新

    • 为 Token 设置合理的过期时间,避免长时间有效的 Token 带来的安全风险。

    • 实现 Token 刷新机制,以便在 Token 过期后获取新的 Token。

  3. HTTPS 使用

    • 始终通过 HTTPS 传输 Token,防止中间人攻击和 Token 被截获。

  4. Token 存储

    • 客户端应安全地存储 Token,避免 XSS 攻击导致 Token 泄露。

    • 使用 HttpOnly 和 Secure 标志的 Cookie 存储 Token。

  5. 防止 Token 重放

    • 实现 Token 唯一性和不可重放机制,如使用短期 Token 和 Refresh Token。

  6. 输入验证与清理

    • 在处理用户输入时,进行严格的验证和清理,防止注入攻击。


总结

通过实现一个简单的 AuthenticationMiddleware 并支持 Bearer Token 认证(尤其是基于 JWT 的认证),可以显著提升自定义 PHP 框架的安全性和灵活性。以下是关键步骤的回顾:

  1. 理解中间件与认证中间件:中间件在请求处理流程中扮演关键角色,认证中间件确保请求的合法性和安全性。

  2. 选择认证方式:Bearer Token(尤其是 JWT)是一种现代、灵活且安全的认证方式,适用于多种应用场景。

  3. 实现 AuthenticationMiddleware:创建一个中间件类,负责解析和验证 Token,并在验证失败时中止请求。

  4. 集成中间件到框架中:在路由定义中指定需要认证的路由,并确保中间件被正确调用。

  5. 生成和管理 Token:在用户登录时生成 JWT Token,并确保 Token 的安全性和有效性。

  6. 遵循安全性最佳实践:确保 Token 的安全存储、传输和管理,防止潜在的安全漏洞。