Laravel 跨域解决方案

我们在用 laravel 进行开发的时候,特别是前后端完全分离的时候,由于前端项目运行在自己机器的指定端口(也可能是其他人的机器) , 例如 localhost:8000 , 而 laravel 程序又运行在另一个端口,这样就跨域了,而由于浏览器的同源策略,跨域请求是非法的。其实这个问题很好解决,只需要添加一个中间件就可以了。

  1. 新建一个中间件
    php artisan make:middleware EnableCrossRequestMiddleware
  2. 书写中间件内容
    <?php
    namespace App\Http\Middleware;
    use Closure;
    class EnableCrossRequestMiddleware
    {
        /**
         * Handle an incoming request.
         *
         * @param  \Illuminate\Http\Request $request
         * @param  \Closure $next
         * @return mixed
         */
        public function handle($request, Closure $next)
        {
            $response = $next($request);
            $origin = $request->server('HTTP_ORIGIN') ? $request->server('HTTP_ORIGIN') : '';
            $allow_origin = [
                'http://localhost:8000',
            ];
            if (in_array($origin, $allow_origin)) {
                $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');
                $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;
        }
    }

    $allow_origin 数组变量就是你允许跨域的列表了,可自行修改。

  3. 然后在内核文件注册该中间件
        protected $middleware = [
            // more
            App\Http\Middleware\EnableCrossRequestMiddleware::class,
        ];

    App\Http\Kernel 类的 $middleware 属性添加,这里注册的中间件属于全局中间件

然后你就会发现前端页面已经可以发送跨域请求了。

会多出一次 methodoptions 的请求是正常的,因为浏览器要先判断该服务器是否允许该跨域请求。

补充

有时候返回的不是 laravel 的 response 对象而是 Symfony 的 response,所以会报 $response->header 方法找不到,所以添加 header 的方法要简单改一下, 可以拼好一个数组直接调用一次,我这里是懒得改了。

$response->headers->add(['Access-Control-Allow-Origin' => $origin]);
$response->headers->add(['Access-Control-Allow-Headers' => 'Origin, Content-Type, Cookie,X-CSRF-TOKEN, Accept,Authorization']);
$response->headers->add(['Access-Control-Expose-Headers' => 'Authorization,authenticated']);
$response->headers->add(['Access-Control-Allow-Methods' => 'GET, POST, PATCH, PUT, OPTIONS']);
$response->headers->add(['Access-Control-Allow-Credentials' => 'true']);

补充 2

另外需要注意的是,lumen 框架直接添加这个 中间件是不行的,妥妥的报 options 路由找不到,因为 lumen 用的是 fast-route 路由组件,跟 laravel 的不是同一个,laravel 可以是因为它帮你做了这件事 ,所以我们要自己注册一个 options路由, 大致代码如下:

$app->router->group([
    'prefix'     => 'api',
    'middleware' => ['cross','api'],
], function ($router) {
    $router->options('/{path:.*}', function ($path) {});
    require __DIR__ . '/../routes/api.php';
});

bootstrap/app.php

本作品采用《CC 协议》,转载必须注明作者和本文链接
本帖由 Summer 于 6年前 加精
qbhy
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 74

如果是跨域,是作者提到的思路,需要给后端api项目添加cors middleware就行;
如果前端项目html/后端项目api在同一个域下(如前端是localhost:8080,后端是localhost:8080/api/v1/accounts),可以通过nginx配置(/var/www/html是前端项目,/var/www/api是后端项目),然后启动nginx进程/php-fpm进程就能work了(这里nginx.conf把请求转发到9000端口,php-fpm.conf中也得监听这个9000端口,可以修改端口但需要保持统一):

server {
        listen       8080;
        server_name  localhost;
        root  /var/www/html/dist;

        location / {
            try_files $uri $uri/ /index.html$is_args$args;
        }

        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }

        location /api/ {
            root /var/www/api/public;
            rewrite ^/api/(.*)$ /$1 break;
            try_files $uri $uri/ /api/index.php$is_args$args;

            location ~ \.php$ {
                rewrite ^/api/(.*)$ /$1 break;
                fastcgi_pass   127.0.0.1:9000;
                fastcgi_index  index.php;
                fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
                include        fastcgi_params;
            }
        }
    }

