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的成功响应。

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

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

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

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

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

2. 分析源码

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

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

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

  1. protected function getRouteForMethods($request, array $methods)
  2. {
  3. if ($request->method() == 'OPTIONS') {
  4. return (new Route('OPTIONS', $request->path(), function () use ($methods) {
  5. return new Response('', 200, ['Allow' => implode(',', $methods)]);
  6. }))->bind($request);
  7. }
  8. $this->methodNotAllowed($methods);
  9. }

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

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

3. 解决办法

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

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

3.1 中间件方案

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

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

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

  1. protected $middleware = [
  2. \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
  3. \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
  4. \App\Http\Middleware\TrimStrings::class,
  5. \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
  6. \App\Http\Middleware\TrustProxies::class,
  7. ];

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

  1. /**
  2. * The application's route middleware groups.
  3. *
  4. * @var array
  5. */
  6. protected $middlewareGroups = [
  7. 'web' => [
  8. \App\Http\Middleware\EncryptCookies::class,
  9. \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
  10. \Illuminate\Session\Middleware\StartSession::class,
  11. // \Illuminate\Session\Middleware\AuthenticateSession::class,
  12. \Illuminate\View\Middleware\ShareErrorsFromSession::class,
  13. \App\Http\Middleware\VerifyCsrfToken::class,
  14. \Illuminate\Routing\Middleware\SubstituteBindings::class,
  15. ],
  16. 'api' => [
  17. 'throttle:60,1',
  18. 'bindings',
  19. \Illuminate\Session\Middleware\StartSession::class,
  20. ],
  21. ];

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

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

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

  1. <?php
  2. namespace App\Http\Middleware;
  3. use Closure;
  4. class PreflightResponse
  5. {
  6. /**
  7. * Handle an incoming request.
  8. *
  9. * @param \Illuminate\Http\Request $request
  10. * @param \Closure $next
  11. * @param string|null $guard
  12. * @return mixed
  13. */
  14. public function handle($request, Closure $next, $guard = null)
  15. {
  16. if($request->getMethod() === 'OPTIONS'){
  17. $origin = $request->header('ORIGIN', '*');
  18. header("Access-Control-Allow-Origin: $origin");
  19. header("Access-Control-Allow-Credentials: true");
  20. header('Access-Control-Allow-Methods: POST, GET, OPTIONS, PUT, DELETE');
  21. 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');
  22. }
  23. return $next($request);
  24. }
  25. }

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

  1. $origin = $request->header('ORIGIN', '*');
  2. header("Access-Control-Allow-Origin: $origin");
  3. header("Access-Control-Allow-Credentials: true");
  4. header('Access-Control-Allow-Methods: POST, GET, OPTIONS, PUT, DELETE');
  5. 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,可知如何在路由中使用正则表达式进行模式匹配。

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

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

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

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

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

  1. Route::options('/{all}', function(Request $request) {
  2. $origin = $request->header('ORIGIN', '*');
  3. header("Access-Control-Allow-Origin: $origin");
  4. header("Access-Control-Allow-Credentials: true");
  5. header('Access-Control-Allow-Methods: POST, GET, OPTIONS, PUT, DELETE');
  6. 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');
  7. })->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

