【web】http请求中的 OPTIONS 详解 & 跨域

1. 导读

有过跨域请求的同学们应该发现过一个http请求有时会请求2次的时候,今天就给大家说说这个http请求的OPTIONS 方法是如何产生以及作用是啥。

2. 解释

互联网上的各个节点之间本来都是连通的,但是有些节点,比如我们的个人电脑连接另外一些节点(比如服务器)的时候,总是通过浏览器。这样,浏览器作为一个中间人,就有机会管理一些连接,就好像高速路上的收费站检查进出的车辆。

这个类比还有一个可以借鉴的地方:就像收费站会根据一些章程文件来检查进出车辆,浏览器也根据一些技术约定来管理进出的连接。这些技术约定由 W3C,WHATWG 等组织制定。

我们今天要谈的跨域请求,Cross Origin Resource Sharing ( CORS ),就是浏览器执行的一种技术约定,一种 Protocol。

CORS(Cross-Origin Resource Sharing)跨域资源共享,这个协议 定义了必须在访问跨域资源时,浏览器与服务器应该如何沟通。CORS背后的基本思想就是使用自定义的HTTP头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功还是失败。

注意:CORS是仅指http请求涉及的跨域问题,跨域一般包含2个场景:

  • http请求(向另一个ip发送get等请求)
  • 页面(例如通过iframe加载另一个ip的页面或通过src引用另一个ip的css、js等文件)2种场景!

那么具体来说,到底什么是“跨域请求”呢?

首先,“域”(Origin)是指浏览器地址栏中显示的主机地址。“跨域”,就是网页程序想要请求的地址和地址栏中的“域”不同。合起来就是,一个网页程序想要去别人家的地址[1]索要资源。

这个概念基本上只在涉及浏览器的网页应用中会出现。在浏览器之外,“域”的概念(浏览器地址栏中显示的主机地址)都没有了,更谈不上跨域了。但这个概念也是重要的,因为目前主流的浏览器,不论是电脑上的,还是手机上的,都支持这个约定;也就是说,不论是哪家的浏览器,只要在他们的地盘活动,这个约定是必须要考虑的。

只要协议、域名、端口有任何一个不同,都被当作是不同的域。

如果违反这个 Protocol 中的约定,会有什么后果么?

有一些 request 会收不到 response,因为 response 被浏览器拦截了,内容不告诉你
另外一些请求会根本发不出去,因为浏览器不允许发出那样的 request。

接下来,我们就来讨论,哪些情况会导致收不到 response,哪些情况会导致 request 失败。

2.1 览器将CORS请求分为两类:简单请求和非简单请求

2.1.1 Simple Requests简单请求

满足以下条件属于简单请求:

  • 请求方式只能是GET、POST、HEAD
  • HTTP请求头限制这几种字段(Accept、Accept-Language、Content-Type、DPR、Downlink、Save-Data、Viewport-Width、Width)
  • Content-Type:只能取application/x-www-form-urlencoded、multipart/form-data、text/plain,只能是特定的3个
  • 请求中的任意XMLHttpRequestUpload对象均没有注册任何事件监听器;XMLHttpRequestUpload对象可以使用
  • 请求中没有使用ReadableStream对象

一旦一个 request 是 simple request,那么,尽管这个请求是跨域的,它也会被浏览器直接放行。但是,在 response 返回的时候,浏览器并不会把 response 直接交给你,而是去检查这个 response 的 headers:

  • 有没有 Access-Control-Allow-Origin
  • 这个 header 的 value 包含 request 发出的地址(也就是“域”)。

如果两个条件都满足, response 会被返回给发出请求的程序;如果没有这个 header 或者 value 不对, response 就会被拦截下来,因为在浏览器看来,这个 response 不属于你(因为服务器没有明确允许你这个“域”来请求它)。如果你使用的是 chrome 浏览器,在 response 被拦截下来的时候,console 中会显示一个类似于下面的错误信息:

repeat this in your console
1

尽管发出request的程序无法得到 response,但是这个请求实际上是被发出了的,而且服务器也会完整的处理这个request。 可以想见,如果被请求的服务器支持被跨域请求,那么它一定会想办法在 response 中加上Access-Controll-Allow-Origin这个 header,并且附上合适的值。

什么是合适的值呢?
在 Request 的headers中会有一个 Origin,只要Access-Controll-Allow-Origin包含这个 Origin 就可以了(如果是 wildcard *,那么就等于包含所有的 Origin)。

2.1.1.1 演示简单请求被拒绝

