Skip to content

Laravel - 中间件

Middleware(中间件)提供了一种方便的机制,用于检查和过滤进入应用的 HTTP 请求。例如,Laravel 内置了一个中间件,用于验证应用用户是否已认证。如果用户未认证,该中间件会将用户重定向到应用的登录界面。但是,如果用户已认证,中间件将允许请求进一步进入应用。

可以将中间件视为 HTTP 请求在到达应用之前必须通过的层。每一层都可以检查请求,甚至可以完全拒绝它。

中间件充当请求和响应之间的中介。它是一种过滤机制。例如,Laravel 内置的 Authenticate 中间件检查用户是否已登录。如果已登录,请求继续处理;否则,用户将被重定向到登录页面。

你可以使用 Artisan 命令生成新的中间件:

php artisan make:middleware <MiddlewareName>

将 <MiddlewareName> 替换为你想要的中间件名称(例如,CheckUserAge)。新的中间件类将放置在 app/Http/Middleware 目录下。

步骤 1:生成中间件。我们来创建一个 EnsureUserIsOldEnough 中间件:

php artisan make:middleware EnsureUserIsOldEnough

步骤 2:运行命令后,Laravel 将输出一条消息,指示中间件已成功创建:

INFO Middleware [app/Http/Middleware/EnsureUserIsOldEnough.php] created successfully.

步骤 3:app/Http/Middleware 目录下将创建 EnsureUserIsOldEnough.php 文件。它将包含一个 handle 方法。我们来修改它,检查请求中假设的 ‘age’ 参数是否大于或等于 18:

<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class EnsureUserIsOldEnough
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
* @return \Symfony\Component\HttpFoundation\Response
*/
public function handle(Request $request, Closure $next): Response
{
// 示例:检查 'age' 输入或用户的年龄属性
// 这是一个简化示例。在实际应用中,你会从数据库或 session 中获取用户年龄。
if ($request->input('age', 0) < 18) {
// 重定向到不同页面或返回错误响应
return redirect('home')->with('error', 'You must be 18 or older to access this page.');
}
return $next($request);
}
}

在使用中间件之前,必须在应用的 HTTP 内核中注册它。HTTP 内核位于 app/Http/Kernel.php。

有三种主要的注册中间件的方式:

  • 全局中间件 (Global Middleware): 它们会运行在每个进入应用的 HTTP 请求上。在 app/Http/Kernel.php 类的 $middleware 属性中注册它们。
  • 中间件组 (Middleware Groups): 将多个中间件分组到一个键下,以便更轻松地分配给路由。Laravel 默认的 web 和 api 组定义在 $middlewareGroups 属性中。
  • 路由中间件 (Route Middleware / Aliases): 为中间件分配一个短别名 (alias),然后可以将其分配给特定路由。在 $middlewareAliases (或旧版本 Laravel 中的 $routeMiddleware) 属性中注册它们。

要让中间件在每次 HTTP 请求期间运行,请将其添加到 $middleware 属性中:

protected $middleware = [
// ... 其他全局中间件
\App\Http\Middleware\EnsureUserIsOldEnough::class, // 如果你想让它成为全局中间件
];

对于仅适用于特定路由的中间件,请在 $middlewareAliases 属性中为其分配一个键。此键将用于在路由定义中引用该中间件。

protected $middlewareAliases = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
// ... 其他别名
'isOldEnough' => \App\Http\Middleware\EnsureUserIsOldEnough::class, // 我们的自定义别名
];

示例:将 EnsureUserIsOldEnough 注册为路由中间件

Section titled “示例:将 EnsureUserIsOldEnough 注册为路由中间件”

我们来将 EnsureUserIsOldEnough 中间件注册到 app/Http/Kernel.php 中,并设置别名为 isOldEnough:

<?php
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
// ... (其他属性,如 $middleware, $middlewareGroups)
protected $middlewareAliases = [
'auth' => \App\Http\Middleware\Authenticate::class,
// ... 其他默认别名
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'isOldEnough' => \App\Http\Middleware\EnsureUserIsOldEnough::class, // 添加了我们的中间件别名
// ... 其他别名
];
}

注册后,你可以在 routes/web.php 文件中将 isOldEnough 中间件分配给路由:

use App\Http\Controllers\ProfileController;
Route::get('/restricted-area', [ProfileController::class, 'show'])->middleware('isOldEnough');
// 你也可以将它应用到一个路由组:
Route::middleware(['isOldEnough'])->group(function () {
Route::get('/another-restricted-page', function () {
return 'Welcome to another restricted page!';
});
});

中间件还可以接收额外的参数。例如,如果你的应用需要验证用户的角色才能允许访问某些操作,一个 CheckRole 中间件可以接收所需的角色作为参数。

要传递参数,请在路由定义中将参数附加到中间件名称后面,用冒号 : 分隔。多个参数用逗号分隔。这些参数将作为 $next 参数之后的参数传递给 handle 方法。

示例:创建并使用 CheckRole 中间件

Section titled “示例:创建并使用 CheckRole 中间件”

步骤 1:创建 CheckRole 中间件:

php artisan make:middleware CheckRole

