Laravel 13 中间件进阶指南

中间件是 Laravel 请求处理流程中的核心组件,提供了强大的请求过滤和响应处理能力。本文将深入探讨 Laravel 13 中间件的高级用法。

中间件基础回顾

创建中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class CheckUserRole
{
public function handle(Request $request, Closure $next, string $role): Response
{
if (! $request->user() || ! $request->user()->hasRole($role)) {
return response()->json(['message' => 'Unauthorized'], 403);
}

return $next($request);
}
}

注册中间件

1
2
3
4
5
6
7
// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
$middleware->alias([
'role' => \App\Http\Middleware\CheckUserRole::class,
'throttle' => \App\Http\Middleware\ThrottleRequests::class,
]);
})

中间件参数

多参数中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class CheckPermission
{
public function handle(
Request $request,
Closure $next,
string $permission,
string $guard = null
): Response {
$guard = $guard ?? config('auth.defaults.guard');

if (! $request->user($guard)?->can($permission)) {
abort(403, "Missing permission: {$permission}");
}

return $next($request);
}
}

路由中使用参数

1
2
3
4
Route::middleware('permission:edit-posts,admin')->group(function () {
Route::put('/posts/{post}', [PostController::class, 'update']);
Route::delete('/posts/{post}', [PostController::class, 'destroy']);
});

可终止中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
use App\Services\AnalyticsService;

class LogRequestDuration
{
public function __construct(
private AnalyticsService $analytics
) {}

public function handle(Request $request, Closure $next): Response
{
$request->attributes->set('start_time', microtime(true));

return $next($request);
}

public function terminate(Request $request, Response $response): void
{
$duration = microtime(true) - $request->attributes->get('start_time');

$this->analytics->record([
'path' => $request->path(),
'method' => $request->method(),
'duration_ms' => round($duration * 1000, 2),
'status' => $response->getStatusCode(),
]);
}
}

中间件优先级

定义优先级顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
$middleware->priority([
\Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests::class,
\Illuminate\Cookie\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\Illuminate\Contracts\Auth\Middleware\AuthenticatesRequests::class,
\Illuminate\Routing\Middleware\ThrottleRequests::class,
\Illuminate\Routing\Middleware\ThrottleRequestsWithRedis::class,
\Illuminate\Contracts\Session\Middleware\AuthenticatesSessions::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\Illuminate\Auth\Middleware\Authorize::class,
\App\Http\Middleware\CheckUserRole::class,
]);
})

中间件组

创建中间件组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
$middleware->group('api', [
\Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
\Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
]);

$middleware->group('web', [
\Illuminate\Cookie\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
]);
})

全局中间件

添加全局中间件

1
2
3
4
5
6
7
8
// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
$middleware->append(\App\Http\Middleware\ForceHttps::class);

$middleware->prepend(\App\Http\Middleware\CheckMaintenanceMode::class);

$middleware->remove(\Illuminate\Http\Middleware\TrustProxies::class);
})

全局中间件示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class ForceHttps
{
public function handle(Request $request, Closure $next): Response
{
if (! $request->secure() && app()->environment('production')) {
return redirect()->secure($request->getRequestUri());
}

return $next($request);
}
}

排除中间件

路由级别排除

1
2
3
4
5
6
7
8
9
10
Route::middleware(['auth', 'verified'])->group(function () {
Route::get('/profile', [ProfileController::class, 'show'])
->withoutMiddleware(Verified::class);

Route::get('/settings', [SettingsController::class, 'index']);
});

Route::withoutMiddleware([Authenticate::class])->group(function () {
Route::get('/public', [PublicController::class, 'index']);
});

条件中间件

基于条件的中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class ConditionalCache
{
public function handle(Request $request, Closure $next, int $minutes = 60): Response
{
$response = $next($request);

if ($this->shouldCache($request, $response)) {
$response->setPublic();
$response->setMaxAge($minutes * 60);
$response->setEtag(md5($response->getContent()));
}

return $response;
}

private function shouldCache(Request $request, Response $response): bool
{
return $request->isMethod('GET') &&
$response->getStatusCode() === 200 &&
! $request->user() &&
! $request->ajax();
}
}

请求修改中间件

添加请求数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class AddRequestContext
{
public function handle(Request $request, Closure $next): Response
{
$request->merge([
'_request_id' => (string) Str::uuid(),
'_timestamp' => now()->toIso8601String(),
'_ip' => $request->ip(),
'_user_agent' => $request->userAgent(),
]);

$request->attributes->set('request_id', Str::uuid()->toString());

return $next($request);
}
}

