圖解|一個跨域問題給我整懵了
本文轉載自微信公眾號「業(yè)余碼農」,作者Amazing10 。轉載本文請聯(lián)系業(yè)余碼農公眾號。
大家好,我是業(yè)余碼農。最近在做全棧項目的時候,遇到一個問題。
由于是前后端分離的項目,所以前后端實際上會運行在不同的域名上。如果是在本地開發(fā),前后端也會分別部署在不同的端口。
這個時候,前端直接請求后端接口,就會遇到所謂的跨域問題。
跨域錯誤
同源策略
說到跨域,首先需要解釋下為什么會出現(xiàn)這樣的跨域問題。這其實都源于瀏覽器的同源策略。
同源策略是瀏覽器中的一個重要的安全策略,是Netscape公司在1995年引入。同源策略的作用就是為了限制不同源之間的交互,從而能夠有效避免XSS、CSFR等瀏覽器層面的攻擊。
同源指的是兩個請求接口URL的協(xié)議(protocol)、域名(host)和端口(port)一致。
同源策略
比如以下例子:
同源與非同源接口
說到瀏覽器的攻擊手段,XSS指的是惡意攻擊者往Web頁面里插入惡意HTML代碼,利用的是用戶對指定網(wǎng)站的信任。
而CSFR指的是跨站請求偽造,是攻擊者通過一些技術手段欺騙用戶的瀏覽器去訪問一個自己曾經認證過的網(wǎng)站并執(zhí)行一些操作(如發(fā)郵件,發(fā)消息,甚至財產操作如轉賬和購買商品)。
由于瀏覽器曾經認證過,所以被訪問的網(wǎng)站會認為是真正的用戶操作而去執(zhí)行。這利用了Web中用戶身份驗證的一個漏洞:簡單的身份驗證只能保證請求是發(fā)自某個用戶的瀏覽器,卻不能保證請求本身是用戶自愿發(fā)出的。這實際上利用的是網(wǎng)站對用戶網(wǎng)頁瀏覽器的信任。
所以根據(jù)瀏覽器的是否同源判定,可以有選擇的限制網(wǎng)站的一些行為。比如非同源的站點會被限制訪問cookie、localStorage以及IndexDB,同時也無法獲取網(wǎng)頁DOM以及JavaScript對象,甚至AJAX的請求也會被攔截。
這樣一來,就能夠有效限制利用瀏覽器以及歷史訪問站點來進行攻擊的目的。
跨域問題
本質上瀏覽器不允許跨域請求好像是件好事,因為這樣對于前端來說更安全。人家辛辛苦苦設計的同源策略,為啥會成為咱們的一個問題呢。
其實這也是沒辦法的事,畢竟前后端分離的項目,迫不得已就是需要進行跨域請求。比如在本地開發(fā)的環(huán)境中,很有可能端口號不一樣。
本地環(huán)境中的跨域問題
在線上環(huán)境中,也同樣會出現(xiàn)這樣的情況。
線上環(huán)境中的跨域問題
解決方案
1 JSONP跨域
上面所提到的跨域問題其實都是因為使用了AJAX/XMLHttpRequest/Fetch API的方式來發(fā)起請求,但是其實在Web頁面上調用JS文件是不受跨域的影響的。不僅如此,擁有src屬性的標簽都擁有跨域的能力。
于是JSONP就是利用上述特點的跨域解決方案。JSONP的原理就是通過發(fā)送帶有Callback參數(shù)的GET請求,服務端將接口返回數(shù)據(jù)拼湊到Callback函數(shù)中,返回給瀏覽器,瀏覽器解析執(zhí)行,從而前端拿到Callback函數(shù)返回的數(shù)據(jù)。
JSONP跨域
前端代碼只需要在頁面中插入<script>標簽,定義好回調函數(shù),同時通過src請求后端接口即可:
- <script>
- var script = document.createElement('script');
- script.type = 'text/javascript';
- script.src = 'http://www.b.domain.com:8080/main?callback=handleCallback';
- document.head.appendChild(script);
- // 回調函數(shù)
- function handleCallback(res) {
- alert(JSON.stringify(res));
- }
- </script>
后端則需要在對應接口將客戶端發(fā)送的callback參數(shù)作為函數(shù)名來包裹住JSON數(shù)據(jù),返回數(shù)據(jù)至客戶端。
- handleCallback({"error": 0, "status": “0"})
JSONP看起來很方便,但是實際上限制很大。由于script、img這些帶src屬性的標簽,在引入外部資源時,使用的都是GET請求。所以JSONP也只能使用GET發(fā)送請求,這也是這種方式已經逐漸被淘汰的原因。
2 代理跨域
既然跨域問題是瀏覽器自己的一種保護措施,那么實際上能夠通過在前后端之間加一道代理層來變相進行跨域請求。
代理跨域
Webpack Server代理
在webpack中可以通過配置proxy來快速獲得接口代理的能力,同時前端請求的URL不需要帶域名,代理服務器會自動自動將請求映射為同域請求。
可在前端webpack.config.js配置代理:
- module.exports = {
- ...
- output: {...},
- devServer: {
- port: 3000,
- proxy: {
- "/api": {
- target: "http://localhost:3001"
- }
- }
- },
- plugins: []
- };
Nginx反向代理
實現(xiàn)思路其實與webpack代理一致,無非是通過Nginx作為跳板機而已。
Nginx反向代理
舉個Nginx配置的例子,就是把本地端口3000代理到3001,這樣在本地就能夠進行跨域調試了。
- server {
- listen 3000;
- server_name localhost;
- location /api {
- proxy_pass http://localhost:3001; #反向代理
- }
- }
原理都是類似的,只不過是將代理操作設置在了后端。若是node項目的話,可以直接利用http-proxy-middleware插件進行代理。本質上webpack也是用這個包做代理服務的,只不過現(xiàn)在把這個放在服務端。
node中間件代理
一個node+express+http-proxy-middleware的例子:
- var express = require('express');
- var proxy = require('http-proxy-middleware');
- var app = express();
- app.use('/', proxy({
- // 代理跨域目標接口
- target: 'http://localhost:3001',
- changeOrigin: true,
- // 修改響應頭信息,實現(xiàn)跨域并允許帶cookie
- onProxyRes: function(proxyRes, req, res) {
- res.header('Access-Control-Allow-Origin', 'localhost');
- res.header('Access-Control-Allow-Credentials', 'true');
- },
- }));
- app.listen(3000);
3 CORS跨域
CORS(Cross-Origin Resource Sharing)是指跨域資源共享,它是一個瀏覽器側的機制,能夠允許服務器標示除了它自己以外的其它域,這樣瀏覽器就可以進行跨域訪問加載資源。
一般現(xiàn)代瀏覽器都支持CORS跨域,只有那種古老的瀏覽器,比如IE10以下的才不支持。
這意味著,實際上瀏覽器雖然會采取同源策略來限制跨域訪問,但是同時又給服務端提供了一個選擇,即通過CORS來可選的提供跨域能力。
CORS跨域
比如上圖這個例子,左邊代表的是前端網(wǎng)頁,右邊代表的是服務器。前端部署在domain-a域名下,但是有兩個資源需要請求來自不同域名的資源。
當資源來源與前端本身所在的域不一致時,便會發(fā)生跨域請求。此時可通過CORS來控制是否允許進行跨域資源的請求。
CORS是瀏覽器提供的能力,而實現(xiàn)CORS的控制和通信是在服務端進行。也就是只要服務端對相應域允許CORS,那么便可進行跨域通信。
簡單請求
瀏覽器根據(jù)請求方法以及HTTP頭部信息將CORS請求分成兩類,簡單請求和非簡單請求。
若滿足下列條件,則視為簡單請求:
- 請求方法屬于GET、POST、HEAD中的一種
- HTTP頭部僅包含Accept、Accept-Language、Content-Language、Content-Type。
其中Content-Type的值僅限于text/plain multipart/form-data application/x-www-form-urlencoded
簡單請求
- 請求中的任意XMLHttpRequestUpload對象均沒有注冊任何事件監(jiān)聽器;XMLHttpRequestUpload對象可以使用XMLHttpRequest.upload屬性訪問。
- 請求中沒有使用ReadableStream對象。
簡單請求的流程很簡單:
簡單請求流程
- 瀏覽器發(fā)出CORS請求時,在頭部添加Origin字段(最后一行),表明請求域:比如
- GET /resources/public-data/ HTTP/1.1
- Host: bar.other
- User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre
- Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
- Accept-Language: en-us,en;q=0.5
- Accept-Encoding: gzip,deflate
- Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
- Connection: keep-alive
- Referer: http://foo.example/examples/access-control/simpleXSInvocation.html
- Origin: http://foo.exampl
服務端收到Origin,決定是否同意跨域請求:
- 如果同意請求,服務端會在返回的響應中添加CORS相關的頭部,比如其中Access-Control-Allow-Origin是必須字段,表示能接受請求的域名,*表示任意域名。
- 若不同意請求,服務端會返回一個正常的HTTP響應,由于不包含Access-Control-Allow-Origin,會被瀏覽器發(fā)現(xiàn),從而拋出本文最上面的錯誤。
- HTTP/1.1 200 OK
- Date: Mon, 01 Dec 2008 00:23:53 GMT
- Server: Apache/2.0.61
- Access-Control-Allow-Origin: *
- Keep-Alive: timeout=2, max=100
- Connection: Keep-Alive
- Transfer-Encoding: chunked
- Content-Type: application/xml
非簡單請求
不滿足簡單請求之外的請求,都是非簡單請求。
非簡單請求的特點是,在發(fā)送正式請求之前,會先發(fā)起一個預檢請求。只要當服務端同意預檢請求時,才會發(fā)送正式的請求。
比如這里舉一個例子,假設需要發(fā)送一個請求,該請求包含自定義的頭部字段X-PINGOTHER,同時Content-Type為application/xml。
可以看出這個請求是妥妥的非簡單請求,所以需要走預檢流程。
- 預檢請求用的是OPTIONS請求方法,在請求頭部會表明Origin,同時還需要帶上兩個特殊的頭部字段。
- Access-Control-Request-Method:用于表明正式請求會用到哪些HTTP請求方法,比如例子中的POST;
- Access-Control-Request-Headers:用于表明正式請求會用哪些額外發(fā)送的頭部字段,比如例子中的X-PINGOTHER和Content-Type。
- 服務端接收到預檢請求后,會檢查上面這幾個字段,確定是否可以接受跨域請求。如果可以,就會在響應的頭部中添加Access-Control-Request-Method和Access-Control-Request-Headers字段用來表示可接受的請求方法和請求頭。
- 瀏覽器接收到了預檢成功的響應后,才會開始發(fā)起正式請求,正式請求的過程就跟簡單請求基本一致了。也就是在請求頭中添加Origin字段,同時服務端的響應也返回相應的CORS必須字段。
非簡單請求流程
雖然說CORS跨域的方案是瀏覽器支持的機制,但是實現(xiàn)確實在服務端。但是其實工作量并不大,只需要設置允許跨域的域名、HTTP頭部以及請求方式等參數(shù)即可。
比如在node+express的項目只需要添加以下代碼就可以實現(xiàn)任意域名跨域的目的。
- app.all('*', function (req: express.Request, res: express.Response, next: express.NextFunction) {
- //設置允許跨域的域名,*代表允許任意域名跨域
- res.header("Access-Control-Allow-Origin", "*");
- //允許的header類型
- res.header("Access-Control-Allow-Headers", "*");
- //跨域允許的請求方式
- res.header("Access-Control-Allow-Methods", "DELETE,PUT,POST,GET,OPTIONS");
- if (req.method.toLowerCase() === 'options')
- res.sendStatus(200) //讓options嘗試請求快速結束
- else
- next()
- })
總結
跨域問題是Web開發(fā)中很常見的問題,解決起來其實也并不復雜。除了上面的方案,也還存在像Iframe、postMessage以及websocket等方案。
但是總的來說不如上面這三種常用,一個比較正經的前后端分離項目更多的還是使用CORS方案進行跨域。省時省力又省心。