問題記錄
vue采用axios封裝的網(wǎng)絡(luò)請求庫中的post方法去請求接口時默認(rèn)發(fā)送了一次OPTIONS請求,然后接口狀態(tài)碼為200,但是并沒有響應(yīng)和數(shù)據(jù)返回。POST 跨域請求服務(wù)器資源的時候,控制臺報了這么一個錯:
XMLHttpRequest cannot load xxxxxxxx. Request header field Content-Type is not allowed by Access-Control-Allow-Headers in preflight response.
查看Chrome 的network請求發(fā)現(xiàn)method這個請求并不是 POST,而是 OPTIONS,突然有點懵逼,因為印象中沒用過這種請求方法。
AJAX跨域請求時的OPTIONS方法
在通過 AJAX 發(fā)起 HTTP 請求的時候,我們最常用的方法大概就是 GET 和 POST 了。實際上除了這兩個以外,HTTP 請求還有 PUT,DELETE,OPTIONS 等等。本文就將對 OPTIONS 請求的作用進(jìn)行介紹,并解決我前兩天遇到的一個與它相關(guān)的問題。
問題的根源
OPTIONS請求簡單來說,就是對于一些可能對服務(wù)器數(shù)據(jù)有影響的請求,如 PUT,DELETE 和搭配某些 MIME 類型的 POST 方法,瀏覽器必須先發(fā)送一個“預(yù)檢請求”,來確認(rèn)服務(wù)器是否允許該請求,允許的話再真正發(fā)送相應(yīng)的請求。
檢查了一下代碼確實調(diào)用的是post方法,我的 Content-Type 設(shè)置的為application/json,同時也有同時用的是 jQuery 的 $.post 請求測試了一下同一個接口都是沒有問題的,查看了下瀏覽器上面的newwork請求頭發(fā)現(xiàn) jQuery 發(fā)送的請求 Content-Type 為 application/x-www-form-urlencoded,而我這邊用 axios 發(fā)送的請求 Content-Type 為 application/json,然后再去查了一下資料發(fā)現(xiàn)發(fā)送的請求內(nèi)容類型如果不是 application/x-www-form-urlencoded,multipart/form-data 或 text/plain 這三者的話,便會觸發(fā) OPTIONS 請求,而 jQuery 發(fā)送的請求內(nèi)容類型默認(rèn)值為 application/x-www-form-urlencoded,這就是為什么ajax可以訪問的原因
前端解決辦法
這下我們知道問題出在哪里了。要想避免這個問題出現(xiàn),要么在服務(wù)器端進(jìn)行設(shè)置,要么讓請求避開上面的限制。前端這邊可以對請求的設(shè)置一下請求頭,然后對請求參數(shù)進(jìn)行一下處理:
可以使用 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 }));
服務(wù)端跨域設(shè)置
在構(gòu)建Public APIs的過程中,首先要解決的第一個問題就是跨域請求的問題。
網(wǎng)絡(luò)應(yīng)用安全模型中很重要的一個概念是“同源準(zhǔn)則”(same-origin policy)。該準(zhǔn)則要求一個網(wǎng)站(由協(xié)議+主機(jī)名+端口號三者確定)的腳本(Script)、XMLHttpRequest和Websocket無權(quán)去訪問另一個網(wǎng)站的內(nèi)容。在未正確設(shè)置的情況下,跨域訪問會提示如下錯誤: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的基本原理是通過設(shè)置HTTP請求和返回中header,告知瀏覽器該請求是合法的。這涉及到服務(wù)器端和瀏覽器端雙方的設(shè)置:請求的發(fā)起(Http Request Header)和服務(wù)器對請求正確的響應(yīng)(Http response header)。
發(fā)起CORS請求
CORS兼容以下瀏覽器:
- Internet Explorer 8+
- Firefox 3.5+
- Safari 4+
- Chrome
原生Javascript可以通過XMLHttpRequest Object或XDomainRequest發(fā)起請求,詳細(xì)的方式可以參見這篇文章:http://www.html5rocks.com/en/tutorials/cors/
JQuery的$.ajax()可以用來發(fā)起XHR或者CORS請求。然而該方法不支持IE下的XDomainRequest,需要使用JQuery的插件來實現(xiàn)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.
}
});
服務(wù)器正確響應(yīng)CORS請求
根據(jù)請求內(nèi)容的不同,瀏覽器會需要添加對應(yīng)的Header或者發(fā)起額外的請求。其中的細(xì)節(jié)都由瀏覽器負(fù)責(zé)處理,對于用戶來講是透明的。我們只需要了解如何針對差異的請求做出適當(dāng)?shù)捻憫?yīng)即可。
我們將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,均為“不是那么簡單的請求”。針對這種請求,瀏覽器會在真實請求前,額外發(fā)起一次類型為OPTIONS的請求(Preflight request),只有服務(wù)器正確響應(yīng)了OPTIONS請求后,瀏覽器才會發(fā)起該請求。(參見下圖)
[圖片上傳失敗...(image-d95c9e-1561973826637)]
下文將針對b.com向a.com發(fā)起跨域請求說明服務(wù)器如何正確響應(yīng)這兩種類型的請求。
簡單請求
瀏覽器在發(fā)出請求前為請求添加Origin來標(biāo)明請求的來源,用戶不可更改此內(nèi)容。但Header中是否有Origin并不能作為判斷是否是CORS請求的標(biāo)準(zhǔn),因為不同瀏覽器對于此內(nèi)容的處理方式并不完全一致,同源請求中也有可能出現(xiàn)Origin。
下面是一個b.com向a.com發(fā)起的一次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...
正確響應(yīng)的返回如下,均由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同意跨域訪問的域名列表。如果允許任意網(wǎng)站請求資源,此處可以寫為'*'
Access-Control-Expose-Headers: 可以設(shè)置返回的Header以傳遞數(shù)據(jù)。簡單請求中允許使用的Header包括:Cache-Control,Content-Language,Content-Type,Expires,Last-Modified,Pragma。
復(fù)雜的請求
如果希望使用PUT,DELETE等RESTful等超出了簡單請求的范圍的請求,瀏覽器則會在發(fā)起真實請求前先向服務(wù)器發(fā)起一次稱作Preflight的OPTIONS的請求,以確保服務(wù)器接受該類型請求。其后才會發(fā)起真實要求的請求。請求的發(fā)起與簡單請求并無差異,而服務(wù)器端則要針對Preflight Request做額外的響應(yīng)。
下面是一次典型的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內(nèi)容。服務(wù)器僅在驗證了這兩項內(nèi)容的合法性之后才會同意瀏覽器發(fā)起真實的請求。
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。因為每次請求均要發(fā)起一次額外的OPTIONS請求是非常低效的,因此可以為瀏覽器保存該返回頭設(shè)置一個緩存的時間,單位為秒。在緩存過期以前,瀏覽器無需再次驗證同一類型的請求是否合法。
真實請求的內(nèi)容則和簡單請求的內(nèi)容完全一致,此處不再贅述。
下圖非常詳細(xì)的再次描述了服務(wù)器對于不同類型的請求如何做出正確的響應(yīng)。
Rails下對CORS請求的配置
首先要確保在Routes.rb中加上對于OPTIONS請求的正確響應(yīng)。
OPTIONS請求會發(fā)至真實請求的同一位置。如果未正確設(shè)置route,則會出現(xiàn)404無法找到請求地址的錯誤。
響應(yīng)該請求的Controller的action方法可以設(shè)置為空,因為該請求的關(guān)鍵僅是正確返回請求頭。
例如:真實請求/api/trips PUT,OPTIONS請求將發(fā)送至/api/trips OPTIONS。
match '/trips', to: 'trips#index', via: [:options]
或者可以使用:
match '*all' => 'application#cor', :constraints => {:method => 'OPTIONS'}
確保了OPTIONS請求可以正確被響應(yīng)之后,在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做出正確的響應(yīng)。對于不是那么簡單的請求,cors_preflight_check則會發(fā)現(xiàn)若請求是OPTIONS的時候,在實際執(zhí)行cors_set_access_control_headers之前,攔截下該請求并返回text/plain的內(nèi)容和正確的請求頭。