本文原作者: Wizey,作者博客:http://wenshixin.gitee.io,即時通訊網(wǎng)收錄時有改動,感謝原作者的無私分享。
1、引言
典型的Web端即時通訊技術(shù)應(yīng)用場景,主要有以下兩種形式:
- 1)作為完整的即時通訊產(chǎn)品進(jìn)行應(yīng)用:比如獨立的Web端IM產(chǎn)品;2)作為某個更大系統(tǒng)中的一部分進(jìn)行應(yīng)用:比如客服系統(tǒng)(相當(dāng)于工單系統(tǒng)里嵌入IM技術(shù)啦)。
對于第一種場景,為了更好的劃分功能邏輯,一個完整的產(chǎn)品通常都會調(diào)用來自于不同服務(wù)器提供的各種接口(比如各種服務(wù)端微服務(wù)接口),那么Web端跨域問題就無法回避了。
對于第二種場景,就更好理解:為了提升系統(tǒng)的可維護(hù)性,不同子系統(tǒng)間代碼的互不傾入、低偶合設(shè)計,導(dǎo)致im子系統(tǒng)或服務(wù)很可能部署于獨立的一臺或多臺服務(wù)器(域名)上,那么跨域問題顯而易見。
所以,對于Web端即時通訊開發(fā)者來說,跨域問題是必須掌握的知識范疇。本文將為你講解跨域問題原理,以及理論聯(lián)系實際,用實踐代碼也為你演示解決跨域問題的幾種方法。
PS:雖然在開發(fā)Web端即時通訊應(yīng)用時,普通的Ajax調(diào)用、iframe文件上傳等存在跨域問題,但好消息是作為技術(shù)核心的 WebSocket 技術(shù)是支持跨域的(不存在跨域問題)!
友情提示:本文配套的實踐代碼,請從文末附件處下載!

(本文同步發(fā)布于:http://www.52im.net/thread-2732-1-1.html)
2、什么是跨域問題
前端調(diào)用的后端接口不屬于同一個域(域名或端口不同),就會產(chǎn)生跨域問題,也就是說你的應(yīng)用訪問了該應(yīng)用域名或端口之外的域名或端口。

通俗的講,跨域問題是因為瀏覽器的同源策略規(guī)定某域下的客戶端在沒明確授權(quán)的情況下,不能讀寫另一個域的資源。而在實際開發(fā)中,前后端常常是相互分離的,并且前后端的項目部署也常常不在一個服務(wù)器內(nèi)或者在一個服務(wù)器的不同端口下。前端想要獲取后端的數(shù)據(jù),就必須發(fā)起請求,如果不做一些處理,就會受到瀏覽器同源策略的約束。后端可以收到請求并返回數(shù)據(jù),但是前端無法收到數(shù)據(jù)。
3、為什么會發(fā)生跨域問題
要同時滿足三個條件才會產(chǎn)生跨域問題:
- 1)瀏覽器限制,而不是服務(wù)端限制,可以查看Network,請求能夠正確響應(yīng),response返回的值也是正確的;2)請求地址的域名或端口和當(dāng)前訪問的域名或端口不一樣;3)發(fā)送的是XHR(XMLHttpRequest)請求,可以使用 a 標(biāo)簽(模擬xhr請求)和 img 標(biāo)簽(模擬json請求)做對比(控制臺只報了一個跨域異常)。

