Js跨域問題是web開發人員最常碰到的一個問題之一。所謂js跨域問題,是指在一個域下的頁面中經過js訪問另外一個不一樣域下 的數據對象,出於安全性考 慮,幾乎全部瀏覽器都不容許這種跨域訪問,這就致使在一些ajax應用中,使用跨域的web service會成爲一個問題。 解決js跨域問題,目前在客戶端和服務端都有一些現成的解決方案,但這些方案並不能解決全部問題。下面咱們先來看下有哪些經常使用的解決方案,並針對空間產品 對跨域問題的需求給出一個space本身的解決方案,但願能對其餘產品組有借鑑意義。javascript
如何在客戶端解決js跨域問題幾乎是全部web開發人員會首先考慮的。目前最經常使用的方法有2種:設置document.domain、經過script 標籤加載。php
採用這種方法的前提是跨域請求涉及的兩個頁面必須屬於一個基礎域(例如都是xxx.com,或是xxx.com.cn),使用同一協議(例如都是 http)和同一端口(例如都是80)。例如,aaa.xxx.com裏面的一個頁面須要調用bbb.xxx.com裏的一個對象,則將兩個頁面的 document.domain都設置爲xxx.com,就能夠實現跨域調用了。 另外,須要注意的是,這種方式只能用在父、子頁面之中,即只有在用iframe進行數據訪問時纔有用。前端
對於瀏覽器來講,script標籤的src屬性所指向資源就跟img標籤的src屬性所指向的資源同樣,都是一個靜態資源,瀏覽器會在適當的時候自 動去加 載這些資源,而不會出現所謂的跨域問題。這樣咱們就能夠經過該屬性將要訪問的數據對象引用進當前頁面而繞過js跨域問題。 例如,在space的個人空間項目中,須要在hi域下管理中心頁面中隨機推薦幾個熱門模塊給用戶,因爲熱門模塊的相關信息都在act域下的php模塊中維 護,若是直接在hi域下經過ajax請求去獲取act域下的推薦模塊列表相關信息就出現js跨域問題。解決這個問題的最簡單方法就是,在hi域下經過 script標籤去訪問act域提供的這個http接口:java
<script type=」text/javascript」 src=」http://act.hi.baidu.com/widget/recommend」><script>
固然,前提是act域的這個http接口必須是返回一段js腳本,如一個json對象數組定義的腳本:web
modlist = [ {「modname」 : 「mod1」, 「usernum」 : 200, 「url」 : 」 /widget/info/1」}, {「modname」 : 」mod2」, 「usernum」 : 300, 」url」 : 」 /widget/info/2」}, … ];
但script標籤也有必定的侷限性,並不能解決全部js跨域問題。script標籤的src屬性值不能動態改變以知足在不一樣條件下獲取不一樣數據的需求, 更重要的是,不能經過這種方式正確訪問以xml內容方式組織的數據。ajax
從上面的說明能夠看到,客戶端的解決方案侷限性太大,並且對於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
若是apache是2.0系列版本,則能夠經過在httpd.conf文件中增長如下配置加以解決:json
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除了用於防攻擊以外,還有一個很重要的功能就是分流。有了分流功能,咱們就能夠將對特定 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的配置文件作相應修改來實現這個目的。
可是,在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是一個基於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方法進行跨域請求,以提升跨域請求的安全性。