关于对nginx的配置指令感兴趣,可以参考nginx.org官网学习下就行。

6年前 评论
Le1561651 2年前

好像没有这个说法吧?

会多出一次 method 为 options 的请求是正常的,因为浏览器要先判断该服务器是否允许该跨域请求。

6年前 评论
qbhy

@Littlesqx 这个有的,看楼上。

6年前 评论
qbhy

@cky 正解。

6年前 评论

改nginx 配置的路过

6年前 评论
qbhy

@tanjibo 不够灵活。

6年前 评论
阿麦

确实是会出现 options 请求的 感谢楼主

6年前 评论
qbhy

@GhostCoder :star:

6年前 评论
godruoyi

用这个也可以啊 老药 :smile: barryvdh/laravel-cors

6年前 评论
qbhy

@godruoyi 嗯,我看过这个包。不过也要知道一下怎么实现嘛,而且我觉得自己写个中间件更加灵活可控。

6年前 评论
Summer

可以弄成一扩展包

6年前 评论
qbhy

@Summer 哈哈,这个别人封装过了,我就不操这个心了。

6年前 评论
Toiu

我在使用中 因为用的passport认证,
所以这一项
$response->header('Access-Control-Allow-Headers', 'Origin, Content-Type, Cookie, X-CSRF-TOKEN, Accept, Authorization, X-XSRF-TOKEN');
最后一个参数接上了 ,X-Requested-With 这里标记一下

6年前 评论
qbhy

@魏文豪 不论需要添加什么请求头,都可以在这个中间件实现,稍微改动一下就可以了。

6年前 评论

666
以前也写过这个中间件,但是没搞出来怎么允许多个域名,点个赞

6年前 评论

我还是不懂得是, barryvdh/laravel-cors这个包,有什么其他功能吗?或者是做的更完善?只写一个中间件这种会不会有什么漏洞

6年前 评论
qbhy

@禹声 你可以去看看他的包用什么实现的。然而我再简单封装一下然后弄成一个包,不也是一个扩展包吗?

6年前 评论

@里暮色中 哈哈,好的好的,因为我看很多人用这个包

6年前 评论

如果是跨域,是作者提到的思路,需要给后端api项目添加cors middleware就行;
如果前端项目html/后端项目api在同一个域下(如前端是localhost:8080,后端是localhost:8080/api/v1/accounts),可以通过nginx配置(/var/www/html是前端项目,/var/www/api是后端项目),然后启动nginx进程/php-fpm进程就能work了(这里nginx.conf把请求转发到9000端口,php-fpm.conf中也得监听这个9000端口,可以修改端口但需要保持统一):

server {
        listen       8080;
        server_name  localhost;
        root  /var/www/html/dist;

        location / {
            try_files $uri $uri/ /index.html$is_args$args;
        }

        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }

        location /api/ {
            root /var/www/api/public;
            rewrite ^/api/(.*)$ /$1 break;
            try_files $uri $uri/ /api/index.php$is_args$args;

            location ~ \.php$ {
                rewrite ^/api/(.*)$ /$1 break;
                fastcgi_pass   127.0.0.1:9000;
                fastcgi_index  index.php;
                fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
                include        fastcgi_params;
            }
        }
    }

关于对nginx的配置指令感兴趣,可以参考nginx.org官网学习下就行。

6年前 评论
Le1561651 2年前
qbhy

@禹声 嗯有现成的就用现成的也挺好的。

6年前 评论
qbhy

@lx1036 nginx 确实可以实现,但是上面说过的不灵活的问题确实也比较现实,不太建议这种方案。

6年前 评论

@里暮色中 修改nginx配置并没有不灵活,相反很灵活啊。给API项目实现跨域,这是个需求,并不是所有后端API项目都需要实现跨域需求。

6年前 评论
qbhy

@lx1036 那要是允许跨域列表需要后台管理员动态增加呢?

6年前 评论
qbhy

@cjjian 晚上好

6年前 评论
qbhy

@cjjian 那也晚上好~

6年前 评论

