Laravel 13 CORS 配置完全指南

跨域资源共享(CORS)是现代 Web 应用中处理跨域请求的关键机制。本文将深入探讨 Laravel 13 中 CORS 配置的各种方法和最佳实践。

CORS 基础

什么是 CORS

CORS 是一种浏览器安全机制,用于控制跨域 HTTP 请求。当 Web 应用向不同域名、端口或协议发起请求时,浏览器会执行 CORS 检查。

Laravel 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' => ['*'],
'allowed_origins_patterns' => [],
'allowed_headers' => ['*'],
'exposed_headers' => [],
'max_age' => 0,
'supports_credentials' => false,
];

基础配置

允许特定域名

1
2
3
4
5
6
7
8
9
10
11
12
13
return [
'paths' => ['api/*'],
'allowed_methods' => ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
'allowed_origins' => [
'https://example.com',
'https://www.example.com',
'https://app.example.com',
],
'allowed_headers' => ['*'],
'exposed_headers' => ['Authorization'],
'max_age' => 86400,
'supports_credentials' => true,
];

允许所有域名

1
2
3
4
5
6
7
8
9
return [
'paths' => ['api/*'],
'allowed_methods' => ['*'],
'allowed_origins' => ['*'],
'allowed_headers' => ['*'],
'exposed_headers' => [],
'max_age' => 0,
'supports_credentials' => false,
];

使用正则表达式

1
2
3
4
5
6
7
8
9
10
11
return [
'paths' => ['api/*'],
'allowed_methods' => ['*'],
'allowed_origins' => [],
'allowed_origins_patterns' => [
'/^https:\/\/[a-z0-9-]+\.example\.com$/',
'/^http:\/\/localhost:[0-9]+$/',
],
'allowed_headers' => ['*'],
'max_age' => 86400,
];

高级配置

按路径配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
return [
'paths' => [
'api/*' => [
'allowed_methods' => ['GET', 'POST', 'PUT', 'DELETE'],
'allowed_origins' => ['https://example.com'],
'allowed_headers' => ['Content-Type', 'Authorization'],
],
'api/public/*' => [
'allowed_methods' => ['GET'],
'allowed_origins' => ['*'],
'allowed_headers' => ['*'],
],
],
];

动态 CORS 配置

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
<?php

namespace App\Http\Middleware;

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

class DynamicCors
{
protected array $config;

public function handle(Request $request, Closure $next): Response
{
if ($request->isMethod('OPTIONS')) {
return $this->handlePreflight($request);
}

$response = $next($request);

$this->addCorsHeaders($request, $response);

return $response;
}

protected function handlePreflight(Request $request): Response
{
$response = response('', 204);

$this->addCorsHeaders($request, $response);

$response->headers->set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
$response->headers->set('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With');
$response->headers->set('Access-Control-Max-Age', '86400');

return $response;
}

protected function addCorsHeaders(Request $request, Response $response): void
{
$origin = $request->header('Origin');

if ($this->isAllowedOrigin($origin)) {
$response->headers->set('Access-Control-Allow-Origin', $origin);
}

$response->headers->set('Access-Control-Allow-Credentials', 'true');
$response->headers->set('Access-Control-Expose-Headers', 'Authorization, X-Total-Count');
}

protected function isAllowedOrigin(?string $origin): bool
{
if (!$origin) {
return false;
}

$allowedOrigins = config('cors.allowed_origins', []);
$patterns = config('cors.allowed_origins_patterns', []);

if (in_array('*', $allowedOrigins)) {
return true;
}

if (in_array($origin, $allowedOrigins)) {
return true;
}

foreach ($patterns as $pattern) {
if (preg_match($pattern, $origin)) {
return true;
}
}

return false;
}
}

认证与 CORS

Sanctum CORS 配置

1
2
3
4
5
6
7
8
// config/sanctum.php
return [
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
'%s%s',
'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
env('APP_URL') ? ','.parse_url(env('APP_URL'), PHP_URL_HOST) : ''
))),
];
1
2
3
4
5
6
7
8
9
return [
'paths' => ['api/*', 'sanctum/csrf-cookie'],
'allowed_methods' => ['*'],
'allowed_origins' => ['https://example.com'],
'allowed_headers' => ['*'],
'exposed_headers' => ['Authorization'],
'max_age' => 86400,
'supports_credentials' => true,
];

CORS 中间件

自定义 CORS 中间件

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
<?php

namespace App\Http\Middleware;

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

