Laravel 处理 Options 请求的原理以及批处理方案
2018-02-07 17:27
866 查看
0. 背景
在前后端分离的应用中,需要使用
CORS完成跨域访问。在
CORS中发送
非简单请求时,前端会发一个请求方式为
OPTIONS的预请求,前端只有收到服务器对这个
OPTIONS请求的正确响应,才会发送正常的请求,否则将抛出跨域相关的错误。
这篇文章主要总结对Laravel中处理
OPTIONS请求处理机制的探索,以及如何正确处理这类
OPTIONS请求的解决方案。
1. 问题描述
Laravel处理
OPTIONS方式请求的机制是个谜。
假设我们请求的URL是
http://localhost:8080/api/test,请求方式是
OPTIONS。
如果请求的URL不存在相关的其它方式(如
GET或
POST)的请求,则会返回
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进行处理。最笨的方法是对跨域请求的每一个
GET或
POST请求都撰写一个同名的
OPTIONS类型的路由。
3. 解决办法
解决方案有两种,一种是添加中间件,一种是使用通配路由匹配方案。
总体思想都是在系统处理
OPTIONS请求的过程中添加相关
header信息。
3.1 中间件方案
在文件
app/Http/Kernel.php中,有两处可以定义中间件。
第一处是总中间件
$middleware,任何请求都会通过这里;第二处是群组中间件
middlewareGroups,只有路由匹配上对应群组模式的才会通过这部分。
这是总中间件
$middleware的定义代码:
protected $middleware = [ \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class, \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class, \App\Http\Middleware\TrimStrings::class, \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, \App\Http\Middleware\TrustProxies::class, ];
这是群组中间件
$middlewareGroups的定义代码:
/** * The application's route middleware groups. * * @var array */ protected $middlewareGroups = [ 'web' => [ \App\Http\Middleware\EncryptCookies::class, \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, \Illuminate\Session\Middleware\StartSession::class, // \Illuminate\Session\Middleware\AuthenticateSession::class, \Illuminate\View\Middleware\ShareErrorsFromSession::class, \App\Http\Middleware\VerifyCsrfToken::class, \Illuminate\Routing\Middleware\SubstituteBindings::class, ], 'api' => [ 'throttle:60,1', 'bindings', \Illuminate\Session\Middleware\StartSession::class, ], ];
由于群组路由中间件是在路由匹配过程之后才进入,因此之前实验中提及的
OPTIONS请求尚未通过此处中间件的
handle函数,就已经返回了。
因此我们添加的中间件,需要添加到
$middleware数组中,不能添加到
api群组路由中间件中。
在
app/Http/Middleware文件夹下新建
PreflightResponse.php文件:
<?php namespace App\Http\Middleware; use Closure; class PreflightResponse { /** * Handle an incoming request. * * @param \Illuminate\Http\Request $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相关文章推荐
- Laravel + Vue 之 OPTIONS 请求的处理
- tomcat 讲解 原理以及请求处理过程
- I/O模型和原理,以及简单解析客户端请求WEB服务器内部处理过程,MPM三种模
- Servlet容器 - Tomcat请求处理原理
- Retrofit请求数据对错误以及网络异常的处理
- URL传参中不能带特殊的字符以及处理方案
- URL传参中不能带特殊的字符以及处理方案
- tomcat原理解析(五):http请求处理
- ASP.NET网页请求以及处理全过程(反编译工具查看源代码)
- Exchange2013提示“出现意外错误,无法处理您的请求”处理方案
- IIS 处理请求 原理
- vysor原理以及Android同屏方案
- Extjs6关于Ajax和form表单提交以及store请求session超时的处理方法
- struts2原理及请求处理核心代码
- macbook pro进水紧急处理方案以及维修建议
- android:imeOptions属性详解以及无效处理
- Rose双机热备两款软件原理介绍以及共享存储双机热备方案和镜像双机热备方案介绍
- Retrofit请求数据对错误以及网络异常的处理
- 第八节:Task的各类Task<TResult>返回值以及通用线程的异常处理方案。
- Android 网络请求超时处理方案