响应修改中间件

添加响应头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class AddSecurityHeaders
{
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);

$response->headers->set('X-Content-Type-Options', 'nosniff');
$response->headers->set('X-Frame-Options', 'DENY');
$response->headers->set('X-XSS-Protection', '1; mode=block');
$response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
$response->headers->set('Permissions-Policy', 'geolocation=(), microphone=()');

if ($request->isSecure()) {
$response->headers->set(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains'
);
}

return $response;
}
}

速率限制中间件

自定义速率限制器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// AppServiceProvider.php
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;

public function boot(): void
{
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});

RateLimiter::for('uploads', function (Request $request) {
return $request->user()
? Limit::perMinute(10)->by($request->user()->id)
: Limit::perMinute(2)->by($request->ip());
});

RateLimiter::for('global', function (Request $request) {
return Limit::perMinute(1000)
->response(function () {
return response('Too many requests', 429);
});
});
}

动态速率限制

1
2
3
4
5
6
7
8
9
10
11
12
13
RateLimiter::for('dynamic', function (Request $request) {
$user = $request->user();

if ($user?->isPremium()) {
return Limit::none();
}

if ($user?->isVerified()) {
return Limit::perMinute(100)->by($user->id);
}

return Limit::perMinute(30)->by($request->ip());
});

CORS 中间件

配置 CORS

1
2
3
4
5
6
7
8
9
10
11
// config/cors.php
return [
'paths' => ['api/*', 'sanctum/csrf-cookie'],
'allowed_methods' => ['*'],
'allowed_origins' => ['https://example.com', 'https://app.example.com'],
'allowed_origins_patterns' => ['/https?:\/\/([a-z0-9-]+\.)?example\.com/'],
'allowed_headers' => ['*'],
'exposed_headers' => ['X-Total-Count', 'X-Page-Count'],
'max_age' => 86400,
'supports_credentials' => true,
];

中间件测试

测试中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<?php

namespace Tests\Feature\Middleware;

use Tests\TestCase;
use App\Models\User;
use Illuminate\Http\Request;
use App\Http\Middleware\CheckUserRole;
use Symfony\Component\HttpFoundation\Response;

class CheckUserRoleTest extends TestCase
{
private CheckUserRole $middleware;

protected function setUp(): void
{
parent::setUp();
$this->middleware = new CheckUserRole();
}

public function test_allows_user_with_correct_role(): void
{
$user = User::factory()->create(['role' => 'admin']);

$request = Request::create('/admin', 'GET');
$request->setUserResolver(fn () => $user);

$response = $this->middleware->handle($request, fn () => new Response(), 'admin');

$this->assertEquals(200, $response->getStatusCode());
}

public function test_denies_user_without_role(): void
{
$user = User::factory()->create(['role' => 'user']);

$request = Request::create('/admin', 'GET');
$request->setUserResolver(fn () => $user);

$response = $this->middleware->handle($request, fn () => new Response(), 'admin');

$this->assertEquals(403, $response->getStatusCode());
}
}

最佳实践

1. 单一职责

1
2
3
4
5
6
7
// 好的做法:每个中间件只做一件事
class Authenticate { }
class Authorize { }
class ThrottleRequests { }

// 不好的做法:一个中间件做太多事
class AuthAndThrottleAndLog { }

2. 使用依赖注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use App\Services\GeoLocationService;

class RestrictByCountry
{
public function __construct(
private GeoLocationService $geoService
) {}

public function handle(Request $request, Closure $next, string ...$countries): Response
{
$country = $this->geoService->getCountryCode($request->ip());

if (! in_array($country, $countries)) {
abort(403, 'This content is not available in your region.');
}

return $next($request);
}
}

3. 合理使用终止中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
use Illuminate\Support\Facades\Log;

class RequestLogger
{
public function handle(Request $request, Closure $next): Response
{
return $next($request);
}

public function terminate(Request $request, Response $response): void
{
Log::channel('requests')->info('Request completed', [
'method' => $request->method(),
'url' => $request->fullUrl(),
'status' => $response->getStatusCode(),
'duration' => defined('LARAVEL_START')
? round((microtime(true) - LARAVEL_START) * 1000, 2)
: null,
]);
}
}

总结

Laravel 13 的中间件系统提供了强大而灵活的请求处理能力。通过合理使用中间件参数、可终止中间件、中间件组和优先级控制,可以构建出安全、高效、可维护的应用程序。记住保持中间件的单一职责原则,充分利用依赖注入,并在适当的时候使用终止中间件来处理耗时操作。