我们通过nodejs,本地启动2个web服务,利用express插件提供静态文件,模拟类似完整的web服务。

步骤1 :编写代码

新建2个启动js脚本,分别命令为express_8881.js和express_8883.js:
在这里插入图片描述
public放置静态页面:
在这里插入图片描述

express_8881.js:

var express = require('express');
var app = express();
//提供静态文件服务
app.use('/public', express.static('public'));

//通过路由,提供一个get的查询服务
app.get('/', function (req, res) {
   // res.set("Access-Control-Allow-Origin","*");
   res.send('Hello World 8881');
})


 //监听8881,并打印可访问地址
var server = app.listen(8881, function () {
 
  var host = server.address().address
  var port = server.address().port
   //显示的有可能是ipv6的格式
  console.log("应用实例,访问地址为 http://%s:%s", host, port)
 
})
123456789101112131415161718192021

express_8883.js,除了端口配置,与前者几乎一样:

var express = require('express');
 var app = express();

//提供静态文件服务
app.use('/public', express.static('public'));
//通过路由,提供一个get的查询服务 
app.get('/', function (req, res) {   
   res.send('Hello World 8883');
})
 
 //监听8883,并打印可访问地址
var server = app.listen(8883, function () {
 
  var host = server.address().address
  var port = server.address().port
 
  //显示的有可能是ipv6的格式
  console.log("应用实例,访问地址为 http://%s:%s", host, port)
 
})
1234567891011121314151617181920

index8881.html,仅提供文本显示:

<html>
    <body>
            Wellecome to 8881 !
    </body>
</html>
12345

index8883.html,稍复杂,里面有个按钮,会触发向8881发起一个跨域的请求:

<html>

    <script>
        function send (){
        var httpRequest = new XMLHttpRequest();//第一步:建立所需的对象
                httpRequest.open('GET', 'http://127.0.0.1:8881/', true);//第二步:设置url               
                
                // 获取数据后的处理程序                
                httpRequest.onreadystatechange = function () {     };
                
                 httpRequest.send();//第三步:发送请求 
        }

    </script>

    <body>
    Wellecome to 8083 !
<br>
        <button onclick="send()">send request to 8881</button>

    </body>
</html>
12345678910111213141516171819202122

步骤2 :启动web 8881服务:

D:\test\nodejs-workspace>node express_8881.js
应用实例,访问地址为 http://:::8881
12

显示的是ipv6的地址,实际是127.0.0.1
在这里插入图片描述

访问http://127.0.0.1:8881/ ,证明直接访问时(非跨域)可以提供一个正常的http请求:
在这里插入图片描述

步骤3 :启动web 8883服务:

D:\test\nodejs-workspace>node express_8883.js
应用实例,访问地址为 http://:::8883
12

在这里插入图片描述
访问http://127.0.0.1:8883/public/index8883.html,我们访问的是一个页面,该页面提供一个按钮,会触发跨域请求
在这里插入图片描述

步骤4 :触发跨域请求:

点击http://127.0.0.1:8883/public/index8883.html页面的 send request to 8881按钮
分析:
在这里插入图片描述
在这里插入图片描述

  • 没有Access-Control-Allow-Origin项,说明是不支持跨域的
  • origin是request headers中的信息,服务端正是根据这个信息拒绝请求的

console打印:

Access to XMLHttpRequest at 'http://127.0.0.1:8881/' from origin 'http://127.0.0.1:8883' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
index8883.html:7 GET http://127.0.0.1:8881/ net::ERR_FAILED
12

在这里插入图片描述

2.1.2 Preflighted Requests 复杂请求

进行跨域的请求只会被分为两类,第一类就是 Simple Requests,其他的都是 Preflighted Requests。所以,只要一个请求,不满足上面一小节中对 simple requests 的要求,就是 preflighted requests 了。也就是说,一个请求只要满足下面几个条件中的任意一个就可以被划归此类了:

看完了 preflighted requests 的入选条件,我们再来从字面和行为上理解一下这种跨域请求。preflight 的中文意思是起飞前的,而 preflighted 的意译可能是:被在起飞前搞了一番的(有点蹩脚,哈哈)。这种跨域请求的实际行为也确实包含 preflight (起飞前)的部分:在一个请求被发出之前,浏览器会先发一个 OPTIONS 请求到目标“域”的服务器上,这个提前发出的请求,被称为 preflight request。

一般的,使用过程中,在跨域的情况下,设置的Content-Type为application/json,所以出现了非简单请求,需要后台配合将options预请求过滤处理即可。

讲完了 preflight request 这个最重要的概念,我们可以比较方便的梳理这种跨域请求的流程:

  • 浏览器发送 preflight request(那个 OPTIONS 请求[2])
  • 浏览器收到 preflight response(也就是刚刚那个 request 的返回)
  • 浏览器根据 preflight response 中的 Access-Control-Allow-Origin, Access-Control-Allow-Headers以及其他Access-Control-*类的headers 中的 value 来判断网页程序真正要发出的 request 是否符合要求
  • 如果这个 request 符合要求,request 被发出,网页程序可以收到正常的 response(如果不出网络通讯上的意外);如果这个 request 被判定为不符合要求,这个 request 干脆就不会被发出。

2.1.2.1 options请求未通过例子

步骤1 :修改代码,构造复杂请求:

下面我们基于 “2.1.1.1 演示简单请求被拒绝”来改造,只要添加一行代码,把简单请求改造为复杂请求即可:

httpRequest.setRequestHeader('Content-Type','text/html')
1

由简单请求的定义得知 Content-Type:只能取application/x-www-form-urlencoded、multipart/form-data、text/plain,只能是特定的3个,这里我们是’text/html’,不在特定的3个内,因此是复杂请求。
不过需要注意的是,一般get请求不需要设置Content-Type,只有post才有必要设置!我们这里为了简单演示,举了一个不标准的例子。get和post请求具体可以参见《js发送get 、post请求的方法简介》

在index8883.html,增加上述代码:

 <script>
        function send (){
        var httpRequest = new XMLHttpRequest();
                httpRequest.open('GET', 'http://127.0.0.1:8881/', true);               
                httpRequest.setRequestHeader('Content-Type','text/html')  //修改默认的Content-Type,变为复杂代码
                          
                httpRequest.onreadystatechange = function () {     };
                
                
                httpRequest.send();
        }

    </script>
12345678910111213

都不用重启服务,刷新http://127.0.0.1:8883/public/index8883.html页面即可。
步骤2 :触发跨域请求:
在这里插入图片描述
从上图能看到实际有2条请求,其中一条正是options请求:

  • options请求,请求本身是成功的
    虽然请求自身是成功的,但服务端的返回信息,没有Access-Control-Allow-Origin,说明是不支持跨域的,这也是目标请求失败的原因。
  • 目标请求显示失败,其实压根没发送

console打印:

Access to XMLHttpRequest at 'http://127.0.0.1:8881/' from origin 'http://127.0.0.1:8883' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
1

与简单请求稍有不同,控制台打印的信息包含关键词显示请求的类型是preflight request类型

在这里插入图片描述

2.1.3 支持跨域请求

只需要在被访问的服务器设置响应头时,添加响应的跨域项即可:

2.1.3.1 对于简单请求

基于 “2.1.1.1 演示简单请求被拒绝”来改造

直接在express_8881.js加上“res.set("Access-Control-Allow-Origin","*");”:

//通过路由,提供一个get的查询服务
app.get('/', function (req, res) {
   // 支持保证跨域
   res.set("Access-Control-Allow-Origin","*");
   res.send('Hello World 8881');
})
123456

需要重启web服务!

简单请求效果:
在这里插入图片描述

2.1.3.2 对于非简单请求(带options请求)

更标准的写法可参见《nodesjs express 统一(全局)设置Respose Headers & 跨域》

对于nodejs express框架来说,由于采用了路由,我们之前的请求是仅响应get方法,因此,对于option等价于无路由,永远返回的是不支持跨域,因此我们需要针对options写个稍复杂的。

基于 “2.1.2.1 options请求未通过例子”来改造

修改express_8881.js:

app.use('/public', express.static('public'));

//在aap.use后面新增一个基础路由,由于是all,因此options也会进去
app.all('*', function(req, res, next) {
   res.header("Access-Control-Allow-Origin","*");
   res.header("Access-Control-Allow-Headers","Origin, X-Requested-With, Content-Type");
   res.header("Access-Control-Allow-Methods", "POST, GET, PUT, OPTIONS, DELETE");
   next();
});
123456789

在aap.use后面新增一个基础路由,由于是all,因此options也会进去;类似java中的switch中的default,如果某个get请求过来,仍然会匹配get请求的,没有的话,会走all

在这里插入图片描述

参考:
《http请求中的 OPTIONS 详解》

posted @ 2023-03-27 09:28  Little_Monster-lhq  阅读(870)  评论(0编辑  收藏  举报