会多出一次 method 为 options 的请求是正常的,因为浏览器要先判断该服务器是否允许该跨域请求
这个怎么解决呢??

6年前 评论
qbhy

@12pengpeng 这个不需要解决,因为这个不是bug.

6年前 评论
medz

在 Laravel 中跨域,如果想用 Laravel 程序实现,可以看看 分享:「新轮子」PHP CORS (Cross-origin resource sharing),解决 PHP 项... 这个中间件支持路由组,路由前缀等模式

6年前 评论

我这里有个更好的解决方法,只修改中间件内容即可:

public function handle($request, Closure $next)
{
    header("Access-Control-Allow-Origin: *");
    $headers = [
        'Access-Control-Allow-Methods'=> 'POST, GET, OPTIONS, PUT, DELETE',
        'Access-Control-Allow-Headers'=> 'Content-Type, X-Auth-Token, Origin'
    ];
    $response = $next($request);
    foreach($headers as $key => $value)
        $response->header($key, $value);
    return $response;
}

其他不变,这样跨域问题基本能解决

5年前 评论
qbhy

@Alwaysyouth 不建议直接用 header 函数。

5年前 评论

我在一个laravel项目的前端模板中使用ajax去请求远程api(非该项目里的api接口),提示:
has been blocked by CORS policy: Request header field X-CSRF-TOKEN is not allowed by Access-Control-Allow-Headers in preflight response
这个需要怎么搞

5年前 评论
qbhy

@blinknull 服务器不允许你携带 X-CSRF-TOKEN 请求头,跟后端说加上 Access-Control-Allow-Headers:X-CSRF-TOKEN 响应头。

5年前 评论

@96qbhy 我是在www.abc.com这个项目的前端模板中(laravel5.5项目),使用ajax,请求http://xxx.baidu.com(这个连接的服务器也是laravel项目,该项目我已经设置了跨域的一些设置,是可以跨域请求的),现在还是提示
has been blocked by CORS policy: Request header field X-CSRF-TOKEN is not allowed by Access-Control-Allow-Headers in preflight response
是不是www.abc.com这个项目也需要设置跨域的一些设置(我其实都已经做过一些设置了,还是提示这个)

5年前 评论
qbhy

@blinknull 允许跨域,但是不允许带那个请求头,看我上一个回答

5年前 评论

@96qbhy 额就是这个原因,谢谢

5年前 评论
qbhy

@blinknull 不客气,多看错误提示,基本都能从错误提示找到解决办法。

5年前 评论

请教问题,我用的dingo扩展包。然后按照楼主的方法,一直跨域失败。

        header("Access-Control-Allow-Origin: *");
        $headers = [
            'Access-Control-Allow-Methods'=> 'POST, GET, OPTIONS, PUT, DELETE',
            'Access-Control-Allow-Headers'=> 'Content-Type, X-Auth-Token, Origin'
        ];
        $response = $next($request);
        foreach($headers as $key => $value)
            $response->header($key, $value);
        return $response;

必须设置header头才可以,不知道什么原因

5年前 评论
qbhy

@isalone 当然必须设置 header 头才可以啊,而且我不建议直接用下面这种写法

 header("Access-Control-Allow-Origin: *");
5年前 评论
qbhy

@isalone 当然必须设置 header 头才可以啊,而且我不建议直接用下面这种写法

 header("Access-Control-Allow-Origin: *");
5年前 评论

难不成你们生产 环境不是用NGINX或者APACHE的???
直接在NGINX里面加不是更好,何必搞什么中间件
NGINX 加到SERVER里面:
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Headers' 'Origin, Content-Type, Cookie, X-CSRF-TOKEN, Accept, Authorization, X-XSRF-TOKEN';
add_header 'Access-Control-Expose-Headers' 'Authorization, authenticated';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PATCH, PUT, OPTIONS';
add_header 'Access-Control-Allow-Credentials' 'true';

5年前 评论
qbhy

@oking 首先 ,我只希望给自己公司的项目跨域访问权限的话,这个 'Access-Control-Allow-Origin' '*' 就不符合需求了。其次,我希望可以后台动态的增删改查这个可跨域列表的话,就不适合写到 nginx 里面去了。最后,如果我有天改了需求,前端需要跨域的同时添加一个 xx 请求头,是不是还得去 nginx 重启一下。

