问题记录
vue采用axios封装的网络请求库中的post方法去请求接口时默认发送了一次OPTIONS请求,然后接口状态码为200,但是并没有响应和数据返回。POST 跨域请求服务器资源的时候,控制台报了这么一个错:
XMLHttpRequest cannot load xxxxxxxx. Request header field Content-Type is not allowed by Access-Control-Allow-Headers in preflight response.
查看Chrome 的network请求发现method这个请求并不是 POST,而是 OPTIONS,突然有点懵逼,因为印象中没用过这种请求方法。
AJAX跨域请求时的OPTIONS方法
在通过 AJAX 发起 HTTP 请求的时候,我们最常用的方法大概就是 GET 和 POST 了。实际上除了这两个以外,HTTP 请求还有 PUT,DELETE,OPTIONS 等等。本文就将对 OPTIONS 请求的作用进行介绍,并解决我前两天遇到的一个与它相关的问题。
问题的根源
OPTIONS请求简单来说,就是对于一些可能对服务器数据有影响的请求,如 PUT,DELETE 和搭配某些 MIME 类型的 POST 方法,浏览器必须先发送一个“预检请求”,来确认服务器是否允许该请求,允许的话再真正发送相应的请求。
检查了一下代码确实调用的是post方法,我的 Content-Type 设置的为application/json,同时也有同时用的是 jQuery 的 $.post 请求测试了一下同一个接口都是没有问题的,查看了下浏览器上面的newwork请求头发现 jQuery 发送的请求 Content-Type 为 application/x-www-form-urlencoded,而我这边用 axios 发送的请求 Content-Type 为 application/json,然后再去查了一下资料发现发送的请求内容类型如果不是 application/x-www-form-urlencoded,multipart/form-data 或 text/plain 这三者的话,便会触发 OPTIONS 请求,而 jQuery 发送的请求内容类型默认值为 application/x-www-form-urlencoded,这就是为什么ajax可以访问的原因
前端解决办法
这下我们知道问题出在哪里了。要想避免这个问题出现,要么在服务器端进行设置,要么让请求避开上面的限制。前端这边可以对请求的设置一下请求头,然后对请求参数进行一下处理:
可以使用 URLSearchParams API
var params = new URLSearchParams();
params.append('param1', 'value1');
params.append('param2', 'value2');
axios.post('/foo', params);
或者 qs
var qs = require('qs');
axios.post('/foo', qs.stringify({ 'bar': 123 }));
服务端跨域设置
在构建Public APIs的过程中,首先要解决的第一个问题就是跨域请求的问题。
网络应用安全模型中很重要的一个概念是“同源准则”(same-origin policy)。该准则要求一个网站(由协议+主机名+端口号三者确定)的脚本(Script)、XMLHttpRequest和Websocket无权去访问另一个网站的内容。在未正确设置的情况下,跨域访问会提示如下错误:No 'Access-Control-Allow-Origin' header is present on the requested resource. 这项限制对于跨域的Ajax请求带来了很多不便。
典型的对于跨域请求的解决方案如下:
- document.domain property
- Cross-Origin Resource Sharing (CORS)
- Cross-document messaging
- JSONP
本文重点讲述的则是其中Cross-Origin Resource Sharing (CORS)的原理和在rails下的配置方式
Cross-Origin Resource Sharing (CORS)
CORS的基本原理是通过设置HTTP请求和返回中header,告知浏览器该请求是合法的。这涉及到服务器端和浏览器端双方的设置:请求的发起(Http Request Header)和服务器对请求正确的响应(Http response header)。
发起CORS请求
CORS兼容以下浏览器:
- Internet Explorer 8+
- Firefox 3.5+
- Safari 4+
- Chrome
原生Javascript可以通过XMLHttpRequest Object或XDomainRequest发起请求,详细的方式可以参见这篇文章:http://www.html5rocks.com/en/tutorials/cors/
JQuery的$.ajax()可以用来发起XHR或者CORS请求。然而该方法不支持IE下的XDomainRequest,需要使用JQuery的插件来实现IE下的兼容性(http://bugs.jquery.com/ticket/8283)
$.ajax({
// The 'type' property sets the HTTP method.
// A value of 'PUT' or 'DELETE' will trigger a preflight request.
type: 'GET',
// The URL to make the request to.
url: 'http://updates.html5rocks.com',
// The 'contentType' property sets the 'Content-Type' header.
// The JQuery default for this property is
// 'application/x-www-form-urlencoded; charset=UTF-8', which does not trigger
// a preflight. If you set this value to anything other than
// application/x-www-form-urlencoded, multipart/form-data, or text/plain,
// you will trigger a preflight request.
contentType: 'text/plain',
xhrFields: {
// The 'xhrFields' property sets additional fields on the XMLHttpRequest.
// This can be used to set the 'withCredentials' property.
// Set the value to 'true' if you'd like to pass cookies to the server.
// If this is enabled, your server must respond with the header
// 'Access-Control-Allow-Credentials: true'.
withCredentials: false
},
headers: {
// Set any custom headers here.
// If you set any non-simple headers, your server must include these
// headers in the 'Access-Control-Allow-Headers' response header.
},
success: **function**() {
// Here's where you handle a successful response.
},
error: **function**() {
// Here's where you handle an error response.
// Note that if the error was due to a CORS issue,
// this function will still fire, but there won't be any additional
// information about the error.
}
});
服务器正确响应CORS请求
根据请求内容的不同,浏览器会需要添加对应的Header或者发起额外的请求。其中的细节都由浏览器负责处理,对于用户来讲是透明的。我们只需要了解如何针对差异的请求做出适当的响应即可。
我们将CORS请求分成以下两种类型:
1、简单请求
2、不是那么简单的请求
其中简单请求要求:
请求类型必须是GET,POST,HEAD三者中的一种
请求头(Header)中仅可以包含:
- Accept
- Accept Language
- Content Language
- Last Event ID
- Content Type:仅接受application/x-www-form-urlencoded,multipart/form-data,text/plain
不满足上述条件的所有请求,例如PUT,DELETE或者是Content Type是application/json,均为“不是那么简单的请求”。针对这种请求,浏览器会在真实请求前,额外发起一次类型为OPTIONS的请求(Preflight request),只有服务器正确响应了OPTIONS请求后,浏览器才会发起该请求。(参见下图)
[图片上传失败...(image-d95c9e-1561973826637)]
下文将针对b.com向a.com发起跨域请求说明服务器如何正确响应这两种类型的请求。
简单请求
浏览器在发出请求前为请求添加Origin来标明请求的来源,用户不可更改此内容。但Header中是否有Origin并不能作为判断是否是CORS请求的标准,因为不同浏览器对于此内容的处理方式并不完全一致,同源请求中也有可能出现Origin。
下面是一个b.com向a.com发起的一次GET请求。
GET /cors HTTP/1.1
Origin: http://b.com
Host: a.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
正确响应的返回如下,均由Access-Control-*开头:
Access-Control-Allow-Origin: http://b.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8
Access-Control-Allow-origin: 此处是Server同意跨域访问的域名列表。如果允许任意网站请求资源,此处可以写为'*'
Access-Control-Expose-Headers: 可以设置返回的Header以传递数据。简单请求中允许使用的Header包括:Cache-Control,Content-Language,Content-Type,Expires,Last-Modified,Pragma。
复杂的请求
如果希望使用PUT,DELETE等RESTful等超出了简单请求的范围的请求,浏览器则会在发起真实请求前先向服务器发起一次称作Preflight的OPTIONS的请求,以确保服务器接受该类型请求。其后才会发起真实要求的请求。请求的发起与简单请求并无差异,而服务器端则要针对Preflight Request做额外的响应。
下面是一次典型的Preflight请求:
OPTIONS /cors HTTP/1.1
Origin: http://b.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: a.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
Access-Control-Request-Method代表真实请求的类型。Access-Control-Request-Headers则代表真实请求的请求头key内容。服务器仅在验证了这两项内容的合法性之后才会同意浏览器发起真实的请求。
Access-Control-Allow-Origin: http://b.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Content-Type: text/html; charset=utf-8
此处并未列举的一项返回头是Access-Control-Max-Age。因为每次请求均要发起一次额外的OPTIONS请求是非常低效的,因此可以为浏览器保存该返回头设置一个缓存的时间,单位为秒。在缓存过期以前,浏览器无需再次验证同一类型的请求是否合法。
真实请求的内容则和简单请求的内容完全一致,此处不再赘述。
下图非常详细的再次描述了服务器对于不同类型的请求如何做出正确的响应。
Rails下对CORS请求的配置
首先要确保在Routes.rb中加上对于OPTIONS请求的正确响应。
OPTIONS请求会发至真实请求的同一位置。如果未正确设置route,则会出现404无法找到请求地址的错误。
响应该请求的Controller的action方法可以设置为空,因为该请求的关键仅是正确返回请求头。
例如:真实请求/api/trips PUT,OPTIONS请求将发送至/api/trips OPTIONS。
match '/trips', to: 'trips#index', via: [:options]
或者可以使用:
match '*all' => 'application#cor', :constraints => {:method => 'OPTIONS'}
确保了OPTIONS请求可以正确被响应之后,在applicationController.rb中如下配置:
before_filter :cors_preflight_check
after_filter :cors_set_access_control_headers
def cors_set_access_control_headers
headers['Access-Control-Allow-Origin'] = '*'
headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, DELETE, OPTIONS'
headers['Access-Control-Max-Age'] = '1728000'
end
def cors_preflight_check
if request.method == 'OPTIONS'
headers['Access-Control-Allow-Origin'] = '*'
headers['Access-Control-Allow-Methods'] = 'POST, PUT, DELETE, GET, OPTIONS'
headers['Access-Control-Request-Method'] = '*'
headers['Access-Control-Allow-Headers'] = 'Origin, X-Requested-With, Content-Type, Accept, Authorization'
headers['Access-Control-Max-Age'] = '1728000'
render :text => '', :content_type => 'text/plain'
end
end
对于简单请求,由cors_set_access_control_headers做出正确的响应。对于不是那么简单的请求,cors_preflight_check则会发现若请求是OPTIONS的时候,在实际执行cors_set_access_control_headers之前,拦截下该请求并返回text/plain的内容和正确的请求头。
参考
- $.http.post请求为什么会变为OPTIONS请求
- 跨域请求原理分析