http接口跨域解決方案記錄

問題記錄

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可以訪問的原因


OPTIONS請求流程圖

前端解決辦法

這下我們知道問題出在哪里了。要想避免這個問題出現(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)。

image.png

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)容和正確的請求頭。

參考

  1. $.http.post請求為什么會變?yōu)镺PTIONS請求
  2. 跨域請求原理分析
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內(nèi)容