post请求失败,变成options请求(CORS)

问题

post请求失败,变成options请求(CORS)的原因

简答

浏览器为了安全起见,会先发送一个 options 请求,确保请求发送是安全的,一般 postdeleteput 等请求都会修改服务器资源,所以浏览器会先发一个请求,问问服务器是否会正确(允许)请求。

出现 options 的情况一般为

  • get | post 请求
  • post 请求的 content-type 不是常规的三个
  • post 请求的 payloadtext/xml

因为跨域,浏览器会为了试探服务器是否会接受请求,先发送一个 options 请求。即便是服务器允许程序跨域访问,若不支持 options 请求,请求也会死掉。

场景

要使用 XMLHttpRequest 对象 POST 请求不在同一域名下的一个站点,即:跨域请求,请求数据格式为 JSON

因此需要使用 setRequestHeader() 方法设置 Content-Typeapplication/json 。设置完这个自定义的 HTTP Headers 后,发现原本可以跨域 post 请求失效了。调试对应的服务端代码,发现 post 请求变成了 options 请求。

这与 CORS(Cross-Origin Resource Sharing,跨站资源共享)策略有关,设置 Content-Type 后,CORS 简单请求变为 Preflighted 请求。在 Preflighted 请求中,XMLHttpRequest 对象会首先发送 options 嗅探,以验证是否有对指定站点的访问权限。


XHR对HTTP请求的访问控制

XHR对象对于HTTP跨域请求有三种:

  • 简单请求
  • Preflighted请求
  • Preflighted认证请求

简单请求不需要发送 options 嗅探请求,但只能按发送简单的getheadpost请求,且不能自定义 HTTP Headers

Preflighted请求和认证请求,XHR 会首先发送一个 options 嗅探请求,然后 XHR 会根据 options 请求返回的 Access-Control-* 等头信息判断是否有对指定站点的访问权限,并最终决定是否发送实际请求信息。

1. 简单请求

简单请求进行跨域访问时,XMLHttpRequest 对象会直接将实际请求发送给服务器。

简单请求具有如下特点:

  • 只能使用 getheadpost 方法。
  • 使用 post 方法向服务器发送数据时,Content-Type只能使用 application/x-www-form-urlencodedmultipart/form-datatext/plain 编码格式。
  • 请求时不能使用自定义的 HTTP Headers
  1. //例如:'http://kubiji.cn' 对 'http://scutephp.other' 有跨域访问权限,我们进行简单请求简单请求后,查看请求头及服务返回头信息。
  2. var xhr = new XMLHttpRequest();
  3. var url = 'http://itbilu.other/resources/public-data/';
  4. xhr.open('GET', url, true);
  5. xhr.onreadystatechange = function() {
  6. if (xhr.readyState == 4) {
  7. //显示请求结果
  8. console.log(xhr.getAllResponseHeaders());
  9. }
  10. };
  11. xhr.send();
  1. // 服务器收到的HTTP请求头,及服务器响应头如下:
  2. //服务器收到的请示头
  3. GET /resources/public-data/ HTTP/1.1
  4. Host: itbilu.other
  5. User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.93 Safari/537.36
  6. Accept-Language:en-US,en;q=0.8,zh-CN;q=0.6,zh;q=0.4,zh-TW;q=0.2,da;q=0.2
  7. Accept-Encoding: gzip,deflate
  8. Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
  9. Connection: keep-alive
  10. Referer: http://kubiji.cn/demo/xhr.html
  11. Origin: http://itbilu.com
  12. //服务响头
  13. HTTP/1.1 200 OK
  14. Date: Tue, 22 SEP 2015 22:23:53 GMT
  15. Server: Nginx/1.8.0
  16. Access-Control-Allow-Origin: *
  17. Keep-Alive: timeout=2, max=100
  18. Connection: Keep-Alive
  19. Transfer-Encoding: chunked
  20. Content-Type: application/xml
  21. [XML Data]

通过上面的信息我们可以看出:XHR 在发送 HTTP 请求时,还发送了一个自定义的 HTTP Headers 字段 Origin,由于这是一个简单请求,所在 XHR 并没有发送 OPTIONS 嗅探请求。
通过服务器的响应头 Access-Control-Allow-Origin: * 可以看出,其对所有站点都是可以通过 XMLHttpRequest 对象访问的。

2. Preflighted请求

Preflighted请求与简单请求不同,Preflighted 请求首先会向服务器发送一个 Options 请求,以验证是否对指定服务有访问权限,之后再发送实际的请求。

Preflighted 请求具有以下特点:

除GET、HEAD、POST方法外,XHR都会使用Preflighted 请求。

使用 POST 方法向服务器发送数据时,Content-Type 使用 application/x-www-form-urlencoded、multipart/form-data 或 text/plain 之外编码格式也会使用 Preflighted 请求。

使用了自定义的HTTP Headers后,也会使用Preflighted 请求。

现在很多数据交互都是基本JSON格式的,下面是一个向服务发送JSON数据的示例:

  1. var xhr = new XMLHttpRequest();
  2. var url = 'http://scutephp.other/resources/post-json/';
  3. var body = {name:'HELLO'};
  4. xhr.open('POST', url, true);
  5. xhr.setRequestHeader('X-ITBILU', 'baidu.com');
  6. xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');
  7. xhr.onreadystatechange = function() {
  8. if (xhr.readyState == 4) {
  9. console.log(xhr.responseText);
  10. }
  11. };
  12. xhr.send(JSON.stringify(body));


在上面的示例中,我们向指定的服务器发送了 JSON 字符串,指定了一个自定义的头信息 X-ITBILU,同时还指定了 Content-Type 为 application/json。下面是服务器收到的请求头信息,及服务器的响应头信息:

  1. //服务器收到的OPTIONS请求头
  2. OPTIONS /resources/post-json/ HTTP/1.1
  3. Host: itbilu.other
  4. User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.93 Safari/537.36
  5. Accept: text/html,application/xhtml+xml,application/xml;q=0.9,/;q=0.8
  6. Accept-Language: en-US,en;q=0.8,zh-CN;q=0.6,zh;q=0.4,zh-TW;q=0.2,da;q=0.2
  7. Accept-Encoding: gzip,deflate
  8. Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
  9. Connection: keep-alive
  10. Origin: http://kubiji.cn
  11. Access-Control-Request-Method: POST
  12. Access-Control-Request-Headers: X-ITBILU
  13. //服务器对OPTIONS请求的响应头
  14. HTTP/1.1 200 OK
  15. Date: Tue, 22 SEP 2015 22:23:55 GMT
  16. Server: Nginx/1.8.0
  17. Access-Control-Allow-Origin: http://kubiji.cn
  18. Access-Control-Allow-Methods: POST, GET, OPTIONS
  19. Access-Control-Allow-Headers: X-ITBILU
  20. Access-Control-Max-Age: 1728000
  21. Vary: Accept-Encoding, Origin
  22. Content-Encoding: gzip
  23. Content-Length: 0
  24. Keep-Alive: timeout=2, max=100
  25. Connection: Keep-Alive
  26. Content-Type: text/plain
  27. //服务器收到的实际POST请求的请求头
  28. POST /resources/post-json/ HTTP/1.1
  29. Host: itbilu.other
  30. User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.93 Safari/537.36
  31. Accept: text/html,application/xhtml+xml,application/xml;q=0.9,/;q=0.8
  32. Accept-Language: en-US,en;q=0.8,zh-CN;q=0.6,zh;q=0.4,zh-TW;q=0.2,da;q=0.2
  33. Accept-Encoding: gzip,deflate
  34. Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
  35. Connection: keep-alive
  36. X-PINGOTHER: itbilu.com
  37. Content-Type: application/json;charset=UTF-8
  38. Referer: http://kubiji.cn/demo/xhr.html
  39. Content-Length: 55
  40. Origin: http://itbilu.com
  41. Pragma: no-cache
  42. Cache-Control: no-cache
  1. {"name":"IT笔录"}
  1. //服务器对POST请求的响应头
  2. HTTP/1.1 200 OK
  3. Date: Tue, 22 SEP 2015 22:23:55 GMT
  4. Server: Nginx/1.8.0
  5. Access-Control-Allow-Origin: http://itbilu.com
  6. Vary: Accept-Encoding, Origin
  7. Content-Encoding: gzip
  8. Content-Length: 256
  9. Keep-Alive: timeout=2, max=99
  10. Connection: Keep-Alive
  11. Content-Type: tapplication/json
  12. [Some GZIP'd payload]


在上面的请求中,我们可以看,XHR第一次并没有发送实例的 POST 请求,而是发送了一个 OPTIONS 请求。在 OPTIONS 请求,服务器收到了以下几个自定义头信息:

  1. Origin: http://itbilu.com
  2. Access-Control-Request-Method: POST
  3. Access-Control-Request-Headers: X-ITBILU


在上面的请示头中:Access-Control-Request-Method 告诉服务器接下来的实际请求是一个 POST 请求。Access-Control-Request-Headers 告诉服务器接下来实际请求将包含一个自定义的请求头 X-ITBILU。

而在服务器对 OPTIONS 请求的响应头中,包含了以下头信息:

  1. Access-Control-Allow-Origin: http://itbilu.com
  2. Access-Control-Allow-Methods: POST, GET, OPTIONS
  3. Access-Control-Allow-Headers: X-ITBILU
  4. Access-Control-Max-Age: 1728000


在上面的响应头中:

Access-Control-Request-Method 告诉请求对象(XHR),服务允许使用POST、GET、OPTIONS方法访问资源。

Access-Control-Request-Headers 告诉请求对象,服务器允许包含自定义请求头X-ITBILU。

Access-Control-Max-Age 告诉请求对象验证有效时长,在接下来的1728000秒(20天)不用再发送 OPTIONS 请求验证合法性。

在 XMLHttpRequest 对象发送 OPTIONS 请求并验证完以上头信息后,才最终发送了实际的 POST 请求。

3. 认证请求

XMLHttpRequest 可以在跨域请求时发送认证信息,但在默认情况下 HTTP Cookies 和 HTTP 认证是不被发送的。要发送 Preflighted 认证请求需要设置 XMLHttpRequest 对象的 withCredentials 属性:

  1. var xhr = new XMLHttpRequest();
  2. var url = 'http://itbilu.other/resources/credentialed-content/';
  3. xhr.open('GET', url, true);
  4. xhr.withCredentials = true;
  5. xhr.onreadystatechange = function() {
  6. if (xhr.readyState == 4) { //显示请求结果
  7. console.log(xhr.getAllResponseHeaders());
  8. }
  9. };
  10. xhr.send();


在上面的示例中,我们设置了 XMLHttpRequest 对象的 withCredentials 属性为 true。虽然这是个简单请求,由于发送了认证信息,所以浏览服务器会拒绝没有 Access-Control-Allow-Credentials: true 响应头的服务器响应。
上面请求的请求头和响应头如下:

  1. GET /resources/access-control-with-credentials/ HTTP/1.1
  2. Host: itbilu.other
  3. User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.93 Safari/537.36
  4. Accept: text/html,application/xhtml+xml,application/xml;q=0.9,/;q=0.8
  5. Accept-Language: en-US,en;q=0.8,zh-CN;q=0.6,zh;q=0.4,zh-TW;q=0.2,da;q=0.2
  6. Accept-Encoding: gzip,deflate
  7. Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
  8. Connection: keep-alive
  9. Referer: http://itbilu.com/demo/credential.html
  10. Origin: http://itbilu.com
  11. Cookie: Hm_lvt_782f5a889da9ba4cae3c5f5575784ec3=1417282790
  12. HTTP/1.1 200 OK
  13. Date: Tue, 22 SEP 2015 22:20:30- GMT
  14. Server: Nginx/1.8.0
  15. Access-Control-Allow-Origin: http://kubiji.cn
  16. Access-Control-Allow-Credentials: true
  17. Cache-Control: no-cache
  18. Pragma: no-cache
  19. Set-Cookie: Hm_lvt_782f5a889da9ba4cae3c5f5575784ec3=1417282790; expires=Wed, 32-OCT-2015 22:30:31 GMT
  20. Vary: Accept-Encoding, Origin
  21. Content-Encoding: gzip
  22. Content-Length: 106
  23. Keep-Alive: timeout=2, max=100
  24. Connection: Keep-Alive
  25. Content-Type: text/plain
  26. [text/plain payload]


通过上面的请求及响应头,我们可以看出。XHR 在发送 GET 请求时,在请求头中包含了 Cookie 信息。而服务器在响应请求时,有一个 Access-Control-Allow-Credentials: true 响应头,表示这是一个认证请求。

认证请求不同于其它请求的一点,要求明确响应 Access-Control-Allow-Origin 头,要明确指定要响应的站点,如:kubiji.cn。上文中 Access-Control-Allow-Origin: * 的响应方式在认证请求中是不合法的。

CORS: Cross-Origin Resource Sharing 跨源资源共享
正如大家所知,出于安全考虑,浏览器会限制脚本中发起的跨站请求。比如,使用 XMLHttpRequest 对象发起 HTTP 请求就必须遵守同源策略(same-origin policy)。

具体而言,Web 应用程序能且只能使用 XMLHttpRequest 对象向其加载的源域名发起 HTTP 请求,而不能向任何其它域名发起请求。

为了能开发出更强大、更丰富、更安全的Web应用程序,开发人员渴望着在不丢失安全的前提下,Web 应用技术能越来越强大、越来越丰富。比如,可以使用 XMLHttpRequest 发起跨站 HTTP 请求。

跨域并非浏览器限制了发起跨站请求,而是跨站请求可以正常发起,但是返回结果被浏览器拦截了。最好的例子是crsf跨站攻击原理,请求是发送到了后端服务器无论是否跨域!注意:有些浏览器不允许从HTTPS的域跨域访问HTTP,比如Chrome和Firefox,这些浏览器在请求还未发出的时候就会拦截请求,这是一个特例。

跨源资源共享标准( cross-origin sharing standard ) 使得以下场景可以使用跨站 HTTP 请求:

如上所述,使用 XMLHttpRequest 发起跨站 HTTP 请求。