之前整理过 Laravel 通过中间件解决 ajax 跨域问题 教程,但最近开发一个公众号领红包的小功能依然翻了车。

当前的允许跨域中间件

AccessControlAllowOrigin.php

    /**
     * @param $request
     * @param Closure $next
     * @return mixed
     * @throws ApiException
     */
    public function handle($request, Closure $next)
    {
        $response = $next($request);
        $origin = $request->server('HTTP_ORIGIN') ? $request->server('HTTP_ORIGIN') : '';
        Log::info('[AccessControlAllowOrigin] [current origin]' . json_encode($request->server()));
        $allow_origin = [
            'xx.com'
        ];
        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;
    }

之前的版本里允许所有域名访问,明显不安全,这次增加了 Origin 头部来源域名判断。其他增加的部分属于景上添花,header 设置主要关键点还是在 headersmethods 部分。

回到问题本身,经测试普通的接口在添加了跨域头 cors(别称)后是可以访问了,但携带了 Authorization 头部的接口浏览器会有两次请求,一次是 options 预检请求,另一次是正式请求。因为需要验证身份,所以还使用了 api 中间件,路由定义如下:

 Route::group([
        'middleware' => ['cors', 'api']
    ], function(Router $router) {
        $router->match(['get'],'/userInfo', 'UserController@UserInfo')->name('api.users.UserInfo');
    });

结果就是 options 401 未通过 api (中间调试多次,也出现过 options 请求过了,但正式请求没过,显示跨域的情况)。并且 options 请求响应头部也没有携带设置的参数,从日志显示分析请求并没有经过 cors 中间件。

这就很奇怪,明明 cors 中间件在前,api 在后。对比两个中间件发现了问题:cors 是在 $next($request) 之后处理请求,而 api 是在 $next($request) 之前处理请求;cors 是后置中间件,api 是前置中间件。也就是说一个请求过来会先执行 api 中间件,再处理请求,最后执行 cors 中间件。所以这两个中间件执行顺序是固定的。

问题就出在这个固定的顺序上,options 请求是不携带参数的,它根本通过不了 api 中间件,然后返回了 401。当时并没有注意到这一点,反复调试了几天都没搞好,搞的整个人都不自信了。

确认了问题的所在,解决方法也就简单了。之前的 Laravel 通过中间件解决 ajax 跨域问题 教程里「需要注意的事项」部分也提到了这一点,我自己给忘了。那就是单独处理 options 请求,设置允许其跨域。因为有多条这样的请求,那么可以在所有的路由请求之前定义一条通用版的 options 路由:

    Route::group([
        'middleware' => ['cors']
    ], function(Router $router) {
        Route::options('/{all}', function(\Illuminate\Http\Request $request) {
//        $origin = $request->header('ORIGIN', '*');
//        header("Access-Control-Allow-Origin: $origin");
//        header("Access-Control-Allow-Credentials: true");
//        header('Access-Control-Allow-Methods: POST, GET, OPTIONS, PUT, DELETE');
//        header('Access-Control-Allow-Headers: Origin, Access-Control-Request-Headers, SERVER_NAME, Access-Control-Allow-Headers,
//        cache-control, token, X-Requested-With, Content-Type, Accept, Connection, User-Agent, Cookie, Authorization');
        })->where(['all' => '([a-zA-Z0-9-]|/)+']);
    });

中间注释掉的部分是另外一个老哥写的路由直接处理 options 跨域的 demo,可以在不使用路由组搭配中间件的情况下单独使用。但因为调试过程中前端报错说没有返回 200 ok 状态,就没有采纳,有兴趣的小伙伴可以单独测试看看。