1. 程式人生 > >如何解決js跨域問題

如何解決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方法進行跨域請求,以提高跨域請求的安全性。