'由跨域引起的一个option请求知识'

下班后……

晓清(前端工程师)和小平(后端工程师)那边聚集了好几个人。我处于好奇心,走过去看了看。看了一会,了解到晓清在给后端发送get请求的时候,页面一切正常;但是发送post请求的时候,会预先发送一个option请求。结果option请求被后台某框架拦截了,无法进行跨域,那么后台的post请求也就无法进行。这个问题导致他们进行了长时间的资料搜索和测试。直到现在还没有好的解决方案。(我也是刚刚才想通,有机会的话,明天再告诉他们了。

跨域跨域资源共享

跨域资源共享 俗称CORS,最好的资料在这里

ps:(搜索资料的时候,正好看到了阮一峰老师的博客——跨域资源共享 CORS 详解。我看了一下,和官方文档基本上一致,只是他的博客看起来很舒服,而且语言也变得简单一些。但其实里面是有错误的。具体哪里的话有问题,我就不指明了。所以之前团队里面有人说阮一峰是全才,我内心是不赞同的。他只是一个很普通的人。把一些入门的知识点整理的很好罢了。不过我内心也非常尊敬他,他翻译的书和一些博客文集写得很有意思。)

如何引起跨域的问题,我就不说了。现在要整理的是跨域之后,如何解决?

跨域之后

浏览器的同源策略,会导致跨域。跨域之后,浏览器还是会发送请求给服务器。(并不是之前某个年轻后端工程师说的浏览器直接拦截,当时很生气哇!!!后端明明不懂还不虚心听或者去查一下,硬要自我想象。)
这时候浏览器发送的请求会分为俩种:简单请求(simple request)和非简单请求(not-so-simple request)。

简单请求

1、某些请求不会触发 CORS 预检请求。本文称这样的请求为“简单请求”,
若请求满足所有下述条件,则该请求可视为“简单请求”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
使用下列方法之一:
* GET
* HEAD
* POST
———— * Content-Type :
———— * //注:仅当POST方法的Content-Type值等于下列之一才算作简单请求
—————— * text/plain
—————— * multipart/form-data
—————— * application/x-www-form-urlencoded
不得人为设置该集合之外的其他首部字段。该集合为:
Accept
Accept-Language
Content-Language
Content-Type (需要注意额外的限制,同上。常见的就前四个。)
DPR
Downlink
Save-Data
Viewport-Width
Width

非简单请求

与前述简单请求不同,非简单请求要求必须首先使用 OPTIONS方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。”预检请求“的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。
当请求满足下述任一条件时,即应首先发送预检请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
* 使用了下面任一 HTTP 方法:
PUT
DELETE
CONNECT
OPTIONS
TRACE
PATCH
* 人为设置了对 CORS 安全的首部字段集合之外的其他首部字段。该集合为:
Accept
Accept-Language
Content-Language
Content-Type (but note the additional requirements below)
DPR
Downlink
Save-Data
Viewport-Width
Width
* Content-Type 的值不属于下列之一:
application/x-www-form-urlencoded
multipart/form-data
text/plain

俩种请求对比

简单请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
GET /resources/public-data/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
Referer: http://foo.example/examples/access-control/simpleXSInvocation.html
Origin: http://foo.example


HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 00:23:53 GMT
Server: Apache/2.0.61
Access-Control-Allow-Origin: *
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: application/xml

[XML Data]

第 1~10 行是请求首部。第10行 的请求首部字段 Origin 表明该请求来源于 http://foo.exmaple。

第 13~22 行是来自于 http://bar.other 的服务端响应。响应中携带了响应首部字段 Access-Control-Allow-Origin(第 16 行)。使用 Origin 和 Access-Control-Allow-Origin 就能完成最简单的访问控制。本例中,服务端返回的 Access-Control-Allow-Origin: * 表明,该资源可以被任意外域访问。如果服务端仅允许来自 http://foo.example 的访问,该首部字段的内容如下:

Access-Control-Allow-Origin: http://foo.example

现在,除了 http://foo.example,其它外域均不能访问该资源(该策略由请求首部中的 ORIGIN 字段定义,见第10行)。Access-Control-Allow-Origin 应当为 * 或者包含由 Origin 首部字段所指明的域名。

非简单请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
OPTIONS /resources/post-here/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
Origin: http://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type


HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain

浏览器检测到,从 JavaScript 中发起的请求需要被预检。从上面的报文中,我们看到,第 1~12 行发送了一个使用 OPTIONS 方法的“预检请求”。 OPTIONS 是 HTTP/1.1 协议中定义的方法,用以从服务器获取更多信息。该方法不会对服务器资源产生影响。 预检请求中同时携带了下面两个首部字段:

1
2
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER

首部字段 Access-Control-Request-Method 告知服务器,实际请求将使用 POST 方法。首部字段 Access-Control-Request-Headers 告知服务器,实际请求将携带两个自定义请求首部字段:X-PINGOTHER 与 Content-Type。服务器据此决定,该实际请求是否被允许。
第1426 行为预检请求的响应,表明服务器将接受后续的实际请求。重点看第 1720 行:

1
2
3
4
Access-Control-Allow-Origin: http://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400

首部字段 Access-Control-Allow-Methods 表明服务器允许客户端使用 POST, GET 和 OPTIONS 方法发起请求。该字段与 HTTP/1.1 Allow: response header 类似,但仅限于在需要访问控制的场景中使用。

首部字段 Access-Control-Allow-Headers 表明服务器允许请求中携带字段 X-PINGOTHER 与 Content-Type。与 Access-Control-Allow-Methods 一样,Access-Control-Allow-Headers 的值为逗号分割的列表。

最后,首部字段 Access-Control-Max-Age 表明该响应的有效时间为 86400 秒,也就是 24 小时。在有效时间内,浏览器无须为同一请求再次发起预检请求。请注意,浏览器自身维护了一个最大有效时间,如果该首部字段的值超过了最大有效时间,将不会生效。

发送完预检请求之后,通过服务器返回的字段,浏览器将进行下一步的POST请求。
当然如果服务器返回的字段不允许,页面就会报错,引起问题。
上面所说的就是后端服务器某框架截取了OPTION请求,返回的字段中不允许进行请求。所以导致报错。

修改方案

1、让前端和后端处于同一个域名,不引起跨域。(线上环境不允许)
2、通过查看框架设置,修改前端发送给后端的OPTION请求,是的前端允许跨域。
3、前端不要引起非简单请求。发送post的时候,发送简单请求就好了。