Become a sponsor

概述
CorsMiddleware 处理跨域请求,支持通过 .env 动态配置允许的来源域名。配置使用逗号分隔的字符串,中间件将其 explode 为数组后做白名单匹配。开发环境可设为 * 允许所有来源,生产环境应指定具体域名。
浏览器的同源策略限制了从一个源(协议+域名+端口)去请求另一个源的资源。当源不同时,就产生了跨域。
同源请求(允许):
页面:https://admin.example.com/page
API: https://admin.example.com/api/user
→ 协议+域名+端口相同,同源
跨域请求(被浏览器阻止):
页面:https://admin.example.com/page
API: https://api.example.com/user
→ 域名不同,跨域
页面:http://admin.example.com/page
API: https://admin.example.com/api/user
→ 协议不同,跨域
页面:https://admin.example.com:80/page
API: https://admin.example.com:8080/api/user
→ 端口不同,跨域简单请求(Simple Request):
满足以下条件的 GET/POST/HEAD 请求:
- Content-Type 为 text/plain、multipart/form-data、application/x-www-form-urlencoded
- 无自定义请求头
→ 浏览器直接发送请求,附带 Origin 头
→ 服务端返回 CORS 头,浏览器判断是否允许
预检请求(Preflight Request):
不满足简单请求条件时(如 PUT/DELETE、JSON 请求体、自定义头):
→ 浏览器先发送 OPTIONS 请求询问服务端
→ 服务端返回允许的方法和头
→ 浏览器再发送实际请求┌──────────┐ ┌──────────┐
│ 浏览器 │ │ 服务器 │
│ (前端) │ │ (后端) │
└────┬─────┘ └────┬─────┘
│ │
│ ─── 1. OPTIONS 预检请求 ──────────────►│
│ Origin: https://admin.example.com │
│ Access-Control-Request-Method: PUT │
│ Access-Control-Request-Headers: │
│ Content-Type, Authorization │
│ │
│ ┌──────────┴──────────┐
│ │ CorsMiddleware │
│ │ 1. 检查 Origin 白名单│
│ │ 2. 返回 CORS 头 │
│ │ 3. 返回 204 空响应 │
│ └──────────┬──────────┘
│ │
│ ◄── 2. 预检响应 ─────────────────────│
│ 204 No Content │
│ Access-Control-Allow-Origin: │
│ https://admin.example.com │
│ Access-Control-Allow-Methods: │
│ GET, POST, PUT, DELETE │
│ Access-Control-Allow-Headers: │
│ Content-Type, Authorization │
│ Access-Control-Max-Age: 3600 │
│ │
│ ─── 3. 实际请求 ─────────────────────►│
│ PUT /api/user/update │
│ Origin: https://admin.example.com │
│ Authorization: Bearer <token> │
│ Content-Type: application/json │
│ │
│ ┌──────────┴──────────┐
│ │ 中间件链 → 控制器 │
│ └──────────┬──────────┘
│ │
│ ◄── 4. 实际响应 ─────────────────────│
│ 200 OK │
│ Access-Control-Allow-Origin: │
│ https://admin.example.com │
│ { "code": 0, "data": {...} } │
│ │; .env
; 开发环境:允许所有来源
[CORS]
ALLOW_ORIGIN = *
; 生产环境:指定具体域名(多个用逗号分隔)
[CORS]
ALLOW_ORIGIN = https://admin.example.com,https://www.example.com
; 多域名示例
[CORS]
ALLOW_ORIGIN = https://admin.example.com,https://m.example.com,https://app.example.com// config/cors.php
return [
// 允许的来源域名,逗号分隔,* 表示允许所有
// 注意:配置含 * 时中间件会回显请求的 Origin,而非直接返回通配符,
// 以兼容携带凭证时浏览器不允许 Allow-Origin 为 * 的限制
'allow_origin' => explode(',', env('cors.allow_origin', '*')),
// 允许的请求方法
'allow_methods' => 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
// 允许的请求头
'allow_headers' => 'Content-Type, Authorization, X-Requested-With, Accept, Origin, Token',
// 暴露给前端的响应头,便于前端读取
'expose_headers' => 'Authorization',
// 预检请求(OPTIONS)结果的缓存时间,单位秒
'max_age' => 3600,
// 是否允许携带凭证(Cookie 等)
'allow_credentials' => true,
];| 配置项 | 说明 | 默认值 | 生产建议 |
|---|---|---|---|
allow_origin | 允许的来源域名 | * | 指定具体域名 |
allow_methods | 允许的 HTTP 方法 | GET, POST, PUT, DELETE, PATCH, OPTIONS | 按需精简 |
allow_headers | 允许的请求头 | 见配置文件 | 按需精简 |
expose_headers | 暴露给前端的响应头 | Authorization | 按需添加 |
max_age | 预检缓存时间(秒) | 3600 | 可适当增大 |
allow_credentials | 是否允许携带凭证 | true | 前端使用 Cookie 时必须为 true |
// app/middleware/CorsMiddleware.php
public function handle(Request $request, \Closure $next): Response
{
$origin = $request->header('Origin', '');
$config = config('cors');
$allowed = $config['allow_origin'] ?? ['*'];
// 判断 origin 是否允许:白名单匹配
if (in_array('*', $allowed)) {
// 配置含 * 时,回显请求 Origin(无 Origin 则用 *)
$allowOrigin = $origin ?: '*';
} else {
// 仅在白名单内才回显 Origin,不在白名单则置空
$allowOrigin = in_array($origin, $allowed) ? $origin : '';
}
$header = [
'Access-Control-Allow-Origin' => $allowOrigin,
'Access-Control-Allow-Credentials' => $config['allow_credentials'] ? 'true' : 'false',
'Access-Control-Allow-Methods' => $config['allow_methods'],
'Access-Control-Allow-Headers' => $config['allow_headers'],
'Access-Control-Expose-Headers' => $config['expose_headers'],
'Access-Control-Max-Age' => (string)$config['max_age'],
];
// OPTIONS 预检请求直接返回 204
if ($request->isOptions()) {
return response('', 204, $header);
}
$response = $next($request);
// 添加跨域头
$response->header($header);
return $response;
}配置通过 explode(',', env(...)) 将逗号分隔的字符串转为数组,中间件使用 in_array() 进行精确匹配:
请求 Origin: https://admin.example.com
白名单数组: ['https://admin.example.com', 'https://www.example.com']
→ in_array() 匹配成功,回显 Origin ✓
请求 Origin: https://evil.com
白名单数组: ['https://admin.example.com', 'https://www.example.com']
→ in_array() 匹配失败,$allowOrigin = '' ✗
→ 浏览器收到空的 Allow-Origin,阻止前端读取响应当配置为 * 时,explode(',', '*') 返回 ['*']。中间件检测到 in_array('*', $allowed) 后,会回显请求的 Origin 而非直接返回 *,这是为了兼容浏览器在携带凭证(Cookie)时不允许 Access-Control-Allow-Origin: * 的限制。
配置: ALLOW_ORIGIN = *
请求 Origin: https://admin.example.com
中间件行为:
in_array('*', ['*']) → true
$allowOrigin = $origin(回显 'https://admin.example.com')
响应头:
Access-Control-Allow-Origin: https://admin.example.com ✓
Access-Control-Allow-Credentials: true ✓
如果直接返回 *:
Access-Control-Allow-Origin: * ✗
Access-Control-Allow-Credentials: true ✗
→ 浏览器报错:Allow-Origin 为 * 时不能携带凭证浏览器在跨域请求前会先发送 OPTIONS 预检请求,中间件通过 $request->isOptions() 判断后,直接返回 response('', 204, $header) 空响应,避免进入业务逻辑。
OPTIONS /api/user/update HTTP/1.1
Origin: https://admin.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type, Authorization
→ 中间件拦截,返回 204 + CORS 头
→ 不进入 AuthMiddleware / Controller请求进入
│
▼
CorsMiddleware(全局,第一个执行)
│ OPTIONS → 直接返回 204 + CORS 头
│ 普通请求 → 设置 CORS 头,继续
▼
AuthMiddleware
│ 校验 JWT + 权限
▼
TenantMiddleware
│ 注入租户上下文
▼
Controller执行顺序
CorsMiddleware 必须是第一个执行的中间件,确保 OPTIONS 预检请求在认证之前就被处理,否则预检请求会因缺少 Token 而被 AuthMiddleware 拒绝。
原因: 请求的 Origin 不在白名单中。
解决:
# 检查 .env 中的 ALLOW_ORIGIN 是否包含前端的域名
[CORS]
ALLOW_ORIGIN = https://admin.example.com原因: 浏览器规定 Access-Control-Allow-Origin: * 时不能携带 Cookie。
解决: 中间件已自动处理——配置为 * 时回显请求 Origin 而非返回通配符。如果仍报错,检查前端是否设置了 withCredentials: true。
原因: OPTIONS 请求被 AuthMiddleware 拦截。
解决: 确保 CorsMiddleware 在 AuthMiddleware 之前执行。检查中间件注册顺序:
// app/middleware.php(全局中间件)
return [
\app\middleware\CorsMiddleware::class, // 必须在最前面
];原因: 请求头不在 allow_headers 列表中。
解决:
// config/cors.php
'allow_headers' => 'Content-Type, Authorization, X-Requested-With, Accept, Origin, Token, X-Custom-Header',原因: 响应头不在 expose_headers 列表中。
解决:
// config/cors.php
'expose_headers' => 'Authorization, X-Total-Count, X-Page-Count',[CORS]
ALLOW_ORIGIN = *允许所有来源,方便前后端分离开发(前端 localhost:3000,后端 localhost:8080)。
[CORS]
ALLOW_ORIGIN = https://test.admin.example.com指定测试域名,提前验证跨域配置。
[CORS]
ALLOW_ORIGIN = https://admin.example.com,https://www.example.com只允许正式域名,不使用 *。
| 响应头 | 说明 | 示例值 |
|---|---|---|
Access-Control-Allow-Origin | 允许的来源 | https://admin.example.com |
Access-Control-Allow-Methods | 允许的方法 | GET, POST, PUT, DELETE |
Access-Control-Allow-Headers | 允许的请求头 | Content-Type, Authorization |
Access-Control-Expose-Headers | 暴露的响应头 | Authorization |
Access-Control-Max-Age | 预检缓存时间 | 3600 |
Access-Control-Allow-Credentials | 是否允许凭证 | true |