做 API 开发时,跨域几乎是绕不开的问题,尤其是前后端分离后,接口和页面往往不在同一个域名下。Laravel 里处理跨域并不复杂,但关键在于先判断该把规则放在 Nginx,还是放在应用层;再看是否需要处理 axios 发起 POST 时连带出现的 OPTIONS 预检请求。下面按这两个判断路径展开,保留原有配置和代码,方便直接对照落地。
为什么会出现跨域限制
跨域本质上不是 Laravel 的问题,而是浏览器的同源策略在起作用。出于安全原因,浏览器会限制 Script 中的跨域请求。由于 XMLHttpRequest 遵循同源策略,所有使用 XMLHttpRequest 构造 HTTP 请求的应用,默认只能访问自己的域名。
这也是为什么前端页面明明能正常打开,但一调用另一个域名下的 API,就会在浏览器里报跨域错误。要让请求通过,服务端必须返回浏览器认可的跨域响应头。
先选方案:改 Nginx 还是改 Laravel
常见处理办法主要有两种,一种在 Web 服务器层解决,另一种在 PHP 应用层解决。两种都能生效,但适用场景不同。

方案一:直接在 Nginx 中放开跨域
如果你有服务器配置权限,而且希望站点统一允许跨域,直接修改 Nginx 是最省事的做法。它的优点是改完即可全局生效,不需要动业务代码。
可以在站点配置节点下加入以下内容:
server
{
listen 80;
index index.html index.htm index.php;
root /www/public/;
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Headers "x-requested-with,Authorization,Accept, Origin, X-Requested-With, Content-Type, Last-Modified";
add_header Access-Control-Allow-Methods GET,POST;
add_header Access-Control-Max-Age 1728000;
# ....
}
这种写法适合统一出口的接口服务,但它也有明显限制:一是控制粒度偏粗,通常是全局放开;二是只能按服务器配置处理,不容易精确到某一组路由或单个接口。对安全要求更细的项目,这种方式未必合适。
方案二:在 Laravel 里用中间件控制
如果你没有 Nginx 修改权限,或者只想让部分接口允许跨域,那么把逻辑放进 Laravel 中间件会更灵活。这样可以按路由组、接口前缀,甚至按来源域名做精细控制。
原文给出的思路是先读取请求头中的 HTTP_ORIGIN,再解析出来源主机与端口,只对白名单中的来源返回跨域头。中间件代码如下:
<?php
namespace AppHttpMiddleware;
use Closure;
class EnableCrossRequest
{
/**
* Handle an incoming request.
*
* @param IlluminateHttpRequest $request
* @param Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
$origin = $request->server('HTTP_ORIGIN') ? $request->server('HTTP_ORIGIN') : '';
$originHost = parse_url($origin, PHP_URL_HOST);
$originPort = parse_url($origin, PHP_URL_PORT);
$originHost .= $originPort ? ':' . $originPort : '';
// 允许跨域的域名 可以加在配置里
$allowOriginHost = [
'test.com',
];
$response = $next($request);
if (in_array($originHost, $allowOriginHost)) {
$response->header('Access-Control-Allow-Origin', $origin);
$response->header('Access-Control-Allow-Headers', 'Origin, Content-Type, Cookie, X-CSRF-TOKEN, Accept, Authorization, X-XSRF-TOKEN, Last-Modified');
$response->header('Access-Control-Expose-Headers', 'Authorization, authenticated');
$response->header('Access-Control-Allow-Methods', 'GET, POST, PATCH, PUT, OPTIONS');
$response->header('Access-Control-Allow-Credentials', 'true');
}
return $response;
}
}
这里有几个关键点值得注意:
Access-Control-Allow-Origin没有直接写成*,而是回写当前请求的来源地址,这更适合配合白名单使用。Access-Control-Allow-Credentials被设置为true,说明这套配置考虑了带凭证的跨域请求场景。- 允许的方法里包含
GET, POST, PATCH, PUT, OPTIONS,为预检请求预留了空间。
如果你的项目需要按环境切换来源域名,文中的注释也已经给出方向:白名单可以进一步抽到配置文件中,而不是直接写死在中间件里。
中间件怎么注册并应用到路由
中间件写好之后,还要在 Laravel 里完成注册,否则路由不会实际使用这段跨域逻辑。
先在 app/Http/Kernel.php 中注册中间件组:
protected $middlewareGroups = [
// ....
'api.cross' => [
AppHttpMiddlewareEnableCrossRequest::class,
],
// ....
];
然后,把它挂到需要跨域的路由组上:
Route::group([
'prefix' => 'api',
'middleware' => ['api.cross'],
], function () {
// 路由
});
这种做法的价值在于,你不必把整站接口都暴露出去。只给 api 这类明确的接口组启用跨域,既能满足前端调用,又比全局放开更容易收敛风险。
axios 发送 POST,为什么会先变成 OPTIONS
很多人第一次排查跨域时会误以为接口被请求了两次,或者怀疑 Laravel 路由重复执行。实际上,跨域场景下只要命中了复杂请求条件,浏览器就可能先发一个 OPTIONS 请求做预检,用来确认服务端是否允许真正的跨域调用。

所以,出现“先 OPTIONS,再真正的 POST”并不反常。原文也明确提到:有两次请求是完全正常的,但如果业务代码把预检请求也当成正式请求处理,就可能产生重复逻辑或无意义消耗。
一种直接的处理方式,是单独为 OPTIONS 增加路由,快速返回空响应:
Route::options('/{all}', function () {
return response('');
})->where(['all' => '([a-zA-Z0-9-]|_|/)+']);
这样做的核心目标不是“取消”预检,而是让预检请求尽快结束,不进入真正的业务处理流程。对于前后端分离项目,这往往能减少接口排错时的干扰。
实际落地时该怎么选
如果你拥有服务器配置权限、接口范围也比较统一,Nginx 方案部署快、维护简单;如果你需要只开放部分接口,或者要按来源域名做细粒度判断,Laravel 中间件更适合。
可以按下面的思路判断:
- 追求统一、全局生效:优先考虑 Nginx。
- 需要只开放某些路由:优先考虑 Laravel 中间件。
- 没有服务器权限:基本只能走应用层方案。
- 前端使用 axios 且出现两次请求:先确认是否为正常的
OPTIONS预检,再决定是否补一个快速响应路由。
从安全性和灵活性来看,把跨域能力精确绑定到具体路由,通常比整站一把放开更稳妥。对 Laravel 项目来说,中间件方案也更符合框架自身的组织方式,后续维护会更清楚。









