同源策略
理解跨域首先必須要了解同源策略。同源策略是瀏覽器上為安全性考慮實(shí)施的非常重要的安全策略。何謂同源:URL由協(xié)議、域名、端口和路徑組成,如果兩個URL的協(xié)議、域名和端口相同,則表示他們同源。
跨域就是通過某些手段來繞過同源策略限制,實(shí)現(xiàn)不同服務(wù)器之間通信的效果。具體策略限制情況可看下表:
| URL | 說明 |
|---|---|
| http://www.a.com/a.js http://www.a.com/b.js | 同一域名下 |
| http://www.a.com/lab/a.js http://www.a.com/script/b.js | 同一域名不同文件夾下 |
| http://www.a.com:8000/a.js http://www.a.com/b.js | 同一域名不同端口 |
| http://www.a.com/a.js https://www.a.com/b.js | 同一域名不同協(xié)議 |
| http://www.a.com/a.js http://127.0.0.100/b.js | 域名和域名對應(yīng)ip |
| http://www.a.com/a.js http://script.a.com/b.js | 主域相同,子域不同 |
| http://www.a.com/a.js http://a.com/b.js | 同一域名,不同二級域名 |
| http://www.a.com/a.js http://www.b.com/b.js | 不同域名 |
只有文件協(xié)議、域名、端口和路徑全部相同,這些文件才會是同源,同源文件間請求無須特殊處理,當(dāng)不同源文件之間發(fā)生請求時,則需要跨域處理。
jsonp實(shí)現(xiàn)跨域原理
什么是jsonp?
參考百度百科,JSONP(JSON with Padding)是JSON的一種“使用模式”,可用于解決主流瀏覽器的跨域數(shù)據(jù)訪問的問題。由于同源策略,一般來說位于 server1.example.com 的網(wǎng)頁無法與不是 server1.example.com的服務(wù)器溝通,而 HTML 的<script> 元素是一個例外。利用 <script> 元素的這個開放策略,網(wǎng)頁可以得到從其他來源動態(tài)產(chǎn)生的 json資料,而這種使用模式就是所謂的 jsonp。用 jsonp抓到的資料并不是 json,而是任意的JavaScript,用 JavaScript 直譯器執(zhí)行而不是用 json解析器解析。
jsonp原理:
首先在客戶端注冊一個callback, 然后把callback的名字傳給服務(wù)器。此時,服務(wù)器先生成 json 數(shù)據(jù)。然后以 javascript 語法的方式,生成一個function , function 名字就是傳遞上來的參數(shù) jsonp 。最后將 json 數(shù)據(jù)直接以入?yún)⒌姆绞?,放置?function 中,這樣就生成了一段 js 語法的文檔,返回給客戶端??蛻舳藶g覽器,解析script標(biāo)簽,并執(zhí)行返回的 javascript 文檔,此時數(shù)據(jù)作為參數(shù),傳入到了客戶端預(yù)先定義好的 callback 函數(shù)里.(動態(tài)執(zhí)行回調(diào)函數(shù))
jsonp作用:
由于同源策略的限制,XmlHttpRequest只允許請求當(dāng)前源(域名、協(xié)議、端口)的資源,為了實(shí)現(xiàn)跨域請求,可以通過script標(biāo)簽實(shí)現(xiàn)跨域請求,然后在服務(wù)端輸出json數(shù)據(jù)并執(zhí)行回調(diào)函數(shù),從而解決了跨域的數(shù)據(jù)請求。
jsonp缺點(diǎn):
- 它只支持GET請求而不支持POST等其它類型的HTTP請求(雖然采用post+動態(tài)生成iframe是可以達(dá)到post跨域的目的,但這樣做是一個比較極端的方式,不建議采用)。
- jsonp易于實(shí)現(xiàn),但是也會存在一些安全隱患,如果第三方的腳本隨意地執(zhí)行,那么它就可以篡改頁面內(nèi)容,截獲敏感數(shù)據(jù)。但是在受信任的雙方傳遞數(shù)據(jù),jsonp是非常合適的選擇??梢钥闯鰜韏sonp跨域一般用于獲取其他域的數(shù)據(jù)。
一言不合上代碼
獲取json數(shù)據(jù)
使用jsonp獲取json數(shù)據(jù),類似同源post請求獲取json數(shù)據(jù),不過jsonp只支持get請求。
1. 客戶端注冊callback,并將callback名字傳給服務(wù)器
$.ajax({ url: "http://localhost:8080" + "/my/order/cancel?orderNo=" + orderNo,
type: "get",
dataType: "jsonp",
jsonp: 'callback',
jsonpCallback: 'jsonp_callback',
success: function (data, status) {
//回調(diào)處理
alert(data);
}
});
上述代碼,callback是回傳至服務(wù)器參數(shù),服務(wù)器使用callback參數(shù)拼接服務(wù)器端請求結(jié)果(json數(shù)據(jù)),返回給客戶端。
*2. 服務(wù)器端處理請求 *
@RequestMapping("/cancel")
@ResponseBodypublic JSONPObject cancelOrder(String orderNo, HttpSession session, HttpServletRequest request, HttpServletResponse response, String callback) {
// 獲取用戶信息
Member member = (Member) session.getAttribute(Constants.SESSION_MEMBER_NAME);
String operateIp = ClientIpUtil.getClientIp(request);
UserOrderOpCtx operateCtx = new UserOrderOpCtx(orderNo,String.valueOf(member.getId()),member.getNickName(),operateIp);
userOrderApiService.cancelOrder(operateCtx);
response.setContentType("text/plain");
return new JSONPObject(callback, MapUtils.getMap(RespEnum.OK, "訂單取消成功!"));
}
根據(jù)請求客戶端請求參數(shù)callback,組裝jsonp數(shù)據(jù),返回給客戶端;
獲取html數(shù)據(jù)
有些業(yè)務(wù)場景需要跨域獲取其他系統(tǒng)頁面數(shù)據(jù),類似同源間get請求;
1. 客戶端注冊callback,并將callback名字傳給服務(wù)器
$.ajax({ url: "http://localhost:8080" + "/my/order/cancel?orderNo=" + orderNo,
type: "get",
dataType: "jsonp",
jsonp: 'callback',
jsonpCallback: 'jsonp_callback',
success: function (data, status) {
//回調(diào)處理
alert(data);
}
});
上述代碼,callback是回傳至服務(wù)器參數(shù),服務(wù)器使用callback參數(shù)拼接服務(wù)器端請求結(jié)果(json數(shù)據(jù)),返回給客戶端。
*2. 服務(wù)器端處理請求 *
@RequestMapping("/cancel")
public void cancelOrder(String orderNo, HttpSession session,HttpServletResponse response, String callback) {
// 獲取用戶信息
Member member = (Member) session.getAttribute(Constants.SESSION_MEMBER_NAME);
String operateIp = ClientIpUtil.getClientIp(request);
UserOrderOpCtx operateCtx = new UserOrderOpCtx(orderNo,String.valueOf(member.getId()),member.getNickName(),operateIp);
userOrderApiService.cancelOrder(operateCtx);
response.setContentType("text/plain");
response.getWriter().write(callback+ "JSON.toJSON(MapUtils.getMap(RespEnum.OK, "訂單取消成功!"))"); //返回jsonp數(shù)據(jù)
}
HTTP訪問控制
跨源資源共享(CROS)讓W(xué)eb應(yīng)用服務(wù)器能支持跨站訪問控制,從而使得安全地進(jìn)行跨站數(shù)據(jù)傳輸成為可能。需要特別注意的是,這個規(guī)范是針對API容器的。比如說,要使得XMLHttpRequest在現(xiàn)代瀏覽器中可以發(fā)起跨域請求。瀏覽器必須能支持跨源共享帶來的新的組件,包括請求頭和策略執(zhí)行。同樣,服務(wù)器端則需要解析這些新的請求頭,并按照策略返回相應(yīng)的響應(yīng)頭以及所請求的資源。
HTTP響應(yīng)頭
這部分里列出了跨域資源共享(Cross-Origin Resource Sharing)時,服務(wù)器端需要返回的響應(yīng)頭信息.上一部分內(nèi)容是這部分內(nèi)容在實(shí)際運(yùn)用中的一個概述.
Access-Control-Allow-Origin
返回的資源需要有一個 Access-Control-Allow-Origin 頭信息,語法如下:
Access-Control-Allow-Origin: <origin> | *
origin參數(shù)指定一個允許向該服務(wù)器提交請求的URI.對于一個不帶有credentials的請求,可以指定為'',表示允許來自所有域的請求.
舉個栗子,允許來自 http://mozilla.com 的請求,你可以這樣指定:
Access-Control-Allow-Origin: http://mozilla.com
如果服務(wù)器端指定了域名,而不是'',那么響應(yīng)頭的Vary值里必須包含Origin.它告訴客戶端: 響應(yīng)是根據(jù)請求頭里的Origin的值來返回不同的內(nèi)容的.
Access-Control-Expose-Headers
Requires Gecko 2.0(Firefox 4 / Thunderbird 3.3 / SeaMonkey 2.1)
設(shè)置瀏覽器允許訪問的服務(wù)器的頭信息的白名單:
Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-Header
這樣, X-My-Custom-Header
和 X-Another-Custom-Header這兩個頭信息,都可以被瀏覽器得到.
Access-Control-Max-Age
這個頭告訴我們這次預(yù)請求的結(jié)果的有效期是多久,如下:
Access-Control-Max-Age: <delta-seconds>
delta-seconds
參數(shù)表示,允許這個預(yù)請求的參數(shù)緩存的秒數(shù),在此期間,不用發(fā)出另一條預(yù)檢請求.
Access-Control-Allow-Credentials
告知客戶端,當(dāng)請求的credientials屬性是true的時候,響應(yīng)是否可以被得到.當(dāng)它作為預(yù)請求的響應(yīng)的一部分時,它用來告知實(shí)際的請求是否使用了credentials.注意,簡單的GET請求不會預(yù)檢,所以如果一個請求是為了得到一個帶有credentials的資源,而響應(yīng)里又沒有Access-Control-Allow-Credentials頭信息,那么說明這個響應(yīng)被忽略了.
Access-Control-Allow-Credentials: true | false
帶有credential的請求在上面討論.
Access-Control-Allow-Methods
指明資源可以被請求的方式有哪些(一個或者多個). 這個響應(yīng)頭信息在客戶端發(fā)出預(yù)檢請求的時候會被返回. 上面有相關(guān)的例子.
Access-Control-Allow-Methods: <method>[, <method>]*
發(fā)出預(yù)檢請求的例子見上,這個例子里就有向客戶端發(fā)送Access-Control-Allow-Methods響應(yīng)頭信息.
Access-Control-Allow-Headers
也是在響應(yīng)預(yù)檢請求的時候使用.用來指明在實(shí)際的請求中,可以使用哪些自定義HTTP請求頭.比如
Access-Control-Allow-Headers: X-PINGOTHER
這樣在實(shí)際的請求里,請求頭信息里就可以有這么一條:
X-PINGOTHER: pingpong
可以有多個自定義HTTP請求頭,用逗號分隔.
Access-Control-Allow-Headers: <field-name>[, <field-name>]*
HTTP 請求頭
這部分內(nèi)容列出來當(dāng)瀏覽器發(fā)出跨域請求時可能用到的HTTP請求頭.注意這些請求頭信息都是在請求服務(wù)器的時候已經(jīng)為你設(shè)置好的,當(dāng)開發(fā)者使用跨域的XMLHttpRequest的時候,不需要手動的設(shè)置這些頭信息.
Origin
表明發(fā)送請求或者預(yù)請求的域
Origin: <origin>
參數(shù)origin是一個URI,告訴服務(wù)器端,請求來自哪里.它不包含任何路徑信息,只是服務(wù)器名.
Note: Origin的值可以是一個空字符串,這是很有用的.
注意,不僅僅是跨域請求,普通請求也會帶有ORIGIN頭信息.
Access-Control-Request-Method
在發(fā)出預(yù)檢請求時帶有這個頭信息,告訴服務(wù)器在實(shí)際請求時會使用的請求方式
Access-Control-Request-Method: <method>
相關(guān)的例子可以在這里找到
Access-Control-Request-Headers
在發(fā)出預(yù)檢請求時帶有這個頭信息,告訴服務(wù)器在實(shí)際請求時會攜帶的自定義頭信息.如有多個,可以用逗號分開.
Access-Control-Request-Headers: <field-name>[, <field-name>]*
尊重版權(quán),參考博客Ajax+Spring MVC實(shí)現(xiàn)跨域請求(JSONP),
HTTP訪問控制