關(guān)于 XMLHTTPRequest 可以參看這篇文章 :《你真的會使用XMLHttpRequest嗎?》。
跨域問題的根本,就是瀏覽器制定的同源策略導(dǎo)致的。
瀏覽器制定同源策略,其中一個重要原因就是對cookie的保護(hù)。
cookie 中存著sessionID 。黑客一旦獲取了sessionID,并且在有效期內(nèi),就可以登錄。當(dāng)我們訪問了一個惡意網(wǎng)站 如果沒有同源策略 那么這個網(wǎng)站就能通過js 訪問document.cookie 得到用戶關(guān)于的各個網(wǎng)站的sessionID 其中可能有銀行網(wǎng)站 等等。通過已經(jīng)建立好的session連接進(jìn)行攻擊,比如CSRF攻擊。
這里需要服務(wù)端配合再舉個例子,現(xiàn)在我扮演壞人 我通過一個iframe 加載某寶的登錄頁面 等傻傻的用戶登錄我的網(wǎng)站的時候 我就把這個頁面彈出用戶一看 阿里唉大公司 肯定安全 就屁顛屁顛的輸入了密碼 注意 如果沒有同源策略 我這個惡意網(wǎng)站就能通過dom操作獲取到用戶輸入的值 從而控制該賬戶所以同源策略是絕對必要的。
還有需要注意的是同源策略無法完全防御CSRF(即(Cross-site request forgery)跨站請求偽造)。
4、解決跨域問題的三種思路
- 1)客戶端瀏覽器解除跨域限制:此方式理論上可以但是不現(xiàn)實;2)發(fā)送JSONP請求替代XHR請求:此種方式雖然有一定的局限性——比如請求只能是GET方式,但對于部署來說很友好,因為不需要修改服務(wù)器配置;3)修改服務(wù)器端配置(包括HTTP服務(wù)器和應(yīng)用服務(wù)器):此方式對于GET、POST請求來說,沒有局限性,但對于部署來說不太友好,需要修改應(yīng)用服務(wù)器、反向代理服務(wù)器的相關(guān)配置。
5、跨域問題解決方法1:設(shè)置瀏覽器解除跨域限制
瀏覽器默認(rèn)都是開啟跨域安全檢查的,我們可以使用命令行啟動瀏覽器,加上禁止安全檢查的參數(shù),以谷歌瀏覽器為例,chrome.exe --disable-web-security --user-data-dir=E:/temp --user-data-dir 為瀏覽器緩存臨時目錄,瀏覽器這時會提示安全問題。

【瀏覽器如何判斷一個請求是不是跨域請求?】
瀏覽器會根據(jù)同源策略來判斷一個請求是不是跨域請求。
非跨域請求:在請求頭中會只包含請求的主機(jī)名:

跨域請求:在請求頭中會既包含要請求的主機(jī)名還包括當(dāng)前的源主機(jī)名,如果這兩者不一致,那就是跨域請求了:

【瀏覽器對請求的分類】
在HTTP1.1 協(xié)議中的,請求方法分為GET、POST、PUT、DELETE、HEAD、TRACE、OPTIONS、CONNECT 八種。瀏覽器根據(jù)這些請求方法和請求類型將請求劃分為簡單請求和非簡單請求。
簡單請求:瀏覽器先發(fā)送(執(zhí)行)請求然后再判斷是否跨域。
請求方法為 GET、POST、HEAD,請求頭header中無自定義的請求頭信息,請求類型Content-Type 為 text/plain、multipart/form-data、Application/x-www-form-urlencoded 的請求都是簡單請求。
非簡單請求:瀏覽器先發(fā)送預(yù)檢命令(OPTIONS方法),檢查通過后才發(fā)送真正的數(shù)據(jù)請求。

預(yù)檢命令會發(fā)送自定義請求頭為Access-Control-Request-Headers: content-type的請求到服務(wù)器,根據(jù)響應(yīng)頭的中的 “Access-Control-Allow-Headers”: “Content-Type” 判斷服務(wù)器是否允許跨域訪問。預(yù)檢命令是可以緩存,服務(wù)器端設(shè)置 “Access-Control-Max-Age”: “3600”,這樣后面發(fā)送同樣的跨域請求就不需要先發(fā)送預(yù)檢命令了。
請求頭的含義如下所示:

響應(yīng)頭的含義如下所示:

請求方法為 PUT、DELETE 的 AJAX 請求、發(fā)送 JSON 格式的 AJAX 請求、帶自定義頭的 AJAX 請求都是非簡單請求。
6、跨域問題解決方法2:使用JSONP替代XHR
6.1 JSONP 是什么
JSONP(JSON with Padding)是JSON的一種補充使用方式,不是官方協(xié)議,而是利用 Script 標(biāo)簽請求資源可以跨域的特點,來解決跨域問題的,是一種變通的解決方案。(詳見《詳解Web端通信方式的演進(jìn):從Ajax、JSONP 到 SSE、Websocke》一文中的第3節(jié)“三、JSONP”)
6.2 使用 JSONP,服務(wù)器后臺代碼需要改動嗎?
答案是需要,這里以Spring Boot為例,在 Spring Boot 1.5 大版本中,添加一個切面來支持JSONP請求。
AJAX代碼如下: $.ajax({ url: baseUrl + "/get1", dataType: "jsonp", // 關(guān)鍵字段 jsonp: "callback", // 前后端默認(rèn)的約定 cache: true, // 表示請求結(jié)果可以被緩存,url中不會有下劃線參數(shù)了 success: function(json) { result = json; } }); 服務(wù)端代碼: @ControllerAdvice public class JsonpAdvice extends AbstractJsonpResponseBodyAdvice { public JsonpAdvice() { super("callback"); } }
6.3 JSONP 實現(xiàn)原理
JSONP請求的類型是JAVAScript腳本(callback 作為前后端的約定,callback的值做為方法名,json內(nèi)容作為方法的參數(shù)),而XHR請求的類型是json類型。

▲ JSONP返回類型和XHR返回類型對比
可以在瀏覽器中查看 Jquery 源碼來驗證 JSONP 是否將請求包裝成了 script 腳本。

▲ JSONP動態(tài)生成script標(biāo)簽
在 Jquery 源碼中打斷點。

▲ 在jquery中打斷點
刷新后查看 element 元素,可以看到 Jquery 在 html 源碼中添加了 script 標(biāo)簽。

▲ jquery動態(tài)生成script腳本
6.4 JSONP的優(yōu)缺點
JSONP的優(yōu)點:部署時不需要應(yīng)用服務(wù)器去進(jìn)行額外的配置,跟普通的非跨域系統(tǒng)部署一模一樣,沒有特別的要求。
JSONP的缺點:
- 1)只支持 GET 方法請求,不管 AJAX 中實際的請求方法是不是 GET;2)服務(wù)端還需要修改代碼(如果你認(rèn)為修改服務(wù)端代碼比修改服務(wù)器的配置相比,很煩的話,這倒是可以算作是缺點);3)發(fā)送的不是 XHR 請求,無法使用 XHR 對象(但這也是為什么可以解決跨域問題的根本)。
7、跨域問題解決方法3:修改應(yīng)用服務(wù)器的跨域配置
根據(jù)現(xiàn)如今網(wǎng)站架構(gòu)設(shè)計,可以將前端應(yīng)用看作調(diào)用方使用服務(wù),將后端應(yīng)用看作被調(diào)用方提供服務(wù)。

根據(jù)服務(wù)器的作用,可以將服務(wù)器分為 HTTP 服務(wù)器和應(yīng)用服務(wù)器,所有修改服務(wù)器端既可以是修改應(yīng)用服務(wù)器,也可以是修改 HTTP 服務(wù)器。
7.1 被調(diào)用方修改
被調(diào)用方的解決思路是在響應(yīng)頭中增加指定的字段允許調(diào)用方服務(wù)器跨域調(diào)用。

在應(yīng)用服務(wù)器增加指定字段:
對于不帶 Cookie 的跨域請求,設(shè)置允許跨域的原始域名為任意域名,”Access-Control-Allow-Origin”: “*“,設(shè)置允許跨域的方法為任意方法,”Access-Control-Allow-Methods”: “*“,但是這樣的星號設(shè)置不能滿足帶 Cookie 的跨域請求。

對于帶 Cookie 的跨域請求,要指名允許跨域請求的調(diào)用方主機(jī)名,Cookie 要加在調(diào)用方。
帶自定義頭的跨域請求,設(shè)置允許跨域的請求頭自定義的請求頭,”Access-Control-Allow-Headers”:”自定義的請求頭”。
在 Java Web 中,可以添加一個過濾器來設(shè)置上面的參數(shù)。

▲ 被調(diào)用方使用Filter解決跨域
而使用 Spring Boot 框架,只需要在 Controller 類上加上 @CrossOrigin 注解就可以輕松解決跨域問題了。
在 HTTP 服務(wù)器增加指定字段:
以常用的 Nginx 服務(wù)器和 Apache 服務(wù)器為例。
Nginx 服務(wù)器允許跨域配置(注意不要手動直接點擊Nginx.exe,否則停止和重新載入配置會失敗的):

Apache 服務(wù)器允許跨域配置:

7.2 調(diào)用方修改
調(diào)用方的解決思路是反向代理,也即是將被調(diào)用方的域名代理到調(diào)用方域名下,這樣就符合同源策略了,也就解決了跨域問題。

▲ 調(diào)用方反向代理效果演示
調(diào)用方修改一般都是直接修改 HTTP 服務(wù)器配置。
Nginx 服務(wù)器反向代理配置:

Apache 服務(wù)器反向代理配置:

8、本文配套的代碼下載
請從鏈接:Web端即時通訊基礎(chǔ)知識補課:一文搞懂跨域的所有問題!-網(wǎng)頁端IM開發(fā)/專項技術(shù)區(qū) - 即時通訊開發(fā)者社區(qū)! 末尾處附件中下載之。
附錄1:Web端即時通訊技術(shù)入門文章提綱
Web即時通訊新手入門貼:
《新手入門貼:詳解Web端即時通訊技術(shù)的原理》
Web端即時通訊技術(shù)盤點請參見:
《Web端即時通訊技術(shù)盤點:短輪詢、Comet、Websocket、SSE》
關(guān)于Ajax短輪詢:
找這方面的資料沒什么意義,除非忽悠客戶,否則請考慮其它3種方案即可。
有關(guān)Comet技術(shù)的詳細(xì)介紹請參見:
《Comet技術(shù)詳解:基于HTTP長連接的Web端實時通信技術(shù)》
《WEB端即時通訊:HTTP長連接、長輪詢(long polling)詳解》
《WEB端即時通訊:不用WebSocket也一樣能搞定消息的即時性》
《開源Comet服務(wù)器iComet:支持百萬并發(fā)的Web端即時通訊方案》
更多WebSocket的詳細(xì)介紹請參見:
《新手快速入門:WebSocket簡明教程》
《WebSocket詳解(一):初步認(rèn)識WebSocket技術(shù)》
《WebSocket詳解(二):技術(shù)原理、代碼演示和應(yīng)用案例》
《WebSocket詳解(三):深入WebSocket通信協(xié)議細(xì)節(jié)》
《WebSocket詳解(四):刨根問底HTTP與WebSocket的關(guān)系(上篇)》
《WebSocket詳解(五):刨根問底HTTP與WebSocket的關(guān)系(下篇)》
《WebSocket詳解(六):刨根問底WebSocket與Socket的關(guān)系》
《理論聯(lián)系實際:從零理解WebSocket的通信原理、協(xié)議格式、安全性》
《Socket.IO介紹:支持WebSocket、用于WEB端的即時通訊的框架》
《socket.io和websocket 之間是什么關(guān)系?有什么區(qū)別?》
有關(guān)SSE的詳細(xì)介紹文章請參見:
《SSE技術(shù)詳解:一種全新的HTML5服務(wù)器推送事件技術(shù)》
更多WEB即時通訊文章請見:
http://www.52im.net/forum.php?mod=collection&action=view&ctid=15
附錄2:更多有關(guān)WEB端即時通訊開發(fā)的精華文章
《Web端即時通訊技術(shù)盤點:短輪詢、Comet、Websocket、SSE》
《SSE技術(shù)詳解:一種全新的HTML5服務(wù)器推送事件技術(shù)》
《socket.io實現(xiàn)消息推送的一點實踐及思路》
《LinkedIn的Web端即時通訊實踐:實現(xiàn)單機(jī)幾十萬條長連接》
《Web端即時通訊技術(shù)的發(fā)展與WebSocket、Socket.io的技術(shù)實踐》
《Web端即時通訊安全:跨站點WebSocket劫持漏洞詳解(含示例代碼)》
《開源框架Pomelo實踐:搭建Web端高性能分布式IM聊天服務(wù)器》
《使用WebSocket和SSE技術(shù)實現(xiàn)Web端消息推送》
《詳解Web端通信方式的演進(jìn):從Ajax、JSONP 到 SSE、Websocket》
《MobileIMSDK-Web的網(wǎng)絡(luò)層框架為何使用的是Socket.io而不是Netty?》
《理論聯(lián)系實際:從零理解WebSocket的通信原理、協(xié)議格式、安全性》
《微信小程序中如何使用WebSocket實現(xiàn)長連接(含完整源碼)》
《八問WebSocket協(xié)議:為你快速解答WebSocket熱門疑問》
《快速了解Electron:新一代基于Web的跨平臺桌面技術(shù)》
《一文讀懂前端技術(shù)演進(jìn):盤點Web前端20年的技術(shù)變遷史》
《Web端即時通訊知識補課:一文搞懂跨域的所有問題!》
>>更多同類文章 ……
(本文同步發(fā)布于:http://www.52im.net/thread-2732-1-1.html)