Laravel 处理 Options 请求的原理以及批处理方案
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 处理 Options 请求的原理以及批处理方案的更多相关文章
- ( 转 ) CORS 有一次 OPTIONS 请求的原理
刚接触前端的时候,以为HTTP的Request Method只有GET与POST两种,后来才了解到,原来还有HEAD.PUT.DELETE.OPTIONS-- 目前的工作中,HEAD.PUT.DELE ...
- Laravel + Vue 之 OPTIONS 请求的处理
问题: 在 Vue 对后台的请求中,一般采用 axios 对后台进行 Ajax 交互. 交互发生时,axios 一般会发起两次请求,一次为 Options 试探请求,一次为正式请求. 由此带来的问题是 ...
- AJAX 请求中多出了一次 OPTIONS 请求 导致 Laravel 中间件无法对 Header 传入的 Token 无法获取
背景知识: 我们会发现,在很多post,put,delete等请求之前,会有一次options请求.本文主要是来讨论一下这是什么原因引起的. 根本原因就是,W3C规范这样要求了!在跨域请求中,分为简单 ...
- AJAX请求中出现OPTIONS请求
背景 有一个前后端分离的VUE项目来发送ajax请求, 查看Nginx日志或使用Chrome Dev Tools查看请求发送情况时, 会看到每次调后台API的请求之前, 都会发送一个OPTIONS请求 ...
- [1.6W字]浏览器跨域请求的原理, 以及解决方法(可以纯前端实现) #flight.Archives011
Title/ 浏览器跨域(CrossOrigin)请求的原理, 以及解决方案详细指南 #flight.Archives011 序: 最近看到又有一波新的创作活动了, 官方给出的话题中有一个" ...
- [转]Laravel 4之请求
Laravel 4之请求 http://dingjiannan.com/2013/laravel-request/ 获取请求数据 获取当前请求所包括的所有GET和POST数据 Route::get(' ...
- 为什么会有OPTIONS请求
在做项目时,很多时候发送一个post请求,是先发送一个option请求,然后再发送post请求,一直这么用之前也没有仔细思考,今天有时间,好好了解一下为什么会多一次请求. 疑问1:什么是options ...
- jquery ajax 请求中多出现一次OPTIONS请求及其解决办法
http://www.tangshuang.net/2271.html 在上一篇<服务端php解决jquery ajax跨域请求restful api问题及实践>中,我简单介绍了如何通过服 ...
- 详解Ajax请求(一)前言——同步请求的原理
我们知道,ajax是一种异步请求的方式,想要了解异步请求,就必须要先从同步请求说起.常见的同步请求的方式是form表单的提交,我们先从一种同步请求的示例说起. 我们希望输入姓名可以从后台得到身份证号. ...
随机推荐
- UVA-755-排序
奇怪,我怎么还有一个排序题目没过 题意如下: 公司喜欢有难忘的电话号码,一个让电话号码变得难忘的方式是有一个拼读起来难忘的单词,比如,你可以呼叫University of Waterloo通过拨打难忘 ...
- 可视化库-seaborn-多变量分析绘图(第五天)
1. sns.stripplot(x='data', y='total_bill', data=tips, jitter=True), 画出竖形的样子,jitter=True为了使得数据尽量分开 im ...
- UI5-文档-4.14-Custom CSS and Theme Colors
有时我们需要定义一些更细粒度的布局,这时我们可以通过向控件添加自定义样式类来使用CSS的灵活性,并根据自己的喜好对它们进行样式化. Preview The space between the butt ...
- UI5-文档-4.9-Component Configuration
在我们介绍了模型-视图-控制器(MVC)概念的所有三个部分之后,现在我们将讨论SAPUI5的另一个重要的结构方面. 在这一步中,我们将把所有UI资产封装在一个独立于索引的组件中.html文件.组件是S ...
- UI5-文档-2-开发环境
这一部分将指导您安装.配置和设置SAPUI5开发环境的最常见和推荐用例. 请注意:您可以在不同的平台上使用SAPUI5.各自平台的许可和维护条件也适用于SAPUI5.例如,如果在SAP云平台上使用SA ...
- MySQL 逻辑备份工具
简介: Mydumper.Myloader 是一个第三方的.开源的 MySQL 逻辑备份工具. 支持多线程,比起 mysqldump 要快很多,也能解决 innobackupex 备份工具对 MyIS ...
- python-股票数据定向爬取
re.findall soup.find_all ---------Q---- for i in ***: ***可以是什么类型,主要是关心什么类型的不可以 ------------trackback ...
- python中时间差中seconds和total_seconds
在python中经常会用到计算两个时间差,两个日期类型进行相减可以获取到时间差 经常会使用seconds来获取,其实seconds获取的是时间差的秒数,遗漏了天 seconds是获取时间部分的差值,而 ...
- Java Thread.interrupt interrupted
Java Thread.interrupt @(Base)[JDK, 线程, interrupt] 原文地址,转载请注明 下面这个场景你可能很熟悉,我们调用Thread.sleep(),conditi ...
- python文件的只读,只写操作
只读:r rb(bytes类型数据) 只写:w wb(bytes类型数据) 在文件最后追加: f = open('log',mode='a',encoding='utf-8') f.write('这里 ...