5年前 评论

@96qbhy 安全性考虑生产环境不会 'Access-Control-Allow-Origin' '*' , 是指定具体域名. 正常情况下这个域名列表不会经常变更, 一个公司就算N个项目,都是用一个域名,就算再添一个新域名,站运维nginx reload一下即可无缝重启WEB SERVER,现在运维工具都很先进,reload 一下所有web机器都是比较简单的事情.

5年前 评论
qbhy

@oking 嗯,有道理。所以我还是会选择中间件。

5年前 评论

@qbhy 浏览器上一直显示response headers下面的
Access-Control-Allow-Origin: *
怎么办?

5年前 评论
qbhy

@coderWaHa 不需要怎么办啊,你返回这个请求头就会显示这个请求头啊

5年前 评论

@qbhy

file
可是返回了啊

5年前 评论

@qbhy 大佬 如果异常了不走中间件 返回500 前端还是显示跨域 这个怎么搞啊

4年前 评论
qbhy

@gotophp 这确实是个问题,建议在 Handler.php 里面处理。

4年前 评论

我加了这个中间件后,并不生效啊。lv5.8版本会自动处理options请求,并且返回固定的header。这个处理cors的,注册在全局中间件里的逻辑,不生效。

4年前 评论

为什么都喜欢用laravel中间件去实现跨域呢,用代理的方式不是更简单吗?

前后分离开发一般都可以用 proxyTable 做代理,生产环境下可以用nginx做反代。

用代理实现跨域还可以少一次options。

4年前 评论
qbhy

@yanthink 用中间件实现比较灵活,不需要代理,用代理还要多一层http消耗,得不偿失。

4年前 评论

@qbhy
代理也是可配置的,没有不灵活。

跨域也有一次options请求,而且对于生产环境来说options请求是远程的,而且还得经过laravel框架,而代理可以是本地的,速度上来说代理应该会更快。

跨域请求得用绝对地址,代理可以用相对地址,换域名就可以更灵活。

配置上很多用户用中间件做跨域会遇到很多问题,而代理就不会。

4年前 评论
qbhy (楼主) 4年前
qbhy (楼主) 4年前
qbhy (楼主) 4年前
qbhy (楼主) 4年前

@qbhy

举个例子:

前端代理

proxyTable: {
    '/api': {
      target: '目标借款域名', 
      changeOrigin: true,
    },
},

或者nginx反向代理

# origin 域名配置
location ~ ^/api/ {
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_pass '目标接口域名'; 
}

假设 origin 域名 和 target 域名都在同一台机器上,那么代理就不需要远程请求。

前端用origin相对地址请求,这时如果需要换目标域名,我们只需改代理域名即可,不需要修改前端代码。

4年前 评论
qbhy (楼主) 4年前
增加一个动态的配置,只允许特定的站点
public function handle($request, Closure $next)
{
    $response = $next($request);
    $origin = $request->server('HTTP_ORIGIN') ? $request->server('HTTP_ORIGIN') : '';
    if (AccessAllow::check_access($origin)) {
        $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');
        $response->header('Access-Control-Expose-Headers', 'Authorization, authenticated');
        $response->header('Access-Control-Allow-Methods', 'GET, POST');
        $response->header('Access-Control-Allow-Credentials', 'true');
    }
    return $response;
}
我现在遇到一个问题 ,为什么这个中间件只能放在 $middleware 中 而不能放在 api 下?
'api' => [
            'throttle:60,1',
            'bindings',
            \App\Http\Middleware\EnableCrossRequestMiddleware::class,
        ],

在前后端分离的应用中,需要使用CORS完成跨域访问。在CORS中发送非简单请求时,前端会发一个请求方式为OPTIONS的预请求,前端只有收到服务器对这个OPTIONS请求的正确响应,才会发送正常的请求,否则将抛出跨域相关的错误。

源码分析

/**
 * Find the first route matching a given request.
 *
 * @param  \Illuminate\Http\Request  $request
 * @return \Illuminate\Routing\Route
 *
 * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
 */
public function match(Request $request)
{
    $routes = $this->get($request->getMethod());

    //首先,我们将看看能否为这个当前请求方法找到匹配的路由。
    //如果可以,很好,我们可以返回它,这样消费者就可以调用它。
    //否则我们将用另一个动词检查路由。
    $route = $this->matchAgainstRoutes($routes, $request);

    if (! is_null($route)) {
        return $route->bind($request);
    }

    //如果没有找到路由,我们现在将检查是否指定了匹配的路由
    // 另一个HTTP动词。如果是这样,我们将需要抛出一个方法不被认可和
    // 通知用户代理此路由应该使用哪个 HTTP verb('GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS')。
    $others = $this->checkForAlternateVerbs($request);

    if (count($others) > 0) {
        return $this->getRouteForMethods($request, $others);
    }

    throw new NotFoundHttpException;
}

这里的逻辑是:

  1. 首先根据当前HTTP方法(GET/POST/PUT/...)查找是否有匹配的路由,如果有(if(! is_null($route))条件成立),非常好,绑定后直接返回,继续此后的调用流程即可;

  2. 否则,根据$request的路由找到可能匹配的HTTP方法(即URL匹配,但是HTTP请求方式为其它品种的),如果count($others) > 0)条件成立,则继续进入$this->getRouteForMethods($request, $others);方法;

  3. 否则抛出NotFoundHttpException,即上述说到的404 NOT FOUND错误。

倘若走的是第2步,则跳转文件的234行,可看到函数逻辑为:

/**
 * Get a route (if necessary) that responds when other available methods are present.
 *
 * @param  \Illuminate\Http\Request  $request
 * @param  array  $methods
 * @return \Illuminate\Routing\Route
 *
 * @throws \Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException
 */
protected function getRouteForMethods($request, array $methods)
{
    if ($request->method() === 'OPTIONS') {
        return (new Route('OPTIONS', $request->path(), function () use ($methods) {
            return new Response('', 200, ['Allow' => implode(',', $methods)]);
        }))->bind($request);
    }

    $this->methodNotAllowed($methods, $request->method());
}

判断如果请求方式是OPTIONS,则返回状态码为200的正确响应(但是没有添加任何header信息),否则返回一个methodNotAllowed状态码为405的错误(即请求方式不允许的情况)。


因为 Laravel 这里对预请求做了处理,如果没有定义路由对 options 做处理 ,laravel 默认会直接返回 200 ,因此中间件组中定义的跨域中间件方法 EnableCrossRequestMiddleware 没有被触发之前就已经返回了相应的请求。
为了解决 options 的问题,这里在 api 路由表中增加如下代码:

Route::options('/{all}', function(Request $request, Response $response) {
    $origin = $request->header('ORIGIN', '*');
    if (AccessAllow::check_access($origin)) {
        $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');
        $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;
})->where(['all' => '([a-zA-Z0-9_]|/)+']);

跨域放在全局中就不会出现这个问题,全局中间件就是这么牛逼!

  protected $middleware = [\
       // more
       \App\Http\Middleware\EnableCrossRequestMiddleware::class,\
  ];

但是……不作不死的我就想把这个中间件放在对应的 api 组中,不然心里就是不爽,吃饭都不香……

4年前 评论
qbhy (楼主) 4年前
MichaelWyen (作者) 4年前
qbhy (楼主) 4年前

发现一个问题不知道怎么解决。是这样的: 加入日志权限问题或者其他问题导致请求接口时抛出500异常,导致 $response = $next($request) 后面的设置跨域的header头的代码没执行,没有返回正常的响应,前端就一直提示的是跨域失败,导致不能通过前端的显示定位问题。

3年前 评论
forever123 3年前
qbhy

@Tsingxu 可以try一下,欢迎加群交流 873213948

3年前 评论

大佬,我想知道为啥在 index.php文件中加上这个 为啥get post没问题,put、delete却还是报跨域呢

Laravel

3年前 评论
翟宇鑫 3年前
JeffreyBool

file

我跨域都设置好了,有的 post 可以跨域,有的一直报这个错误,

file

2年前 评论
qbhy (楼主) 2年前
JeffreyBool (作者) 2年前
qbhy (楼主) 2年前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!