目录¶
理解中间件与认证中间件¶
中间件(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 路由,确保只有携带有效令牌的请求才能访问受保护的资源。
步骤:¶
创建 AuthenticationMiddleware 类
实现 Token 验证逻辑
集成中间件到框架
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)进行认证,需要实现以下功能:
生成 JWT Token:在用户登录时生成并返回 JWT Token。
验证 JWT Token:在中间件中验证 Token 的有效性。
刷新 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);
?>
安全性考虑¶
在实现认证中间件时,安全性是首要考虑因素。以下是一些关键的安全性最佳实践:
使用强密钥:
确保 JWT 使用强大的密钥进行签名,避免使用易猜测的字符串。
将密钥存储在安全的位置(如环境变量),而非代码库中。
Token 过期与刷新:
为 Token 设置合理的过期时间,避免长时间有效的 Token 带来的安全风险。
实现 Token 刷新机制,以便在 Token 过期后获取新的 Token。
HTTPS 使用:
始终通过 HTTPS 传输 Token,防止中间人攻击和 Token 被截获。
Token 存储:
客户端应安全地存储 Token,避免 XSS 攻击导致 Token 泄露。
使用 HttpOnly 和 Secure 标志的 Cookie 存储 Token。
防止 Token 重放:
实现 Token 唯一性和不可重放机制,如使用短期 Token 和 Refresh Token。
输入验证与清理:
在处理用户输入时,进行严格的验证和清理,防止注入攻击。
总结¶
通过实现一个简单的 AuthenticationMiddleware
并支持 Bearer Token 认证(尤其是基于 JWT 的认证),可以显著提升自定义 PHP 框架的安全性和灵活性。以下是关键步骤的回顾:
理解中间件与认证中间件:中间件在请求处理流程中扮演关键角色,认证中间件确保请求的合法性和安全性。
选择认证方式:Bearer Token(尤其是 JWT)是一种现代、灵活且安全的认证方式,适用于多种应用场景。
实现 AuthenticationMiddleware:创建一个中间件类,负责解析和验证 Token,并在验证失败时中止请求。
集成中间件到框架中:在路由定义中指定需要认证的路由,并确保中间件被正确调用。
生成和管理 Token:在用户登录时生成 JWT Token,并确保 Token 的安全性和有效性。
遵循安全性最佳实践:确保 Token 的安全存储、传输和管理,防止潜在的安全漏洞。