新聞中心
“前端如何解決跨域問題?” 這個是前段在知乎看到的一個提問,這幾乎是做前端都會遇到的一個問題,產(chǎn)生的情況可能會很多,解決一個問題還是要先了解下為什么會產(chǎn)生這樣問題,學(xué)習(xí)最好的方法就是結(jié)合一些實(shí)際的案例來學(xué)習(xí),理解和掌握也會更加的深刻,本文結(jié)合 Node.js 寫一些 Demo 看一下跨域問題及解決辦法,最好是自己看完也能夠動手操作下~

網(wǎng)站建設(shè)哪家好,找成都創(chuàng)新互聯(lián)公司!專注于網(wǎng)頁設(shè)計(jì)、網(wǎng)站建設(shè)、微信開發(fā)、小程序定制開發(fā)、集團(tuán)企業(yè)網(wǎng)站建設(shè)等服務(wù)項(xiàng)目。為回饋新老客戶創(chuàng)新互聯(lián)還提供了策勒免費(fèi)建站歡迎大家使用!
Cross-origin Resource Sharing 中文名稱 “跨域資源共享” 簡稱 “CORS”,它突破了一個請求在瀏覽器發(fā)出只能在同源的情況下向服務(wù)器獲取數(shù)據(jù)的限制。
本文會先從一個示例開始,分析是瀏覽器還是服務(wù)器的限制,之后講解什么時候會產(chǎn)生預(yù)檢請求,在整個過程中,也會講解一下解決該問題的實(shí)現(xiàn)方法,文末會再總結(jié)如何使用 Node.js 中的 cors 模塊和 Nginx 反向代理來解決跨域問題。
文中使用 Node.js 做一些 Demo 的演示,每一小節(jié)之后也會給予代碼的 Demo 地址。
瀏覽器還是服務(wù)器的限制
先思考下,CORS 是瀏覽器端還是服務(wù)器端的限制?為了更好的說明這個問題,從一段示例開始。
從一段示例開始
index.html
client.js
創(chuàng)建 client.js 用來加載上面 index.html。設(shè)置端口為 3010。
- const http = require('http');
- const fs = require('fs');
- const PORT = 3010;
- http.createServer((req, res) => {
- fs.createReadStream('index.html').pipe(res);
- }).listen(PORT);
server.js
創(chuàng)建 server.js 開啟一個服務(wù),根據(jù)不同的請求返回不同的響應(yīng)。設(shè)置端口為 3011。
- const http = require('http');
- const PORT = 3011;
- http.createServer((req, res) => {
- const url = req.url;
- console.log('request url: ', url);
- if (url === '/api/data') {
- return res.end('ok!');
- }
- if (url === '/script') {
- return res.end('console.log("hello world!");');
- }
- }).listen(PORT);
- console.log('Server listening on port ', PORT);
測試分析原因
運(yùn)行上面的 client.js、server.js 瀏覽器輸入 http://127.0.0.1:3010 在 Chrome 瀏覽器中打開 Network 項(xiàng)查看請求信息,如下所示:
左側(cè)是使用 fetch 請求的 127.0.0.1:3011/api/data 接口,在請求頭里可以看到有 Origin 字段,顯示了我們當(dāng)前的請求信息。另外還有三個 Sec-Fetch-* 開頭的字段,這是一個新的草案 Fetch Metadata Request Headers[1]。
其中 Sec-Fetch-Mode 表示請求的模式,通過左右兩側(cè)結(jié)果對比也可以看出左側(cè)是跨域的。Sec-Fetch-Site 表示的是這個請求是同源還是跨域,由于我們這兩個請求都是由 3010 端口發(fā)出去請求 3011 端口,是不符合同源策略的。
看下瀏覽器 Console 下的日志信息,根據(jù)提示得知原因是從 “http://127.0.0.1:3010” 訪問 “http://127.0.0.1:3011/api/data” 被 CORS 策略阻止了,沒有 “Access-Control-Allow-Origin” 標(biāo)頭。
在看下服務(wù)端的日志,因?yàn)檎埱?3011 服務(wù),所以就看下 3011 服務(wù)的日志信息:
- Server listening on port 3011
- request url: /script
- request url: /api/data
在服務(wù)端是有收到請求信息的,說明服務(wù)端是正常工作的。
我們也可以在終端通過 curl 命令測試下,在終端脫離瀏覽器環(huán)境也是可以正常請求的。
- $ curl http://127.0.0.1:3011/api/data
- ok!
本節(jié)代碼示例:
- github.com/qufei1993/http-protocol/tree/master/example/cors/01
總結(jié)回答最開始提出的問題
瀏覽器限制了從腳本內(nèi)發(fā)起的跨源 HTTP 請求,例如 XMLHttpRequest 和我們本示例中使用的 Fetch API 都是遵循的同源策略。
當(dāng)一個請求在瀏覽器端發(fā)送出去后,服務(wù)端是會收到的并且也會處理和響應(yīng),只不過瀏覽器在解析這個請求的響應(yīng)之后,發(fā)現(xiàn)不屬于瀏覽器的同源策略(地址里面的協(xié)議、域名和端口號均相同)也沒有包含正確的 CORS 響應(yīng)頭,返回結(jié)果被瀏覽器給攔截了。
預(yù)檢請求
預(yù)檢請求是在發(fā)送實(shí)際的請求之前,客戶端會先發(fā)送一個 OPTIONS 方法的請求向服務(wù)器確認(rèn),如果通過之后,瀏覽器才會發(fā)起真正的請求,這樣可以避免跨域請求對服務(wù)器的用戶數(shù)據(jù)造成影響。
看到這里你可能有疑問為什么上面的示例沒有預(yù)檢請求?因?yàn)?CORS 將請求分為了兩類:簡單請求和非簡單請求。我們上面的情況屬于簡單請求,所以也就沒有了預(yù)檢請求。
讓我們繼續(xù)在看下簡單請求和非簡單請求是如何定義的。
預(yù)檢請求定義
根據(jù) MDN 的文檔定義,請求方法為:GET、POST、HEAD,請求頭 Content-Type 為:text/plain、multipart/form-data、application/x-www-form-urlencoded 的就屬于 “簡單請求” 不會觸發(fā) CORS 預(yù)檢請求。
例如,如果請求頭的 Content-Type 為 application/json 就會觸發(fā) CORS 預(yù)檢請求,這里也會稱為 “非簡單請求”。
“MDN 文檔 developer.mozilla.org/en-US/docs/Web/HTTP/CORS 簡單請求”[2] 有更多關(guān)于簡單請求的字段定義。
預(yù)檢請求示例
通過一個示例學(xué)習(xí)下預(yù)檢請求。
設(shè)置客戶端
為 index.html 里的 fetch 方法增加一些設(shè)置,設(shè)置請求的方法為 PUT,請求頭增加一個自定義字段 Test-Cors。
上述代碼在瀏覽器執(zhí)行時會發(fā)現(xiàn)是一個非簡單請求,就會先執(zhí)行一個預(yù)檢請求,Request Headers 會有如下信息:
- OPTIONS /api/data HTTP/1.1
- Host: 127.0.0.1:3011
- Access-Control-Request-Method: PUT
- Access-Control-Request-Headers: content-type,test-cors
- Origin: http://127.0.0.1:3010
- Sec-Fetch-Mode: cors
可以看到有一個 OPTIONS 是預(yù)檢請求使用的方法,該方法是在 HTTP/1.1 協(xié)議中所定義的,還有一個重要的字段 Origin 表示請求來自哪個源,服務(wù)端則可以根據(jù)這個字段判斷是否是合法的請求源,例如 Websocket 中因?yàn)闆]有了同源策略限制,服務(wù)端可以根據(jù)這個字段來判斷。
Access-Control-Request-Method 告訴服務(wù)器,實(shí)際請求將使用 PUT 方法。
Access-Control-Request-Headers 告訴服務(wù)器,實(shí)際請求將使用兩個頭部字段 content-type,test-cors。這里如果 content-type 指定的為簡單請求中的幾個值,Access-Control-Request-Headers 在告訴服務(wù)器時,實(shí)際請求將只有 test-cors 這一個頭部字段。
設(shè)置服務(wù)端
上面講解了客戶端的設(shè)置,同樣的要使請求能夠正常響應(yīng),還需服務(wù)端的支持。
修改我們的 server.js 重點(diǎn)是設(shè)置 Response Headers 代碼如下所示:
- res.writeHead(200, {
- 'Access-Control-Allow-Origin': 'http://127.0.0.1:3010',
- 'Access-Control-Allow-Headers': 'Test-CORS, Content-Type',
- 'Access-Control-Allow-Methods': 'PUT,DELETE',
- 'Access-Control-Max-Age': 86400
- });
為什么是以上配置?首先預(yù)檢請求時,瀏覽器給了服務(wù)器幾個重要的信息 Origin、Method 為 PUT、Headers 為 content-type,test-cors 服務(wù)端在收到之后,也要做些設(shè)置,給予回應(yīng)。
Access-Control-Allow-Origin 表示 “http://127.0.0.1:3010” 這個請求源是可以訪問的,該字段也可以設(shè)置為 “*” 表示允許任意跨源請求。
Access-Control-Allow-Methods 表示服務(wù)器允許客戶端使用 PUT、DELETE 方法發(fā)起請求,可以一次設(shè)置多個,表示服務(wù)器所支持的所有跨域方法,而不單是當(dāng)前請求那個方法,這樣好處是為了避免多次預(yù)檢請求。
Access-Control-Allow-Headers 表示服務(wù)器允許請求中攜帶 Test-CORS、Content-Type 字段,也可以設(shè)置多個。
Access-Control-Max-Age 表示該響應(yīng)的有效期,單位為秒。在有效時間內(nèi),瀏覽器無須為同一請求再次發(fā)起預(yù)檢請求。還有一點(diǎn)需要注意,該值要小于瀏覽器自身維護(hù)的最大有效時間,否則是無效的。
看下增加了預(yù)檢請求的效果,第一次先發(fā)出了 OPTIONS 請求,并且在請求頭設(shè)置了本次請求的方法和 Headers 信息,服務(wù)端在 Response 也做了回應(yīng),在 OPTIONS 成功之后,瀏覽器緊跟著才發(fā)起了我們本次需要的真實(shí)請求,如圖右側(cè)所示 Resquest Method 為 PUT。
本節(jié)代碼示例:
- github.com/qufei1993/http-protocol/tree/master/example/cors/02
CORS 與認(rèn)證
對于跨域的 XMLHttpRequest 或 Fetch 請求,瀏覽器是不會發(fā)送身份憑證信息的。例如我們要在跨域請求中發(fā)送 Cookie 信息,就要做些設(shè)置:
為了能看到效果,我先自定義了一個 cookie 信息 id=NodejsRoadmap。
重點(diǎn)是設(shè)置認(rèn)證字段,本文中 fetch 示例設(shè)置 credentials: "include" 如果是 XMLHttpRequest 則設(shè)置 withCredentials:"include"
經(jīng)過以上設(shè)置,瀏覽器發(fā)送實(shí)際請求時會向服務(wù)器發(fā)送 Cookies,同時服務(wù)器也需要在響應(yīng)中設(shè)置 Access-Control-Allow-Credentials 響應(yīng)頭
- res.writeHead(200, {
- 'Access-Control-Allow-Origin': 'http://127.0.0.1:3010',
- 'Access-Control-Allow-Credentials': true
- });
如果服務(wù)端不設(shè)置瀏覽器就不會正常響應(yīng),會報(bào)一個跨域錯誤,如下所示:
Access to fetch at 'http://127.0.0.1:3011/api/data' from origin 'http://127.0.0.1:3010' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: The value of the 'Access-Control-Allow-Credentials' header in the response is '' which must be 'true' when the request's credentials mode is 'include'.
還有一點(diǎn)需要注意,如果我們在請求中設(shè)置了 credentials: "include" 服務(wù)端就不能設(shè)置 Access-Control-Allow-Origin: "*" 只能設(shè)置為一個明確的地址。
本節(jié)代碼示例:
- github.com/qufei1993/http-protocol/tree/master/example/cors/03
解決跨域問題的幾種方法
通過上面的分析了解跨域產(chǎn)生的原因之后,解決其實(shí)并不難,上面的講解中其實(shí)也提供了解決方案,例如在 Node.js 中我們可以設(shè)置響應(yīng)頭部字段 Access-Control-Allow-Origin、Access-Control-Expose-Headers、Access-Control-Allow-Methods 等,但是在實(shí)際開發(fā)中這樣設(shè)置難免繁瑣,下面介紹幾種常用的解決方法。
使用 CORS 模塊
在 Node.js 中推薦你使用 cors 模塊 github.com/expressjs/cors[3]。
在我們本節(jié)的示例中,一直使用的 Node.js 原生模塊來編寫我們的示例,在引入 cors 模塊后,可以按照如下方式改寫:
- const http = require('http');
- const PORT = 3011;
- const corsMiddleware = require('cors')({
- origin: 'http://127.0.0.1:3010',
- methods: 'PUT,DELETE',
- allowedHeaders: 'Test-CORS, Content-Type',
- maxAge: 1728000,
- credentials: true,
- });
- http.createServer((req, res) => {
- const { url, method } = req;
- console.log('request url:', url, ', request method:', method);
- const nextFn = () => {
- if (method === 'PUT' && url === '/api/data') {
- return res.end('ok!');
- }
- return res.end();
- }
- corsMiddleware(req, res, nextFn);
- }).listen(PORT);
cors 在預(yù)檢請求之后或在預(yù)檢請求里并選項(xiàng)中設(shè)置了 preflightContinue 屬性之后才會執(zhí)行 nextFn 這個函數(shù),如果預(yù)檢失敗就不會執(zhí)行 nextFn 函數(shù)。
如果你用的 Express.js 框架,使用起來也很簡單,如下所示:
- const express = require('express')
- const cors = require('cors')
- const app = express()
- app.use(cors());
JSONP
瀏覽器是允許像 link、img、script 標(biāo)簽在路徑上加載一些內(nèi)容進(jìn)行請求,是允許跨域的,那么 jsonp 的實(shí)現(xiàn)原理就是在 script 標(biāo)簽里面加載了一個鏈接,去訪問服務(wù)器的某個請求,返回內(nèi)容。
相比上面 CORS 模塊,JSONP 只支持 GET 請求,顯然是沒有 CORS 模塊強(qiáng)大的。
Nginx 代理服務(wù)器配置跨域
使用 Nginx 代理服務(wù)器之后,請求不會直接到達(dá)我們的 Node.js 服務(wù)器端,請求會先經(jīng)過 Nginx 在設(shè)置一些跨域等信息之后再由 Nginx 轉(zhuǎn)發(fā)到我們的 Node.js 服務(wù)端,所以這個時候我們的 Nginx 服務(wù)器去監(jiān)聽的 3011 端口,我們把 Node.js 服務(wù)的端口修改為 30011,簡單配置如下所示:
- server {
- listen 3011;
- server_name localhost;
- location / {
- if ($request_method = 'OPTIONS') {
- add_header 'Access-Control-Allow-Origin' 'http://127.0.0.1:3010';
- add_header 'Access-Control-Allow-Methods' 'PUT,DELETE';
- add_header 'Access-Control-Allow-Headers' 'Test-CORS, Content-Type';
- add_header 'Access-Control-Max-Age' 1728000;
- add_header 'Access-Control-Allow-Credentials' 'true';
- add_header 'Content-Length' 0;
- return 204;
- }
- add_header 'Access-Control-Allow-Origin' 'http://127.0.0.1:3010';
- add_header 'Access-Control-Allow-Credentials' 'true';
- proxy_pass http://127.0.0.1:30011;
- proxy_set_header Host $host;
- }
- }
本節(jié)代碼示例:
- github.com/qufei1993/http-protocol/tree/master/example/cors/04
總結(jié)
如果你是一個前端開發(fā)者,在工作難免會遇到跨域問題,雖然它屬于瀏覽器的同源策略限制,但是要想解決這問題還需瀏覽器端與服務(wù)端的共同支持,希望讀到這篇文章的讀者能夠理解跨域產(chǎn)生的原因,最后給予的幾個解決方案,也希望能解決你對于跨域這個問題的困惑。
作者簡介:五月君,軟件設(shè)計(jì)師,公眾號「Nodejs技術(shù)?!棺髡摺?/p>
參考資料
[1]Fetch Metadata Request Headers: https://w3c.github.io/webappsec-fetch-metadata/
[2]“MDN 文檔 developer.mozilla.org/en-US/docs/Web/HTTP/CORS 簡單請求”: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
[3]github.com/expressjs/cors: https://github.com/expressjs/cors
本文轉(zhuǎn)載自微信公眾號「Nodejs技術(shù)?!?,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系Nodejs技術(shù)棧公眾號。
分享標(biāo)題:前端如何解決跨域問題?
標(biāo)題URL:http://www.5511xx.com/article/coigpdj.html


咨詢
建站咨詢
