您的位置:首页 > 编程语言 > PHP开发

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

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

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: