zoukankan      html  css  js  c++  java
  • Laravel 处理 Options 请求的原理以及批处理方案

    0. 背景

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

    这篇文章主要总结对Laravel中处理OPTIONS请求处理机制的探索,以及如何正确处理这类OPTIONS请求的解决方案。

    1. 问题描述

    Laravel处理OPTIONS方式请求的机制是个谜。

    假设我们请求的URL是http://localhost:8080/api/test,请求方式是OPTIONS

    如果请求的URL不存在相关的其它方式(如GETPOST)的请求,则会返回404 NOT FOUND的错误。

    如果存在相同URL的请求,会返回一个状态码为200的成功响应,但没有任何额外内容。

    举例而言,在路由文件routes/api.php中如果存在下面的定义,则以OPTIONS方式调用/api/test请求时,返回状态码为200的成功响应。

    Route::get('/test', 'TestController@test');
    

    但同时通过分析可以发现,这个OPTIONS请求不会进到此api路由文件的生命周期内,至少该GET请求所在路由文件api所绑定的中间件是没有进入的。

    此时如果手动添加一个OPTIONS请求,比如:

    Route::get('/test', 'TestController@test');
    Route::options('/test', function(Request $request) {
        return response('abc');
    });
    

    则至少会进入该GET请求所在路由文件api绑定的中间件,可以在相关handle函数中捕获到这个请求。

    2. 分析源码

    通过仔细查看Laravel的源码,发现了一些端倪。

    在文件vendor/laravel/framework/src/Illuminate/Routing/RouteCollection.php的第159行左右,源码内容如下:

            $routes = $this->get($request->getMethod());
    
            // First, we will see if we can find a matching route for this current request
            // method. If we can, great, we can just return it so that it can be called
            // by the consumer. Otherwise we will check for routes with another verb.
            $route = $this->matchAgainstRoutes($routes, $request);
    
            if (! is_null($route)) {
                return $route->bind($request);
            }
    
            // If no route was found we will now check if a matching route is specified by
            // another HTTP verb. If it is we will need to throw a MethodNotAllowed and
            // inform the user agent of which HTTP verb it should use for this route.
            $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行,可看到函数逻辑为:

        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);
        }
    

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

    此处Laravel针对OPTIONS方式的HTTP请求处理方式已经固定了,这样就有点头疼,不知道在哪里添加代码针对OPTIONS请求的header进行处理。最笨的方法是对跨域请求的每一个GETPOST请求都撰写一个同名的OPTIONS类型的路由。

    3. 解决办法

    解决方案有两种,一种是添加中间件,一种是使用通配路由匹配方案。

    总体思想都是在系统处理OPTIONS请求的过程中添加相关header信息。

    3.1 中间件方案

    在文件app/Http/Kernel.php中,有两处可以定义中间件。

    第一处是总中间件$middleware,任何请求都会通过这里;第二处是群组中间件middlewareGroups,只有路由匹配上对应群组模式的才会通过这部分。

    这是总中间件$middleware的定义代码:

        protected $middleware = [
            IlluminateFoundationHttpMiddlewareCheckForMaintenanceMode::class,
            IlluminateFoundationHttpMiddlewareValidatePostSize::class,
            AppHttpMiddlewareTrimStrings::class,
            IlluminateFoundationHttpMiddlewareConvertEmptyStringsToNull::class,
            AppHttpMiddlewareTrustProxies::class,
        ];
    

    这是群组中间件$middlewareGroups的定义代码:

        /**
        * The application's route middleware groups.
        *
        * @var array
        */
        protected $middlewareGroups = [
            'web' => [
                AppHttpMiddlewareEncryptCookies::class,
                IlluminateCookieMiddlewareAddQueuedCookiesToResponse::class,
                IlluminateSessionMiddlewareStartSession::class,
                // IlluminateSessionMiddlewareAuthenticateSession::class,
                IlluminateViewMiddlewareShareErrorsFromSession::class,
                AppHttpMiddlewareVerifyCsrfToken::class,
                IlluminateRoutingMiddlewareSubstituteBindings::class,
            ],
            'api' => [
                'throttle:60,1',
                'bindings',
                IlluminateSessionMiddlewareStartSession::class,
            ],
        ];
    

    由于群组路由中间件是在路由匹配过程之后才进入,因此之前实验中提及的OPTIONS请求尚未通过此处中间件的handle函数,就已经返回了。

    因此我们添加的中间件,需要添加到$middleware数组中,不能添加到api群组路由中间件中。

    app/Http/Middleware文件夹下新建PreflightResponse.php文件:

    <?php
    
    namespace AppHttpMiddleware;
    use Closure;
    class PreflightResponse
    {
        /**
        * Handle an incoming request.
        *
        * @param  IlluminateHttpRequest  $request
        * @param  Closure  $next
        * @param  string|null  $guard
        * @return mixed
        */
        public function handle($request, Closure $next, $guard = null)
        {
            if($request->getMethod() === 'OPTIONS'){
                $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, X-XSRF-TOKEN');
            }
            return $next($request);
        }
    }
    

    其中这里针对OPTIONS请求的处理内容是添加多个header内容,可根据实际需要修改相关处理逻辑:

    $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, X-XSRF-TOKEN');
    

    至此,所有OPTIONS方式的HTTP请求都得到了相关处理。

    3.2 通配路由匹配方案

    如果不使用中间件,查询Laravel官方文档Routing,可知如何在路由中使用正则表达式进行模式匹配。

    Route::get('user/{id}/{name}', function ($id, $name) {
        //
    })->where(['id' => '[0-9]+', 'name' => '[a-z]+']);
    

    类似的,可以撰写针对OPTIONS类型请求的泛化处理路由条件:

    Route::options('/{all}', function(Request $request) {
         return response('options here!');
    })->where(['all' => '([a-zA-Z0-9-]|/)+']);
    

    *注:这里正则表达式中不能使用符号*

    因此,针对跨域问题,对于OPTIONS方式的请求可以撰写如下路由响应:

    Route::options('/{all}', function(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');
    })->where(['all' => '([a-zA-Z0-9-]|/)+']);
    

    这样所有的OPTIONS请求都能找到匹配的路由,在此处可统一处理所有OPTIONS请求,不需要额外进行处理。

    4. 参考链接

    The PHP Framework For Web Artisanslaravel.com

    https://medium.com/@neo/handling-xmlhttprequest-options-pre-flight-request-in-laravel-a4c4322051b9medium.com

  • 相关阅读:
    AcWing 157. 树形地铁系统 (hash判断树同构)打卡
    AcWing 156. 矩阵 (哈希二维转一维查询)打卡
    AcWing 144. 最长异或值路径 01字典树打卡
    AcWing 143. 最大异或对 01字典树打卡
    AcWing 142. 前缀统计 字典树打卡
    AcWing 139. 回文子串的最大长度 hash打卡
    AcWing 138. 兔子与兔子 hash打卡
    常用C库函数功能及用法
    编程实现C库函数
    C语言面试题5
  • 原文地址:https://www.cnblogs.com/mouseleo/p/8427669.html
Copyright © 2011-2022 走看看