如何解決js跨域問題

Js跨域問題是web開發人員最常碰到的一個問題之一。所謂js跨域問題,是指在一個域下的頁面中通過js訪問另一個不同域下的數據對象,出於安全性考 慮,幾乎所有瀏覽器都不允許這種跨域訪問,這就導致在一些ajax應用中,使用跨域的web service會成為一個問題。 解決js跨域問題,目前在客戶端和服務端都有一些現成的解決方案,但這些方案並不能解決所有問題。下面我們先來看下有哪些常用的解決方案,並針對空間產品 對跨域問題的需求給出一個space自己的解決方案,希望能對其他產品組有借鑒意義。
客戶端解決方案
如何在客戶端解決js跨域問題幾乎是所有web開發人員會首先考慮的。目前最常用的方法有2種:設置document.domain、通過script 標簽加載。
設置document.domain
采用這種方法的前提是跨域請求涉及的兩個頁面必須屬於一個基礎域(例如都是xxx.com,或是xxx.com.cn),使用同一協議(例如都是 http)和同一端口(例如都是80)。例如,aaa.xxx.com裡面的一個頁面需要調用bbb.xxx.com裡的一個對象,則將兩個頁面的 document.domain都設置為xxx.com,就可以實現跨域調用瞭。 另外,需要註意的是,這種方式隻能用在父、子頁面之中,即隻有在用iframe進行數據訪問時才有用。
通過script標簽加載
對於瀏覽器來說,script標簽的src屬性所指向資源就跟img標簽的src屬性所指向的資源一樣,都是一個靜態資源,瀏覽器會在適當的時候自動去加 載這些資源,而不會出現所謂的跨域問題。這樣我們就可以通過該屬性將要訪問的數據對象引用進當前頁面而繞過js跨域問題。 例如,在space的我的空間項目中,需要在hi域下管理中心頁面中隨機推薦幾個熱門模塊給用戶,由於熱門模塊的相關信息都在act域下的php模塊中維 護,如果直接在hi域下通過ajax請求去獲取act域下的推薦模塊列表相關信息就出現js跨域問題。解決這個問題的最簡單方法就是,在hi域下通過 script標簽去訪問act域提供的這個http接口:
<script type=”text/javascript” src=”http://act.hi.baidu.com/widget/recommend”><script>
當然,前提是act域的這個http接口必須是返回一段js腳本,如一個json對象數組定義的腳本:
modlist = [
{“modname” : “mod1”,  “usernum” : 200, “url” : ” /widget/info/1”},
{“modname” : ”mod2”,  “usernum” : 300, ”url” : ” /widget/info/2”},

];
但script標簽也有一定的局限性,並不能解決所有js跨域問題。script標簽的src屬性值不能動態改變以滿足在不同條件下獲取不同數據的需求, 更重要的是,不能通過這種方式正確訪問以xml內容方式組織的數據。
服務端解決方案
從上面的說明可以看到,客戶端的解決方案局限性太大,而且對於ajax跨域請求,無論兩個域是否屬於同個基礎域,都無法在客戶端加以解決。也就是說,如果 我們要想在ajax請求中訪問其他域下的數據,就隻能通過服務端進行處理瞭。 服務端的解決方案的基本原理就是,由客戶端將請求發給本域服務器,再由本域服務器的代理來請求數據並將響應返回給客戶端。 最常用的服務器解決方案就是利用web服務器本身提供的proxy功能,如apache和lighttpd的mod_proxy模塊。在百度內 部,transmit的分流功能也可以解決部分跨域問題。但這些方法都有一定的局限性,鑒於安全性等問題的考慮,space這邊最後開發瞭一個專門用於處 理跨域請求代理服務的spproxy模塊,用於徹底解決js跨域問題。 下面我們將以空間的開放平臺為例,簡單介紹下如何通過apache的mod_proxy、transmit的分流以及space的spproxy模塊來解 決該跨域問題,並簡單介紹下spproxy的一些特性、缺點及下一步的改進計劃。 空間在展現每個UWA開放模塊之前都必須請求該模塊的xml源代碼以進行解析,每個模塊的源代碼文件都是存放在act域下的/ow/uwa目錄下,那麼在 用戶空間首頁(hi域)中請求該xml文件時就會存在js跨域問題。要解決該問題,隻能讓js向hi域的web服務器請求xml文件,而hi域web服務 器則通過一定的代理機制(如mod_proxy、transmit分流、spproxy)向act域的web服務器請求文件。
利用apache的mod_proxy模塊
如果apache是2.0系列版本,則可以通過在httpd.conf文件中增加以下配置加以解決:
ProxyRequests  Off
<Proxy *>
Order deny,allow
Allow from all
</Proxy>
ProxyPass  /ow/uwa  http://act.hi.baidu.com/ow/uwa
其中,ProxyRequests 指令關閉瞭mod_proxy的正向代理功能而啟用反向代理功能,Proxy指令使得該配置對所有訪問生效,ProxyPass指令使得對本域的/ow /uwa目錄下的任何資源的訪問都會在內部被轉換為一個對act.hi.baidu.com域下的/ow/uwa目錄下對應資源的代理請求。 這樣,js就可以直接通過訪問http://hi.baidu.com/ow/uwa/0/1/0/10001.xml 獲取位於act域下的/ow/uwa/0/1/0/目錄下的10001.xml文件。
如果apache是經過百度各產品線修改過的1.3版本,則需要mod_proxy和mod_rewrite模塊一起配合來達到同樣的目的。首先需要在 httpd.conf中增加以下Location指令:
<Location /ow/uwa>
SetHandler proxy-server
order allow,deny
Allow from all
</Location>
這樣,對於本域下的/ow/uwa目錄下的任何資源的訪問都會首先由proxy-server這個handler(mod_proxy模塊內部定義的一個 handler)來處理,但光有這段配置還不行,因為還不proxy-server還不知道應該怎麼處理,僅僅知道需要自己處理而已。這時還需要在配置段中增加一個rewrite規則:
RewriteRule ^/ow/uwa/(.*)$  http://act.hi.baidu.com/ow/uwa/$1?%{QUERY_STRING} [P,L]
Rewrite規則最後的[P,L]表明該rewrite是通過mod_proxy代理過去,而不是通過外部重定向過去。如果去掉P標志,即采用以下 rewrite規則:
RewriteRule ^/ow/uwa/(.*)$  http://act.hi.baidu.com/ow/uwa/$1?%{QUERY_STRING} [L]
則響應返回給客戶端時標明的資源uri將是重定向後的uri,在我們的例子中就是act.hi.baidu.com域的uri,則瀏覽器仍然會出現 js跨 域問題。 以上隻是對apache的proxy功能的簡單應用,更好更強大的介紹可以參考資料【1】和【2】。 Mod_proxy雖然強大,但我們並沒有用它來解決跨域問題。首先,要使用它必須要求我們的每臺前端機器都能夠訪問外網,否則我們就隻能將請求代理到其 中一臺前端機器上(通過機器名做內網域名進行rewrite或代理),而這顯然是不可取的,因為我們的一個域名通常由很多前端機器組成,隻代理到其中一臺 機器會導致該機器壓力與其他機器相比很不均衡,甚至撐不住壓力,而給所有前端機器都加訪問外網權限又可能會存在一些安全性策略問題(具體原因不清楚,但 op和sa顯然是不會贊同這種做法)。其次,由於apache本身並沒有很好的防ddos攻擊機制,一旦有人通過代理去攻擊目標域(比如說我們的競爭對手 的網站),則在目標域的web服務器上看來,攻擊者就成瞭我們瞭,這樣的事情發生時,我們就百口莫辯,跳進黃河也洗不清瞭。
利用transmit分流方案
用過transmit的產品線應該都知道,transmit除瞭用於防攻擊之外,還有一個很重要的功能就是分流。有瞭分流功能,我們就可以將對特定 url 的訪問分發給不同的apache處理,從而實現跨域訪問的目的。 還是以空間開放平臺的這個例子為例,假設我們的act域在jx機房內由jx-space-act00.jx和jx-space-act01.jx這兩臺機 器組成,apache的端口為8080,則隻要我們在transmit的配置文件transmit_common.conf中增加以下配置:
PP_APACHE_DIR    :   /ow/uwa/
PP_APACHE0      :   jx-space-act00.jx:8080
PP_APACHE1      :   jx-space-act01.jx:8080
則重啟transmit後,南方用戶就可以通過訪問http://hi.baidu.com/ow/uwa/0/1/0/10001.xml 而獲取http://act.hi.baidu.com/ow/uwa/0/1/0/10001.xml這個url所執行的xml內容,從而解決跨域問 題。如果我們在hi域下的js同時還想異步獲取act域下的其他數據,比如說/sys/widget/xxx接口提供的數據,則隻需要在 PP_APACHE_DIR配置項中增加一個目錄定義:
PP_APACHE_DIR    :   /ow/uwa/, /sys/widget/
由於舊版本的transmit隻支持一個分流,所以不能通過它來同時解決對多個外域的跨域請求問題,同時,要支持舊版本transmit,後端的 apache需要做相應的代碼修改和配置才行,這也限制瞭我們的分流功能不能解決跨非百度域的跨域問題。不過好消息是,gm最近發佈的新版本 transmit允許增加n個分流,同時支持後端apache不做任何修改,那麼對於舊版本transmit所碰到的限制也就不再存在瞭,通過它就可以在 一定程度上很好地解決跨域問題瞭。具體配置方法與舊版本類似,大傢可以參考新版本transmit的配置文件做相應修改來實現這個目的。
利用spproxy模塊
但是,在space的開放平臺系統中,我們並不是通過transmit來解決跨域問題,前面也提到瞭,transmit隻能在一定程度上解決這個問題。為 什麼這麼說呢?由於transmit增加分流是需要在修改配置後重啟transmit程序的,而且隨著分流分支的增加,其性能會不斷降低,畢竟每次請求到 來時它都需要遍歷所有分流分支以判斷應該走哪條分支,而對於開放平臺來說,任何一個新的開放模塊都有可能會引入一個甚至多個新的外域,這會導致 transmit的分流分支數隨著開放模塊數量的增加而線性增加,這無論在op運維上還是程序性能上都將是不可接受的。 基於這樣的考慮,space在開放平臺二期項目中引入瞭一個新的模塊——spproxy模塊,用於提供跨域請求代理服務,從而徹底解決瞭js跨域問題。 從某種意義上講,spproxy其實就是一個ui,它接收來自apache的請求,並處理該請求獲取真正的頁面數據,然後返回給apache,再由 apache返回給客戶端。Spproxy隻接收一個apache命令號(AC_SYS_PROXY : 38),並提供瞭兩個http接口:
/sys/pxy/ajax?url=xxxx和/sys/pxy/xml?url=xxx
其中,/sys/pxy/是可以通過apache配置文件來修改成其他目錄名的,url參數就是js希望跨域請求的數據的uri(需要進行url編碼,如 果url中有參數),xml接口與ajax接口的唯一區別是,spproxy會強制將前者返回的內容的Content-Type設為text/xml,而 對於後者,則是外域服務器返回的是何種Content-Type就是何種type。 Apache端隻需要增加以下兩個配置就可以讓spproxy來處理以上兩個http接口的請求,當然,前提是所用的apache是經過ns改寫過的 apache,目前主要是1.3版本的apache:
CmdNoMap   pxy      38
CmdHost      pxy     10.23.64.185 20540
其中,pxy就是http接口中的第二個目錄名,可以自定義,例如配置裡如果寫的是proxy,則http接口就是/sys/proxy /ajax?url=xxx和/sys/proxy/xml?url=xxx;38是spproxy能夠處理的命令號,可以在編譯時修改成其他 值;10.23.64.185 20540是spproxy所在機器的ip和spproxy的偵聽端口。 通過以上配置後,hi域下的js就可以通過異步訪問http://hi.baidu.com/sys/pxy/xml?url=http: //act.hi.baidu.com/ow/uwa/0/1/0/10001.xml來跨域訪問http://act.hi.baidu.com/ow /uwa/0/1/0/10001.xml瞭。如果跨域訪問的資源uri帶參數,如http://act.hi.baidu.com/widget /recommend?num=6,則在訪問時需要將參數值進行url編碼,如http://hi.baidu.com/sys/pxy /xml?url=http%3A%2F %2Fact%2Ehi%2Ebaidu%2Ecom%2Fwidget%2Frecommend%3Fnum%3D6。
Spproxy介紹
Spproxy是一個基於epoll網絡模型開發的單進程模塊,包含一個數據抓取線程和定時加載線程:  抓取線程 ,對跨域請求進行代理,抓取指定url對應的頁面內容並返回給前端,此線程采用epoll模型提高請求處理的並發度  定時加載線程,定時加載域名白名單以及部分可重加載的配置項(如各種超時時間、是否強制指定cache過期時間等) spproxy通過一個域名白名單限制js能夠跨域訪問的域名以降低安全風險,需要增加一個js能夠跨域訪問的外域時隻需要在spproxy的域名白名單 文件spproxy_domainlist.txt中增加一行即可,5分鐘後(具體生效時間可配置)即會生效。 由於采用的是epoll網絡模型,spproxy本身能夠很好地抵禦慢連接攻擊,同時,它還具有與space ui同樣強大的防攻擊功能。 為瞭減少對外域服務器的請求以提高跨域請求的響應速度,同時又降低外域服務器封殺我們的代理服務的風險,spproxy本身做瞭一個相對簡單的cache 功能。如果外域服務器返回的頁面http頭中指定瞭cache過期時間,spproxy就會根據該http頭對該頁面的cache過期時間算一個比較合理 的過期值並對頁面進行cache;如果外域服務器返回的http頭中沒有指定cache過期時間或要求不進行cache,則spproxy還是會對該頁面 進行短期的cache,過期時間可配置。 另外,對於spproxy模塊中涉及的大多數超時時間配置及域名白名單都是可以定時重加載的,從而實現線上服務調整參數、增加信任域時無需重啟服務作廢 cache的目的。 不過,spproxy目前也還存在一些缺點:  返回給spproxy的響應體不能是經過壓縮編碼的,spproxy在向外域請求時會在http頭中標明這一點,這會增加讀響應時間和外域網站的帶寬消耗  Spproxy目前隻是根據外域服務器的http響應頭中的Cache-Control字段中的max-age屬性計算頁面的cache過期時間,而實際 上很多網站返回的cache-control字段並不是通過max-age來標示cache過期時間的  Spproxy目前隻支持GET方法,不支持其他http方法,而且,spproxy不支持任意大小的外域頁面,但可以通過配置改變它所能接收的頁面數據 量的最大值 下一步,spproxy將會在解析http響應頭中的cache-control字段方面做些改進以便更加合理地控制spproxy對返回頁面的 cache,另外,下一步還將支持通過POST方法進行跨域請求,以提高跨域請求的安全性。

作者“ERDP技術架構”

發佈留言