web 跨域請求共享資源(OCRS)
跨域引用資源技術及其技術選型
一、源
同源策略是瀏覽器都必須遵循的策略,這就限制了js去呼叫和修改非同域下的資料。試想如果沒有這個策略,在另外一個域的js就能輕易修改當前你正在呼叫的頁面。那就天下大亂,毫無安全可言了。遵循同源策略就對非同域的資源呼叫加上了許多限制。 我們知道css、image、及js本身(指檔案本身)是可以跨域引用的。但html檔案和js具體的資料呼叫如ajax是不能跨域了。還有,所謂同源是指兩個頁面的協議、域名、埠完全相同,否則都算跨域。通常我們會有很多二級域名,這時需要設定document.domain與主域名一致,否則也認為是跨域。
二、典型的資源共享案例
1)、html
2)、公開的資料介面想被各系統呼叫。(不同域名)
3)、請求第三方url結果。(不同域名)
這裡先列出這三種需求,因為不同的需求,技術選型也不一樣。
三、Cross-Origin Resource Sharing
這是W3C推出的跨域資源標準規範,其核心部分是通過HTTP請求頭來控制跨域的安全性,主要有兩個頭描述,一是Origin:描述當前的資源是可用來跨域請求的;另外一個是Access-Control-Allow-Orgin:用來描述哪些域是可以訪問該資源,允許設定多個。
由於IE8之前的版本及其他瀏覽器稍低些的版本不遵循此規範,從當前來看其應用還不廣泛,因此暫不考慮用此方案來解決上述三個案例。如要對Cross-Origin Resource Sharing進官方進一步瞭解:http://www.w3.org/TR/cors/。
四、跨域請求資源方案
1、配置反向代理
比如你的資源在ip/resource/resource.html下,中介軟體只配置resource目錄給
resource.caidao8.com。 現在有www.caidao8.com、practise.caidao8.com都想跨域呼叫resource.html。可以通過中介軟體,把ip/resource也目前也配置到域resource.caidao8.com、www.caidao8.com下。這樣在resource.caidao8.com要呼叫resource.html時,就直接變成地址http://resource.caidao8.com/resource/resource.html,www.caidao8.com呼叫時地址變成http://www.caidao8.com/resource/resource.html。實際上如果你有其他不同的域,比如www.caidao8.cn,也可以把資源配置一份到www.caidao8.cn。就看你擁有幾個域並配置它們。
結論:通過中介軟體配置讓共用資源變成多種域名下可訪問是解決跨域的一種有效方法。但方案受限於共享的資源完全是自己能控制的。還有像tomcat、jetty本身一般不做反向代理,如果要在開發環境檢視效果和除錯,就得給開發環境也安裝nginx/apache,
增加了開發環境的複雜性,考慮到整個團隊實施問題,在實際中我們團隊沒有采用通過nginx/apache配置這種方式。
2、IFrame解決跨域問題及其不足之處
IFrame解決跨域問題,只限於a、b是主次域名,首先設定document.domain使主次統一的主域名就能統一域名。我們設定a是parent,b 放在a下的iframe源。首先在b頁面加上一段
<script>$(document).attr(“domain”,”caidao8.com”);</script>(本文所有js程式碼以jquery表示)。在a下編寫js程式碼:
$jq(document).attr("domain","caidao8.com");
//jquery找不到object,會自動建立此物件
$jq("<iframe id='dddiframe'></iframe>").attr("src","http://testresource.caidao8.com/billtemplate/aaa.html").appendTo("body");
//後續事件必須等iframe資源載入完成後才能操作,而不同的瀏覽器對iframe資源是否載入完成事件不一致
//iframe下使用jquery的bind與javascript原意attachEvent,兩者執行順序不同,使用jquery bind繫結未果,採用js Dom物件來操作
var iframe = $jq("#dddiframe")[0];
if (iframe.attachEvent){ //解決ie不認iframe onload問題
iframe.attachEvent("onload", function(){
alert($jq("#dddiframe").contents().find("#billid").html());
});
} else {
iframe.onload = function(){
//FF及Chrome能正常獲取
alert($jq("#dddiframe").contents().find("body").html());
};
}
注意彎路:因為所請求b的資源是靜態資源,一般情況下服務端和瀏覽器客戶端會有快取,特別是Chrome瀏覽器,我在開發的過程中,為請求的b資源加了一段
<script>$(document).attr(“domain”,”caidao8.com”);</script>,FF和IE在強刷請求入口後(沒有直接強刷b的url,而是強刷a的入口),都產生作用,在Chrome下,強刷a入口,iframe裡請求的b資源卻還是一直是本地的快取。這個問題困擾了整整一天,搜了很多資料,還以為是Chrome的一個bug(在Chrome官方也有人提出問題,但沒有確認是bug),因為一開始我使用的是12.0.742.91 m版本,使用偵錯程式並沒有提示任何出錯資訊,而是直接在偵錯程式裡看到iframe下的contentDocument物件為 undefined;升級Chrome到版本17.0.963.79 m後還是有進步,再進入偵錯程式,這回偵錯程式給出了報錯資訊,提示協議、域及埠要匹配,這才注意到,原來b資源一直還是本地快取的資源,直接強刷b資源後,終於把Chrome也搞定了。
結論:如果用iframe來實現跨域訪問,資料獲取被推到了前端JS,而繞過了伺服器,節省服務端資源開銷。而且資料直接在客戶端互動,速度也最快。在我開發的過程中,請求a的入口http://www.caidao8.com:8080/test而上面的b資源地址是
http://testresource.caidao8.com/billtemplate/aaa.html,這兩者的埠並不一致,但瀏覽器似乎有放低同源策略port這個約束標準,這樣的埠差異是能被放行的。
Iframe跨域的應用場景受限於主次域名,網上有一種說法,就是a、b兩個非主次域名的也通過iframe來實現跨域訪問,實際上並不好實現。另外要對所有要引用資源跨域HTML資源加上document.domain=”yourdomain.com”,這無疑也是件麻煩的事情。
3、JSONP
JSONP是本質上是一種Javascript注入,通過ajax的方式請求並回調注入服務端資料,它要求服務端提供標準的json格式的資料。
這是freemarker定義的一個標準jsonp返回資料模板樣例:
${callback!}({"options":[
<#if datadicts?? && datadicts?size > 0>
<#list datadicts as datadict>
{
"value":"${datadict.key}","text":"${datadict.value}"
}
<#if datadict_has_next>,</#if>
</#list>
</#if>
]});
Callback引數就是ajax端請求的回撥函式,datadicts是服務端定義的資料,這裡略去了JAVA部分的程式碼,你可以寫個Servlet或是Action,把callback及要傳出資料按json格式輸出。這裡用freemarker取例是因為其模板內容看起來更加直觀,你也可以直接用servlet response輸出你的內容。
再貼PHP寫的JSONP服務端程式碼樣例:
<?php
// create a new curl resource
function geturlcontent(){
$ch = curl_init();
// set URL and other appropriate options
curl_setopt($ch, CURLOPT_URL, "http://testresource.caidao8.com/billtemplate/ec51de3cd33749aa8401c9b6854ec2d6.html");
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);//不直接輸出,返回到變數
$content = curl_exec($ch);
curl_close($ch);
//mb_convert_encoding($content,'utf-8');
return json_encode($content);
}
$fun = $_GET["callback"];
$output = $fun."({\"htmlstr\":".geturlcontent()."})";
echo $output;
?>
Jquery呼叫jsonp樣例:
$jq.getJSON("http://localhost/dong/jsonp/getbilltemplate.php?callback=?",function(data){
$jq("#saveid").before(data.htmlstr);
//因為getJSON預設是非同步的,因此必須把後續業務程式碼跑在這裡。
});
結論:為了做比較,本人在開發的過程中特意把iframe實現的跨域請求html功能,用jsonp來實現,之所以使用PHP,是因為PHP在Nginx/apache下能直接解析,因為HTML資源本身與業務無關,這樣可以省去java中介軟體及jvm虛擬機器層。由於HTML本身內容不符合json格式,因此在輸出時,需要json_encode()方法,經測試發現這樣的效率遠低於iframe跨域會規,也比不上下面要講的請求代理方式。因此在請求HTML資源時使用jsonp方式是不合適的。那如果資料本身不是html資源,而是普通的資料,jsonp用來跨域傳輸公開資料倒是一種既簡單又實用方案。jsonp可以在任何不同的域之間呼叫,前提是你要相信服務端的資料是可信賴和非惡意的,對於服務端來說其資料介面必須是公開的。
4、Open API
目前大型的網站基本上開放或部分開放其API,甚至有人直接把開放API說成雲概念,從服務端的角度來理解,開放的API可以理解成是JSONP的進一步擴充套件,加入了認證校驗安全方面的業務。當然服務端可能會進一步收集客戶端的流量等資料,對客戶端的資源訪問做進一步控制。通常API的資料格式會支援JSON、Rest、標準XML格式,相對於JSONP的簡單處理,開放API在服務端的設計上要複雜一些,客戶端呼叫一般也需要申請並通過認證後才能呼叫介面,當然可能有一部分介面是完全公開的。從客戶端的角度來說,開放api與jsonp的處理就完全不一樣了,開放api需要客戶端自己寫解析器去解析,因為json、rest風格基本上是一致的,因此也可以到網上直接獲取高效的解析器。這裡不進一步講述api服務端及客戶端詳細實現。
結論:如果服務端想開放資料及資料介面,又有較複雜的許可權及資源等業務方面的控制,那無疑要選擇Open API。但個人認為在公司內部不同的專案之間,採用開放API的方式也更加有利於程式碼的集中管理及應用優化。
5、請求代理
這裡的請求代理很簡單,實際上就是模擬Http請求,然後獲取到請求結果後,再轉成服務輸出。一段JAVA程式碼樣例:
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.methods.GetMethod;
…//這裡省去一些程式碼
public void getbilltemplate(){
HttpClient httpClient = new HttpClient();
//建立GET方法的例項
GetMethod getMethod = new GetMethod(Configuration.get("webapp.resource.mediaRoot","http://testresource.caidao8.com")+"/billtemplate/ec51de3cd33749aa8401c9b6854ec2d6.html");
//使用系統提供的預設的恢復策略
getMethod.getParams().setParameter(HttpMethodParams.RETRY_HANDLER,
// new DefaultHttpMethodRetryHandler());
try {
// getRequest().setCharacterEncoding("UTF-8");
//執行getMethod
int statusCode = httpClient.executeMethod(getMethod);
if (statusCode != HttpStatus.SC_OK) {
System.err.println("Method failed: "
+ getMethod.getStatusLine());
}
//讀取內容
byte[] responseBody = getMethod.getResponseBody();
writeAjax(new String(responseBody,"UTF-8"));
} catch (HttpException e) {
//發生致命的異常,可能是協議不對或者返回的內容有問題
System.out.println("Please check your provided http address!");
e.printStackTrace();
} catch (IOException e) {
//發生網路異常
e.printStackTrace();
} finally {
//釋放連線
getMethod.releaseConnection();
}
}
這段程式碼的作用,就是先請求url:http://testresource.caidao8.com/billtemplate/ec51de3cd33749aa8401c9b6854ec2d6.html,然後再把獲取到的內容輸出。你可以把這裡的url換成任何你想要的url。這樣通過這個代理,你就能獲取到你想要的其他站點的資源內容。當然這樣獲取的資源內容大都就是一堆html內容輸出,如果你要獲取裡面的資料,需要進一步解析內容。下面是我用ajax請求這個代理輸出
$jq.ajax({
url:"/billstudy/getbilltemplate.hx",
datatype:"html",
cache:false,
async:false,
success:function(data){
$jq("#saveid").before(data);
}
});
由於直接使用datatype:”html”,其效率還是可以接受的。
結論:請求代理可以獲取站外任何公開資源,如果進一步抽象和優化,這不就是爬蟲的原型麼!
五、選型
回到第二大點的三種典型案例,我們可以給出選型答案:
典型的資源共享案例 |
選型參考 |
html做為模板,想被各系統共用。(主次域名) |
Iframe跨域方案 如果不考慮開發階段的複雜性:選擇配置反向代理方案更加直接。 |
公開的資料介面想被各系統呼叫。(不同域名) |
如果是一般資料:jsonp 複雜業務控制的資料:open API 如果是html資料:請求代理 |
請求第三方url結果。(不同域名) |
請求代理 |
把五種解決方案作列表對比:
解決方案 |
場景描述 |
配置反向代理 |
共享資源屬於自己內部,可以把資源隨意配置到你擁有域名下。限於檔案資源。 |
Iframe跨域 |
限於主次域名的檔案資源。 |
jsonp |
完全跨域並完全公開的json格式資料傳輸。 |
Open API |
包含較複雜業務控制及約定的資料傳輸。 |
請求代理 |
完全跨域的url內容抓取代理。 |