摘要:同源策略帶來兩個問題無法獲取非本域的資源無法調(diào)取接口無法傳遞以下提供三種方案以供參考。期間存在跨域,但沒有違背同源策略。
跨域問題 配置在進行前后端分離的開發(fā)中,跨域是一個不得不解決的問題。以下基于 Vue-Resource、PHP 及 Nginx 介紹跨域問題及其解決方案。
Nginx 中的配置只是簡單的指向 PHP 代碼的所在目錄:
server{ listen 80; server_name localhost; root /mnt/apps; index index.php index.html index.htm; location / { index index.php index.html; } location ~ .php$ { fastcgi_pass localhost:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } }
PHP 只是輸出一個 JSON 數(shù)據(jù),代碼如下:
// api.php "value" ]; echo json_encode($response);
Vue-Resource 為調(diào)用該接口,試圖獲取其中的數(shù)據(jù):
this.$http.get("http://localhost:8088/api.php").then(res => { console.log(res); })測試
首先,我們在 Postman 中進行測試:
可以看到,這一接口是能夠返回預(yù)期值的。
但當(dāng)我們刷新 Vue 頁面時,控制臺中卻沒有輸出想要的值,而是拋出了錯誤:
XMLHttpRequest cannot load http://localhost:8088/api.php. No "Access-Control-Allow-Origin" header is present on the requested resource. Origin "http://localhost:8080" is therefore not allowed access.跨域問題
跨域問題基于瀏覽器的同源策略,簡而言之,就是腳本不能調(diào)用來自不同域名、不同協(xié)議、不同端口的資源。如上,來自 localhost:8080 的 JavaScript 代碼試圖獲取 localhost:8088 的 PHP 返回值(視為資源),便違背了同源策略,從而引發(fā)跨域問題。
有關(guān)同源策略的更多信息,可以參考知乎的這篇討論:對于瀏覽器的同源策略你是怎樣理解的呢?。
同源策略帶來兩個問題:
無法獲取非本域的資源(無法調(diào)取 API 接口);
無法傳遞 Cookie;
以下提供三種方案以供參考。
PS:有同學(xué)可能會有疑問,為什么在 Postman 中不會有跨域問題呢?注意,跨域問題針對的是腳本對資源的訪問限制,而 Postman 本身基于客戶端代碼,是 C/S 架構(gòu),自然不會有此問題。這就像是使用 curl 調(diào)用接口也不會受同源策略影響一樣。
方案一:Jsonp 方案當(dāng)我們談及獲取非本域資源時,可以發(fā)現(xiàn)并不是所有類型的資源都受同源策略限制的,比如圖片和 JavaScript、CSS 等。
這也使得我們可以轉(zhuǎn)換思路,采用一種取巧的方式獲得那些被同源策略拒絕的資源。比如,服務(wù)端動態(tài)地把數(shù)據(jù)放在 JavaScript 中,在前端請求時,將動態(tài)生成的 JavaScript 文件返回,文件中的內(nèi)容包含相應(yīng)的數(shù)據(jù)。
可是,單純的在 JavaScript 文件中包含數(shù)據(jù)是不能被前端獲取的。因為通過 JavaScript 代碼是不能讀取文件中的內(nèi)容的。所以,我們的思路還需要轉(zhuǎn)換一下。考慮以下場景:
引用的遠程 JavaScript 文件為:
這樣一來,通過這種方式,我們便在本地獲取到了遠程的數(shù)據(jù)。期間存在跨域,但沒有違背同源策略。
也就是說,只要后臺能夠根據(jù)前臺的請求,動態(tài)的生成一個調(diào)用特殊函數(shù)的 JavaScript 文件就可以了。具體流程如下:
在這其中,有幾個問題需要解決:
JavaScript 代碼如何才能向后臺請求 JavaScript 文件?
前后端如何商議 handle 函數(shù)的名字?
先來看第一個問題。事實上,在 JavaScript 代碼中是不能直接向后臺索要 JavaScript 文件的。除非使用 DOM 操作創(chuàng)建一個 script 標(biāo)簽,再將請求地址通過 src 填充到該標(biāo)簽之中。可即使是這樣,讓后臺動態(tài)生成 JavaScript 文件的方案還是不合適,這無疑增加了后臺的負擔(dān)。
不生成文件,如何實現(xiàn)函數(shù)調(diào)用和調(diào)用時傳參呢?
在 JavaScript 中,我們可以使用 eval() 使得字符串具有特殊意義。如 eval("handle("data")") 可以使得中間的字符串變?yōu)?handle 函數(shù)的執(zhí)行。這樣一來,后臺便不必再生成 JavaScript 文件,而只用發(fā)送字符串,再由前臺通過 eval 處理即可。
第二個問題相對容易解決,我們都知道,在進行 API 請求時,無論是 GET 還是 POST,都可以攜帶參數(shù)。也就是說,我們只需要把想要后臺使用的函數(shù)名通過參數(shù)傳遞即可,如 http://localhost:8088/api.php?callback=handle。后臺接收到請求后,取得 callback 參數(shù)即可獲得所需的函數(shù)名。
這樣一來,整個流程就變?yōu)榱耍?/p>
實現(xiàn)采用原生方法實現(xiàn)時,我們需要準(zhǔn)備一個接收函數(shù)(如 handle),以及在收到數(shù)據(jù)后使用 eval 將其包裹。這一過程實際上引入了很多與業(yè)務(wù)無關(guān)的代碼。
借助 Vue-Resource 或 jQuery 等庫,我們可以輕松地實現(xiàn) jsonp:
將原先的 JavaScript 代碼改變?yōu)椋?/p>
this.$http.jsonp("http://localhost:8088/api.php").then(res => { console.log(res); })
并將 PHP 代碼修改為:
"value" ]; $callback = $_GET["callback"]; $json = json_encode($data); echo $callback."(".$json.")";測試
此時,我們可以在看到如下結(jié)果:
如此,我們便得到了想要的數(shù)據(jù)。
注意這里的請求 URL,Vue-Resource 自動幫我們加上了 callback 參數(shù),即接收函數(shù)的名字。
如果我們想要自行指定接收參數(shù)的名字,或者在請求時添加額外的參數(shù),可以使用如下方式:
this.$http.jsonp("http://localhost:8088/api.php", { params: { param1: 1 }, jsonp: "callback" }).then(res => { console.log(res); })
在瀏覽器的控制臺中,我們可以找到此次請求的網(wǎng)絡(luò)傳輸過程:
可以看到,這里實際是發(fā)起了一次 GET 請求。
此外,由上圖可知,使用 jsonp 的方式,Cookie 值也是可以成功傳遞的。
但是,這種做法其實是存在一些問題。因為需要適配 jsonp 的需求,返回值實際上變成了接收函數(shù)與實際數(shù)據(jù)的字符串拼接:
這確實是解決了跨域的需求,但對于不跨域的請求,就需要另行處理了。
另外,由于本身不能指定請求類型,采用 jsonp 難以進行 RESTful 風(fēng)格的 API 請求(除非使用請求頭方法覆蓋),因而對于愈發(fā)流行的 API 請求范式,這一方式也顯得有些過時。
方案二:服務(wù)端代碼中增加響應(yīng)頭 方案同源策略的目的是為了安全性,那么有沒有一種方法,使得客戶端和服務(wù)器之間彼此信任,從而同意對方跨域訪問呢?
以下討論通過添加響應(yīng)頭的方式解決跨域問題。
實現(xiàn) 跨域解決首先我們將 PHP 和 JavaScript 代碼還原:
header("Content-type : application/json"); $data = [ "key" => "value" ]; echo json_encode($data);
this.$http.get("http://localhost:8088/api.php").then(res => { console.log(res); })
這時瀏覽器又會提示出現(xiàn)跨域問題。
接著我們?yōu)?PHP 代碼增加一條語句:
header("Access-Control-Allow-Origin : *");
此時便可以得到期望的返回值了。可以注意到,我們此時并沒有修改前臺代碼。
PUT、DELETE 等復(fù)雜請求問題下面,我們將代碼稍作修改,將前端請求方式改為 PUT:
this.$http.put("http://localhost:8088/api.php").then(res => { console.log(res); })
這時,瀏覽器又拋出了跨域錯誤。為了解決這一問題,我們還需要給 PHP 代碼加入一個響應(yīng)頭:
header("Access-Control-Allow-Methods : PUT");
當(dāng)加入這一響應(yīng)頭后,瀏覽器依然會拋出跨域問題,只不過這一問題現(xiàn)在變成了:
XMLHttpRequest cannot load http://localhost:8088/api.php. Request header field Content-Type is not allowed by Access-Control-Allow-Headers in preflight response.
提示我們,Content-type 這一請求頭不被允許。
針對這一問題,我們按照提示,在 PHP 代碼再增加一個響應(yīng)頭:
header("Access-Control-Allow-Headers : Content-type");
此時,跨域問題便解決了。注意,這里不能使用 * 號,所需增加的內(nèi)容需要根據(jù)實際情況。
為什么采用 GET 請求的時候,不需要這一條代碼呢?這是由于 HEAD、GET、POST 類型的請求為簡單請求,只需增加 Access-Control-Allow-Origin: * 即可。但除此之外的請求方式均為復(fù)雜請求。在復(fù)雜請求發(fā)起時,瀏覽器會首先發(fā)送 OPTIONS 類型的請求,詢問瀏覽器是否同意跨域,以及允許跨域的條件(OPTIONS 無需寫入 Access-Control-Allow-Methods 中),這一步被稱為預(yù)請求(preflight request)。
就上面的情況而言,因為服務(wù)端沒有設(shè)置是否允許復(fù)雜請求及具有特殊 Content-type 頭的請求進行跨域,所以請求被攔截了下來。
通過瀏覽器的控制臺我們可以更清晰地看到這些過程:
首先發(fā)送了一次 OPTIONS 請求:
然后才是真正的 PUT 請求:
同理,當(dāng)使用 DELETE 等其他復(fù)雜請求時,只需修改響應(yīng)頭即可。
關(guān)于這一點,這篇博客有著非常詳細的介紹:CORS 跨域 access-control-allow-headers 的問題 - CSDN。
emulateHTTP 與 emulateJSON在 Vue-Resource 中,提供了這兩種參數(shù)。
其中,前者可以將 PUT、DELETE 和 PATCH 請求轉(zhuǎn)換為 POST,并通過 X-HTTP-Method-Override 請求頭標(biāo)識真實的請求類型。這種做法可以兼容一些舊版本的協(xié)議。
而后者可以將請求的 body 使用 application/x-www-form-urlencoded 編碼。有關(guān) x-www-form-urlencoded 可以參考 form-data、x-www-form-urlencoded、raw、binary的區(qū)別。
Cookie 問題通過瀏覽器的控制臺,或是將后臺代碼改為:
header("Access-Control-Allow-Origin : *"); header("Access-Control-Allow-Methods : PUT"); header("Access-Control-Allow-Headers : Content-type"); header("Content-typ : application/json"); $data = [ "key" => "value" ]; $data = $_COOKIE; echo json_encode($data);
我們可以檢測到,這次的請求并沒有攜帶 Cookie 進行發(fā)送。而沒有 Cookie 會使得大量需求無法實現(xiàn)。那么該如何解決這一問題呢?
此時,我們需要對前端和后臺代碼同時進行修改:
修改 JavaScript 代碼為:
this.$http.put("http://localhost:8088/api.php",{}, { credentials: true, }).then(res => { console.log(res); })
注意,在 credentials: true 的前面還有一個 {},這是因為對于 PUT、POST、DELETE 等可以攜帶 body 體參數(shù)的請求而言,其第二個參數(shù)為 body 參數(shù)項,其他配置需要放在第三個參數(shù)中。
對于 GET 請求,我們則需要把它放到第二個參數(shù)中:
this.$http.get("http://localhost:8088/api.php",{ credentials: true, }).then(res => { console.log(res); })
在 PHP 代碼中,我們需要增加一個響應(yīng)頭:
header("Access-Control-Allow-Credentials: true");
此時,刷新瀏覽器可以發(fā)現(xiàn)又拋出了一個錯誤:
XMLHttpRequest cannot load http://localhost:8088/api.php. Response to preflight request doesn"t pass access control check: The value of the "Access-Control-Allow-Origin" header in the response must not be the wildcard "*" when the request"s credentials mode is "include". Origin "http://localhost:8080" is therefore not allowed access. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.
這提示我們,當(dāng)使用 credentials 時,Access-Control-Allow-Origin 的響應(yīng)頭不能設(shè)置為 *。此時,我們需要把原本的后臺設(shè)置改為:
$origin = $_SERVER["HTTP_ORIGIN"]; header("Access-Control-Allow-Origin : ".$origin);
即前端域名為什么,后臺便允許什么跨域。
這樣一來,我們便又可以正常的得到 Cookie 值了:
通過這種方式依然需要我們增加很多與業(yè)務(wù)無關(guān)的代碼。當(dāng)然了,我們可以通過在后臺框架中增加中間件的方式為響應(yīng)結(jié)果統(tǒng)一添加響應(yīng)頭。雖然這種方式確實很方便,但當(dāng)后臺返回的狀態(tài)碼不是 2xx,即后臺報錯或進行重定向時,瀏覽器收到的結(jié)果依然會變成跨域錯誤。這種情況使得我們無法在測試時準(zhǔn)確的知道哪里出現(xiàn)了問題。
為了更進一步的改進跨域方案,我們試著將增加響應(yīng)頭的工作交給服務(wù)器來做,如 Apache 或 Nginx。
方案三:使用 Nginx 配置的方式解決跨域在修改 Nginx 配置前,我們先將 PHP 代碼還原:
header("Content-type : application/json"); $data = $_COOKIE; echo json_encode($data);
注意,JavaScript 代碼與之前相同。如需考慮 Cookie 問題,還是需要在異步請求中中加入 credentials: true 的。
然后,我們修改 Nginx 配置為:
server{ listen 80; server_name localhost; root /mnt/apps; index index.php index.html index.htm; location / { index index.php index.html; } location ~ .php$ { add_header "Access-Control-Allow-Origin" "$http_origin"; add_header "Access-Control-Allow-Credentials" "true"; add_header "Access-Control-Allow-Methods" "PUT, DELETE"; add_header "Access-Control-Allow-Headers" "Content-type"; fastcgi_pass localhost:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } }
注意,這里主要的代碼在于通過 Nginx 為響應(yīng)附加響應(yīng)頭:
add_header "Access-Control-Allow-Origin" "$http_origin"; add_header "Access-Control-Allow-Credentials" "true"; add_header "Access-Control-Allow-Methods" "PUT, DELETE"; add_header "Access-Control-Allow-Headers" "Content-type";
而添加的響應(yīng)頭類型均與之前在 PHP 代碼中的改動一致。
如此,也可以解決跨域問題。
靜態(tài)資源文件跨域問題在進行前端開發(fā)時,很可能會使用字體圖標(biāo),當(dāng)我們試圖獲取非本域的如 iconfont.eot 等資源時,很可能也會在控制臺中看到跨域拒絕的報錯信息。
諸如此類的問題都可以使用添加響應(yīng)頭來實現(xiàn)。只不過對于這種靜態(tài)資源文件,由于它們都是 GET 請求,所以我們只需要在服務(wù)器中添加 Access-Control-Allow-Origin: *。如在 Nginx 中:
location ~* .(eot|otf|ttf|woff|svg)$ { add_header "Access-Control-Allow-Origin" "*"; }
這樣,在前端請求這些后綴名的資源文件時,便不會出現(xiàn)報錯信息了。
后記:Laravel 的坑最近在使用 Laravel 時,發(fā)現(xiàn)了一個詭異的現(xiàn)象:當(dāng)在 Laravel 中使用中間件進行跨域時(代碼如下):
class CORSProtection { public function handle($request, Closure $next) { $response = $next($request); if(isset($_SERVER["HTTP_ORIGIN"])){ $origin = $_SERVER["HTTP_ORIGIN"]; $response->header("Access-Control-Allow-Origin", $origin); } $response->header("Access-Control-Allow-Headers", "Content-Type, X-Requested-With"); $response->header("Access-Control-Allow-Credentials", "true"); $response->header("Access-Control-Allow-Methods", "POST, PUT, DELETE, OPTIONS"); return $response; } }
來自前端的跨域請求:GET 和不帶參數(shù)的 POST 都可以正常發(fā)送。而一旦 POST 中攜帶了參數(shù),瀏覽器就會輸出跨域錯誤。
通過查看瀏覽器的網(wǎng)絡(luò),發(fā)現(xiàn)在 POST 請求前確實發(fā)送了一次 OPTIONS 請求,但該請求的響應(yīng)并沒有按我們之前所說的攜帶上允許跨域的信息頭。
通過查閱資料發(fā)現(xiàn),Laravel 會對 OPTIONS 請求自動返回 200 狀態(tài)碼而無視中間件或其他形式的響應(yīng)頭附加。詳見參考:關(guān)于 Laravel 下 Cors 跨域 POST 請求的一種實現(xiàn)方法。
此時,我們需要在路由中強制加入對 OPTIONS 類請求的響應(yīng),以使得 OPTIONS 探測請求能夠正確的響應(yīng)我們想要的跨域允許信息:
Route::options("{any}", function ($any) { return response("ok"); })->middleware("cors");
由于 Laravel 中似乎沒有缺省路由,這里需要根據(jù)請求 URL 的層級添加不同的路由。
但是,為什么 GET(無論帶參與否)以及不帶參的 POST 請求都沒有出現(xiàn)這一問題呢?還記得前面提到的復(fù)雜請求和簡單請求嗎?GET 和 POST 雖然都是簡單請求,但當(dāng) POST 攜帶參數(shù)時,由于大多數(shù)前端 HTTP 請求框架的默認 POST 帶參請求頭 Content-Type 都是 application/json,不屬于簡單請求的類型,因而觸發(fā)了 OPTIONS 探測。而 Laravel 會默認對 OPTIONS 請求返回 200 狀態(tài),而不是攜帶我們定義好的那些響應(yīng)頭,于是就出現(xiàn)了上述詭異的情況。
這里要說明的是,使用 Nginx 方案是不會遇到這一問題的。此外,在請求中添加請求頭,強制 Content-Type 為 x-www-form-urlencoded 或其他簡單請求類型時(如 Vue-Resource 中使用 emulateJSON: true),也可以跨過這一問題。
JSON數(shù)據(jù)的HTTP Header應(yīng)該怎么標(biāo)記? - segmentfault
Vue2.0 vue-source.js jsonp demo vue跨域請求 - 博客園
說說JSON和JSONP,也許你會豁然開朗 - 博客園
CORS 跨域 access-control-allow-headers 的問題 - CSDN
跨域(CORS) 解決方案中,為什么 Access-Control-Allow-Methods 不起作用? - segmentfault
使用withCredentials發(fā)送跨域請求憑據(jù) - iteye
PHP Ajax 跨域問題最佳解決方案 - 博客園
Nginx CORS實現(xiàn)JS跨域 - CSDN
關(guān)于Laravel下Cors跨域POST請求的一種實現(xiàn)方法
PHP and laravel知識點小小積累 - 博客園
關(guān)于 Content-Type:application/x-www-form-urlencoded 和 Content-Type:multipart/related - 博客園
四種常見的 POST 提交數(shù)據(jù)方式
Laravel-Ajax-AcrossDoamin (跨域) Post傳Json - 博客園
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/23211.html
摘要:關(guān)于,強烈推薦閱讀跨域資源共享詳解阮一峰另外,這里也整理了一個實現(xiàn)原理圖簡化版如何判斷是否是簡單請求瀏覽器將請求分成兩類簡單請求和非簡單請求。 前言 從剛接觸前端開發(fā)起,跨域這個詞就一直以很高的頻率在身邊重復(fù)出現(xiàn),一直到現(xiàn)在,已經(jīng)調(diào)試過N個跨域相關(guān)的問題了,16年時也整理過一篇相關(guān)文章,但是感覺還是差了點什么,于是現(xiàn)在重新梳理了一下。 個人見識有限,如有差錯,請多多見諒,歡迎提出iss...
摘要:在接觸前端開發(fā)起,跨域這個詞就一直以很高的頻率在我們學(xué)習(xí)工作中重復(fù)出現(xiàn),最近在工作中遇到了跨域的相關(guān)問題,這里我把它總結(jié)記錄一下。 在接觸前端開發(fā)起,跨域這個詞就一直以很高的頻率在我們學(xué)習(xí)工作中重復(fù)出現(xiàn),最近在工作中遇到了跨域的相關(guān)問題,這里我把它總結(jié)記錄一下。關(guān)于跨域,有N種類型,現(xiàn)在我只專注于ajax請求跨域(ajax跨域只是屬于瀏覽器同源策略中的一部分,其它的這里不做介紹),內(nèi)容...
摘要:跨域完全講解今天在慕課網(wǎng)上學(xué)習(xí)了跨域完全講解我在收集面試題的時候其實就已經(jīng)有過跨域的問題的了,當(dāng)時候知道了為什么會存在跨域,以及跨域解決的方案有哪些,今天隨著課程的學(xué)習(xí),又加深了跨域的理解,以此記錄下來。 AJAX跨域完全講解 今天在慕課網(wǎng)上學(xué)習(xí)了AJAX跨域完全講解:https://www.imooc.com/learn/947 我在收集AJAX面試題的時候其實就已經(jīng)有過AJAX跨域...
摘要:跨域完全講解今天在慕課網(wǎng)上學(xué)習(xí)了跨域完全講解我在收集面試題的時候其實就已經(jīng)有過跨域的問題的了,當(dāng)時候知道了為什么會存在跨域,以及跨域解決的方案有哪些,今天隨著課程的學(xué)習(xí),又加深了跨域的理解,以此記錄下來。 AJAX跨域完全講解 今天在慕課網(wǎng)上學(xué)習(xí)了AJAX跨域完全講解:https://www.imooc.com/learn/947 我在收集AJAX面試題的時候其實就已經(jīng)有過AJAX跨域...
閱讀 3593·2021-11-23 09:51
閱讀 2795·2021-11-23 09:51
閱讀 676·2021-10-11 10:59
閱讀 1672·2021-09-08 10:43
閱讀 3223·2021-09-08 09:36
閱讀 3289·2021-09-03 10:30
閱讀 3293·2021-08-21 14:08
閱讀 2195·2021-08-05 09:59