class CustomCors
{
protected array $allowedOrigins = [];
protected array $allowedMethods = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'];
protected array $allowedHeaders = ['Content-Type', 'Authorization'];
protected array $exposedHeaders = ['Authorization', 'X-Total-Count'];
protected int $maxAge = 86400;
protected bool $supportsCredentials = true;

public function __construct()
{
$this->allowedOrigins = config('cors.allowed_origins', []);
}

public function handle(Request $request, Closure $next): Response
{
if ($request->isMethod('OPTIONS')) {
return $this->handlePreflight($request);
}

$response = $next($request);

return $this->addCorsHeaders($request, $response);
}

protected function handlePreflight(Request $request): Response
{
$response = response('', 204);

$origin = $request->header('Origin');

if ($this->isAllowed($origin)) {
$response->headers->set('Access-Control-Allow-Origin', $origin);
$response->headers->set('Access-Control-Allow-Methods', implode(', ', $this->allowedMethods));
$response->headers->set('Access-Control-Allow-Headers', implode(', ', $this->allowedHeaders));
$response->headers->set('Access-Control-Max-Age', $this->maxAge);

if ($this->supportsCredentials) {
$response->headers->set('Access-Control-Allow-Credentials', 'true');
}
}

return $response;
}

protected function addCorsHeaders(Request $request, Response $response): Response
{
$origin = $request->header('Origin');

if ($this->isAllowed($origin)) {
$response->headers->set('Access-Control-Allow-Origin', $origin);

if ($this->supportsCredentials) {
$response->headers->set('Access-Control-Allow-Credentials', 'true');
}

if (!empty($this->exposedHeaders)) {
$response->headers->set('Access-Control-Expose-Headers', implode(', ', $this->exposedHeaders));
}
}

return $response;
}

protected function isAllowed(?string $origin): bool
{
if (!$origin) {
return false;
}

return in_array('*', $this->allowedOrigins) || in_array($origin, $this->allowedOrigins);
}
}

API 路由 CORS

路由组 CORS

1
2
3
4
5
6
7
8
// routes/api.php
Route::middleware(['cors'])->group(function () {
Route::get('/public', [PublicController::class, 'index']);
});

Route::middleware(['cors:strict'])->group(function () {
Route::get('/private', [PrivateController::class, 'index']);
});

条件 CORS

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<?php

namespace App\Http\Middleware;

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

class ConditionalCors
{
public function handle(Request $request, Closure $next, string $type = 'default'): Response
{
$config = $this->getConfig($type);

if ($request->isMethod('OPTIONS')) {
return $this->handlePreflight($request, $config);
}

$response = $next($request);

return $this->addCorsHeaders($request, $response, $config);
}

protected function getConfig(string $type): array
{
return match ($type) {
'public' => [
'origins' => ['*'],
'methods' => ['GET'],
'credentials' => false,
],
'strict' => [
'origins' => config('app.frontend_url'),
'methods' => ['GET', 'POST', 'PUT', 'DELETE'],
'credentials' => true,
],
default => config('cors'),
};
}

protected function handlePreflight(Request $request, array $config): Response
{
$response = response('', 204);

$response->headers->set('Access-Control-Allow-Origin', $config['origins'][0] ?? '*');
$response->headers->set('Access-Control-Allow-Methods', implode(', ', $config['methods']));
$response->headers->set('Access-Control-Allow-Headers', 'Content-Type, Authorization');

return $response;
}

protected function addCorsHeaders(Request $request, Response $response, array $config): Response
{
$response->headers->set('Access-Control-Allow-Origin', $config['origins'][0] ?? '*');

if ($config['credentials'] ?? false) {
$response->headers->set('Access-Control-Allow-Credentials', 'true');
}

return $response;
}
}

CORS 调试

CORS 调试中间件

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

namespace App\Http\Middleware;

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

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

if (app()->environment('local', 'testing')) {
$response->headers->set('X-CORS-Debug-Origin', $request->header('Origin', 'none'));
$response->headers->set('X-CORS-Debug-Method', $request->method());
$response->headers->set('X-CORS-Debug-Path', $request->path());
}

return $response;
}
}

CORS 测试

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
<?php

namespace Tests\Feature;

use Tests\TestCase;

class CorsTest extends TestCase
{
public function test_cors_headers_are_set()
{
$response = $this->withHeaders([
'Origin' => 'https://example.com',
])->options('/api/users');

$response->assertStatus(204);
$response->assertHeader('Access-Control-Allow-Origin', 'https://example.com');
$response->assertHeader('Access-Control-Allow-Methods');
}

public function test_preflight_request()
{
$response = $this->withHeaders([
'Origin' => 'https://example.com',
'Access-Control-Request-Method' => 'POST',
'Access-Control-Request-Headers' => 'Content-Type, Authorization',
])->options('/api/users');

$response->assertStatus(204);
$response->assertHeader('Access-Control-Allow-Origin');
$response->assertHeader('Access-Control-Allow-Methods');
$response->assertHeader('Access-Control-Allow-Headers');
}

public function test_disallowed_origin()
{
$response = $this->withHeaders([
'Origin' => 'https://malicious.com',
])->options('/api/users');

$response->assertHeaderMissing('Access-Control-Allow-Origin');
}
}

总结

Laravel 13 的 CORS 配置提供了:

  • 灵活的配置文件设置
  • 支持正则表达式匹配
  • 动态 CORS 中间件
  • 认证 Cookie 支持
  • 条件 CORS 配置
  • 完善的测试支持

正确配置 CORS 是构建安全跨域 API 的基础。