Laravel 处理 Options 请求的原理以及批处理方案的更多相关文章

  1. ( 转 ) CORS 有一次 OPTIONS 请求的原理

    刚接触前端的时候,以为HTTP的Request Method只有GET与POST两种,后来才了解到,原来还有HEAD.PUT.DELETE.OPTIONS-- 目前的工作中,HEAD.PUT.DELE ...

  2. Laravel + Vue 之 OPTIONS 请求的处理

    问题: 在 Vue 对后台的请求中,一般采用 axios 对后台进行 Ajax 交互. 交互发生时,axios 一般会发起两次请求,一次为 Options 试探请求,一次为正式请求. 由此带来的问题是 ...

  3. AJAX 请求中多出了一次 OPTIONS 请求 导致 Laravel 中间件无法对 Header 传入的 Token 无法获取

    背景知识: 我们会发现,在很多post,put,delete等请求之前,会有一次options请求.本文主要是来讨论一下这是什么原因引起的. 根本原因就是,W3C规范这样要求了!在跨域请求中,分为简单 ...

  4. AJAX请求中出现OPTIONS请求

    背景 有一个前后端分离的VUE项目来发送ajax请求, 查看Nginx日志或使用Chrome Dev Tools查看请求发送情况时, 会看到每次调后台API的请求之前, 都会发送一个OPTIONS请求 ...

  5. [1.6W字]浏览器跨域请求的原理, 以及解决方法(可以纯前端实现) #flight.Archives011

    Title/ 浏览器跨域(CrossOrigin)请求的原理, 以及解决方案详细指南 #flight.Archives011 序: 最近看到又有一波新的创作活动了, 官方给出的话题中有一个" ...

  6. [转]Laravel 4之请求

    Laravel 4之请求 http://dingjiannan.com/2013/laravel-request/ 获取请求数据 获取当前请求所包括的所有GET和POST数据 Route::get(' ...

  7. 为什么会有OPTIONS请求

    在做项目时,很多时候发送一个post请求,是先发送一个option请求,然后再发送post请求,一直这么用之前也没有仔细思考,今天有时间,好好了解一下为什么会多一次请求. 疑问1:什么是options ...

  8. jquery ajax 请求中多出现一次OPTIONS请求及其解决办法

    http://www.tangshuang.net/2271.html 在上一篇<服务端php解决jquery ajax跨域请求restful api问题及实践>中,我简单介绍了如何通过服 ...

  9. 详解Ajax请求(一)前言——同步请求的原理

    我们知道,ajax是一种异步请求的方式,想要了解异步请求,就必须要先从同步请求说起.常见的同步请求的方式是form表单的提交,我们先从一种同步请求的示例说起. 我们希望输入姓名可以从后台得到身份证号. ...

随机推荐

  1. Linux入门之vi

    How to Use the vi Editor* *Copyright 1991 by Simon Fraser University. Reprinted with permission. The ...

  2. springTask任务调度

    1什么是任务调度 在企业级应用中,经常会制定一些“计划任务”,即在某个时间点做某件事情,核心是以时间为关注点,即在一个特定的时间点,系统执行指定的一个操作.常见的任务调度框架有Quartz和Sprin ...

  3. sqoop1的安装以及数据导入导出测试

    下载 wget http://mirror.bit.edu.cn/apache/sqoop/1.4.7/sqoop-1.4.7.bin__hadoop-2.6.0.tar.gz 解压 tar -zxf ...

  4. WDA-FPM-3-SEARCH(OIF)

    转载:https://www.cnblogs.com/sapSB/p/10097830.html   FPM三:简单的SEARCH(OIF) 这里是使用FPM Workbench自动生成的,没有去SE ...

  5. Algorithm-多目标优化-博文路径

    参考博文: 多目标进化算法(MOEA)概述: https://blog.csdn.net/qithon/article/details/72885053 多目标优化问题的算法及其求解: https:/ ...

  6. mongodb基础学习13-聚集aggregate操作

    aggregate可以用的操作与sql的对应关系 下面来看具体操作例子: 分组求和: 求总记录数 商品价格大于50记录分组求和 商品价格大于50且分组记录大于2的分组记录条件 分组库存数,并按库存排序 ...

  7. one by one 项目 part 3

    mysql error:Table 'performance_schema.session_variables' doesn't exist 打开cmd 输入:mysql_upgrade -u roo ...

  8. Java8 Stream语法详解 2

    1. Stream初体验 我们先来看看Java里面是怎么定义Stream的: A sequence of elements supporting sequential and parallel agg ...

  9. eclipse UTF-8

    1. 你本地开发环境IDE,默认配置也是gbk,改为utf82. 检查你tomcat等服务器中间件GBK改成UTF8eclipse工作空间的编码设置成UTF-8,具体操作是:windows---pre ...

  10. python使用外部PY文件的变量

    在用python和selenium编写登录等脚本时,一直都是给用户名和密码直接赋值.但是考虑到这样不便于管理,而且可能多个地方用到同一个变量,所以想把变量放在一个单独的文件中进行管理. 以登录脚本为例 ...