步骤 2:修改 app/Http/Middleware/CheckRole.php 中的 handle 方法以接受一个角色参数:

<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class CheckRole
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string $role 需要检查的角色。
* @return \Symfony\Component\HttpFoundation\Response
*/
public function handle(Request $request, Closure $next, string $role): Response
{
// 假设 $request->user()->hasRole($role) 是一个检查用户角色的方法
// 这是你实际角色检查逻辑的占位符。
if (! $request->user() || ! $request->user()->hasRole($role)) {
// 为了演示,我们直接中止请求。在实际应用中,应重定向或返回 JSON 错误。
abort(403, 'Unauthorized action. Required role: ' . $role);
}
// 对于本教程的原始输出风格:
// echo "Required Role: ".$role."<br>";
return $next($request);
}
}

步骤 3:在 app/Http/Kernel.php 中注册 CheckRole 中间件并为其设置别名,例如 role:

protected $middlewareAliases = [
// ... 其他别名
'role' => \App\Http\Middleware\CheckRole::class,
];

步骤 4:创建一个简单的控制器,例如 AdminController:

php artisan make:controller AdminController

向 app/Http/Controllers/AdminController.php 添加一个方法:

<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class AdminController extends Controller
{
public function dashboard()
{
return "Welcome to the Admin Dashboard!";
}
}

步骤 5:在 routes/web.php 中定义一个使用中间件并带有参数的路由:

// 在 routes/web.php 文件中
use App\Http\Controllers\AdminController;
// 这假设你已设置认证并且用户模型有 hasRole() 方法。
// 如果没有完整的认证设置,你可能需要模拟用户或调整中间件逻辑以进行测试。
Route::get('/admin/dashboard', [AdminController::class, 'dashboard'])
->middleware(['auth', 'role:editor']); // 需要 'auth' 和 'role' 中间件,'role' 带参数 'editor'。

步骤 6:要测试此功能,请访问 /admin/dashboard。如果认证用户拥有 ‘editor’ 角色,他们将看到仪表板。否则,他们将收到 403 错误(或你在 CheckRole 中实现的任何逻辑)。

常见陷阱:如果你的 CheckRole 中间件依赖于 $request->user(),请确保它在认证中间件 (auth) 之后运行。

有时,中间件可能需要在响应发送到浏览器之后执行一些工作。例如,会话中间件可能在响应发送后将会话数据写入存储。为了实现这一点,在你的中间件上定义一个 terminate 方法。

terminate 方法接收请求和响应。可终止的中间件应注册为全局中间件或包含在中间件组中。

示例:创建一个 LogResponseSent 终止中间件

Section titled “示例:创建一个 LogResponseSent 终止中间件”

步骤 1:创建 LogResponseSent 中间件:

php artisan make:middleware LogResponseSent

步骤 2:向 app/Http/Middleware/LogResponseSent.php 添加 handle 和 terminate 方法:

<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;
class LogResponseSent
{
public function handle(Request $request, Closure $next): Response
{
// Log::info('LogResponseSent: 在将请求发送到下一个中间件/控制器之前处理请求。');
// 对于本教程的原始输出风格:
// echo "Executing statements of handle method of LogResponseSent Middleware.<br>";
return $next($request);
}
/**
* 处理响应已发送到浏览器后的任务。
*
* @param \Illuminate\Http\Request $request
* @param \Symfony\Component\HttpFoundation\Response $response
* @return void
*/
public function terminate(Request $request, Response $response): void
{
Log::info('LogResponseSent: 响应已发送到浏览器。', [
'url' => $request->fullUrl(),
'status' => $response->getStatusCode()
]);
// 对于本教程的原始输出风格:
// echo "<br>Executing statements of terminate method of LogResponseSent Middleware.";
}
}

步骤 3:将 LogResponseSent 注册为全局中间件(或注册到像 web 这样的组中)在 app/Http/Kernel.php 里:

protected $middleware = [
// ... 其他全局中间件
\App\Http\Middleware\LogResponseSent::class,
];

步骤 4:创建一个控制器,例如 HomeController:

php artisan make:controller HomeController

添加一个索引方法:

<?php
namespace App\Http\Controllers;
class HomeController extends Controller
{
public function index()
{
// echo "<br>HomeController: Processing request.<br>";
return "Welcome Home!";
}
}

步骤 5:在 routes/web.php 中定义一个路由:

// 在 routes/web.php 文件中
use App\Http\Controllers\HomeController;
Route::get('/home-terminable', [HomeController::class, 'index']);

步骤 6:当你访问 http://your-app.test/home-terminable 时,首先运行 handle 方法中的逻辑,然后是控制器,接着浏览器接收到 “Welcome Home!”,最后执行 terminate 方法中的逻辑,写入你的日志文件(例如,storage/logs/laravel.log)。

要查看原始教程风格的 echoed 输出,你需要确保你的 Web 服务器配置(如带 PHP-FPM 的 Nginx)允许输出缓冲,并且能够刷新,这样 terminate 中的 echo 才能可见。但是,日志记录是标准的用例。

有关更深入的信息,请参考官方 Laravel 关于中间件的文档:https://laravel.com/docs/middleware