1 JSONP的原理與實(shí)現(xiàn)
1.1 同源策略
前端跨域是每個(gè)前端人繞不過的坎,也是必須了解的一個(gè)知識(shí)點(diǎn)。我記得第一次遇到前端跨域這個(gè)坑的時(shí)候,真是無語到極點(diǎn),對于一個(gè)移動(dòng)端出身的人來說,覺得這個(gè)玩意無可理喻。但是后來慢慢了解,覺得前端的同源策略是非常有必要的。同源策略就是瀏覽器默認(rèn)讓www.baidu.com不能加載來自www.google.com的數(shù)據(jù)。對于現(xiàn)在來說,所有數(shù)據(jù)都是同源的可能性基本上很小,比如我們公司靜態(tài)資源www.image.com和前端資源www.htmlcss.com的CDN路徑都不一樣,前端獲取后臺(tái)數(shù)據(jù)www.apidata.com又是另一個(gè)地址。如何解決這個(gè)坑呢?我們公司通過兩種方式來避開。具體就是通過設(shè)置Access-Control-Allow-Origin來做POST請求,用JSONP來實(shí)現(xiàn)GET請求,因?yàn)?code>JSONP只能實(shí)現(xiàn)GET請求。
1.1.1 通過Access-Control-Allow-Origin支持跨域
有些人肯定就納悶了,我就喜歡跨域,我就不關(guān)注安全,難道就沒有辦法了嗎?當(dāng)然是否定的。你需要做的,只是讓服務(wù)器在返回的header里面加上Access-Control-Allow-Origin這個(gè)域就可以了。這樣瀏覽器在接收到服務(wù)器返回的數(shù)據(jù),就不會(huì)因?yàn)檫`反同源策略限制你拿到數(shù)據(jù)了。下面就用抓包來具體看一下:
當(dāng)我打開這里點(diǎn)開h5鏈接這個(gè)鏈接的時(shí)候。會(huì)去https//m.ctrip.com通過POST請求數(shù)據(jù),這里就用到了跨域。
:method: POST
:authority: m.ctrip.com
:scheme: https
:path: /restapi/xyz
content-length: 290
pragma: no-cache
cache-control: no-cache
accept: application/json
origin: https://pages.ctrip.com
user-agent: Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Mobile Safari/537.36
content-type: application/json;charset=UTF-8
referer: https://pages.ctrip.com/ztrip
accept-encoding: gzip, deflate, br
accept-language: zh-CN,zh;q=0.9,zh-TW;q=0.8,en;q=0.7
{請求體,post請求的參數(shù)}
服務(wù)器返回的響應(yīng)頭如下:
:status: 200
server: Tengine/2.1.2
date: Thu, 28 Dec 2017 11:01:29 GMT
content-type: application/json;charset=utf-8
access-control-allow-origin: *
access-control-expose-headers: RootMessageId
cache-control: private
vary: Accept-Encoding
clogging_trace_id: 8196881814119217567
rootmessageid: 921812-0a0e0de1-420683-219524
x-powered-by: CTrip/SOA2.0 Win32NT/.NET
soa20-response-status: Success
x-aspnet-version: 4.0.30319
x-powered-by: ASP.NET
x-gate: ctrip-gate
x-gate-instance: unknown
x-originating-url: http://m.ctrip.com/xyz
x-gate-remote-call-cost: 9
content-encoding: gzip
slb-http-protocol-version: HTTP/2.0
access-control-expose-headers: slb-http-protocol-version
{服務(wù)器返回的有用數(shù)據(jù)}
我們可以看到,這里有access-control-allow-origin這個(gè)響應(yīng)域就解決了問題。這個(gè)方法是最簡單的,而且前端POST請求最常見的方法(不確定還有其他好的解決方案)。這種方式最好就是通過他獲取服務(wù)數(shù)據(jù),不要加載js腳本。小心被別人注入攻擊。
1.1.2 JSONP的基本原理
講JSONP之前,我先亮出一段常見的代碼。下面這個(gè)方法主要就是動(dòng)態(tài)的創(chuàng)建一個(gè)script標(biāo)簽,然后設(shè)置src屬性。并且添加到document的第一個(gè)script標(biāo)簽之前。也就是說動(dòng)態(tài)去加載一個(gè)javscript腳本。
function loadJs(src, attrs = {}) {
return new Promise((resolve, reject) => {
const ref = document.getElementsByTagName('script')[0]
//創(chuàng)建一個(gè)scrpt標(biāo)簽
const script = document.createElement('script')
//設(shè)置script標(biāo)簽的資源路徑
script.src = src
script.async = true
//設(shè)置屬性
for (let key in attrs) {
script.setAttribute(key, attrs[key])
}
//script標(biāo)簽加入document中
ref.parentNode.insertBefore(script, ref)
script.onload = resolve
script.onerror = reject
})
}
最有意思的是script標(biāo)簽的src不受跨域限制。也就是說wwww.baidu.com的文件可以通過上面這個(gè)方法無限制的加載www.google.com的js文件。這個(gè)就是JSONP的實(shí)現(xiàn)的最基本原理。每一個(gè)JSONP請求就是動(dòng)態(tài)的創(chuàng)建script元素,然后通過src屬性去加載數(shù)據(jù),而且一般是通過callback這個(gè)回調(diào)方法來返回服務(wù)器數(shù)據(jù),然后再把script標(biāo)簽移除。如此周而復(fù)始的循環(huán),想想都累啊。下面看一個(gè)JSON的標(biāo)準(zhǔn)格式,服務(wù)器會(huì)獲取到callback這個(gè)回調(diào)方法。然后通過方法調(diào)用的方式把數(shù)據(jù)返回來,也就是執(zhí)行callbackFun方法。serverdata就是服務(wù)器給客戶端的數(shù)據(jù)。至于callback這個(gè)名字,可以自己定義,有客戶端和服務(wù)器商量決定。
function callbackFun(serverdata){
console.log(serverdata)
}
<script src="http://wwww.baidu.com/jsonp.js?callback=callbackFun"></script>
1.2 JSONP的實(shí)現(xiàn)
下面我會(huì)對JSONP做一個(gè)最基本的實(shí)現(xiàn)。使用Vue和node.js分別實(shí)現(xiàn)客戶端和服務(wù)端,代碼地址。
首先我們先看客戶端的實(shí)現(xiàn):
//獲取header的第一個(gè)子元素
let container = document.getElementsByTagName("head")[0];
/**
* 生成隨機(jī)字符串
*/
function makeid() {
var text = "";
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (var i = 0; i < 5; i++)
text += possible.charAt(Math.floor(Math.random() * possible.length));
return text;
}
/**
* jsonp請求的實(shí)現(xiàn)。返回一個(gè)promise對象對應(yīng)請求成功和請求失敗。
* @param {*請求的url} url
* @param {*請求的參數(shù)} options
*/
function jsonpRequest(url, options) {
return new Promise((resolve, reject) => {
try {
if (!url) {
reject({
err: new Error("url不能為空"),
result: null
});
}
if (!document || !global) {
reject({
err: new Error("系統(tǒng)環(huán)境有問題"),
result: null
});
}
//創(chuàng)建一個(gè)script元素
let scriptNode = document.createElement("script");
//請求參數(shù)
let data = options || {};
//回調(diào)函數(shù)的具體值,服務(wù)器和客戶端就根據(jù)這個(gè)方法名來確定請求與返回?cái)?shù)據(jù)之間的對應(yīng)。
let fnName = "jsonp" + makeid();
// 把callback加入請求參數(shù)中
data["callback"] = fnName;
// 拼接url
var params = [];
//參數(shù)的拼接與處理
for (let [key, value] of Object.entries(data)) {
params.push(encodeURIComponent(key) + "=" + encodeURIComponent(data[key]));
}
url = (url.indexOf("?")) > 0 ? (url + "&") : (url + "?");
url += params.join("&");
//把處理好的url賦值給script元素的src屬性。
scriptNode.src = url;
// 把回調(diào)函數(shù)暴露為全局方法。script加載回來以后,會(huì)執(zhí)行fnName對應(yīng)的這個(gè)方法。
global[fnName] = function(ret) {
resolve({
err: null,
result: ret
})
//請求完成。刪除script元素
container.removeChild(scriptNode);
//全局對象中刪除已經(jīng)請求完成的回調(diào)方法
delete global[fnName];
}
// script元素遇到錯(cuò)誤
scriptNode.onerror = function(err) {
reject({
err: err,
result: null
})
//刪除script元素和全局回調(diào)方法
container.removeChild(scriptNode);
global[fnName] && delete global[fnName];
}
//指定元素類型
scriptNode.type = "text/javascript";
//把script元素添加到header元素中。到這里script元素就會(huì)自動(dòng)加載src。也就是我們的請求發(fā)出去了。
container.appendChild(scriptNode)
} catch (error) {
//異常處理捕獲
reject({
err: error,
result: null
});
}
});
}
export default jsonpRequest;
這段代碼主要做了如下幾件事:
- 創(chuàng)建一個(gè)
script標(biāo)簽元素,并且添加到header元素里面。 - 拼接
script元素的src屬性,其中必然好漢callback這個(gè)參數(shù),服務(wù)端根據(jù)這個(gè)參數(shù)的值回調(diào)。 - 回調(diào)以后需要手動(dòng)把
script標(biāo)簽元素移除,并且刪除全局的回調(diào)函數(shù)名。
客戶端的使用如下,是不是感覺簡潔明了,比ES5的回調(diào)爽多了:
import jsonpRequest from "../lib/jsonpRequest.js";
async sendJSONPRequest() {
//參數(shù)
let params = {
name: "老黃",
site: "www.huangchengdu.com"
};
this.showLoading();
//發(fā)送請求
let {
err,
result
} = await jsonpRequest(
"https://www.huangchengdu.com/jsonp/jsonpRequest",
params
);
//處理返回的數(shù)據(jù)
this.hiddenLoading();
if (err) {
alert(err.message || "請求出錯(cuò)了");
this.serverData.err = JSON.stringify(err);
} else {
this.serverData = result;
}
}
服務(wù)端的實(shí)現(xiàn)如下。
let express = require('express');
let router = express.Router();
//JSONP請求
router.get('/jsonpRequest', function(req, res, next) {
//console.log("=====================" + JSON.stringify(req.query));
//獲取name和site參數(shù)的值
let name = req.query.name;
let site = req.query.site;
//拼接回調(diào)值
let serverres = {
serverReceive:{
name:name,
site:site
},
serverSend:"hello," + name + ".your site is https://" + site
}
//返回值。其實(shí)就是callback....()種種類型javascript字符串
res.end(req.query.callback + "(" + JSON.stringify(serverres) + ")")
});
module.exports = router;
服務(wù)端代碼說明如下:
-
res.end是express表示對http請求返回。具體返回的數(shù)據(jù)類似于callback隨機(jī)數(shù)(服務(wù)端數(shù)據(jù))這種類型。 - 客戶端在收到
callback隨機(jī)數(shù)(服務(wù)端數(shù)據(jù))這個(gè)數(shù)據(jù)以后,會(huì)自動(dòng)按照javascript腳本解析執(zhí)行。具體就是一個(gè)全局方法調(diào)用,方法名是callback隨機(jī)數(shù),參數(shù)是服務(wù)端數(shù)據(jù)。這樣就實(shí)現(xiàn)了服務(wù)端數(shù)據(jù)的回調(diào)。 - 客戶端在global對象下面注冊了
callback隨機(jī)數(shù)這個(gè)方法。具體代碼是上面global[fnName] = function(ret) {這一行。 -
callback隨機(jī)數(shù)是服務(wù)端和客戶端商量,具體可以自己決定,真實(shí)的時(shí)候類似于callbacksuijishu這種類型。
1.2.1 JSONP請求報(bào)文
JSONP本質(zhì)上就是一個(gè)普通的GET請求。無非就是這個(gè)請求是通過script標(biāo)簽來發(fā)送的。而且請求參數(shù)里面必定會(huì)有一個(gè)callback參數(shù)。
下面我們具體抓包看一下我們的請求報(bào)文:
GET /jsonp/jsonpRequest?name=%E8%80%81%E9%BB%84&site=www.huangchengdu.com&callback=jsonpiFuL4 HTTP/1.1
Host: www.huangchengdu.com
Accept: */*
Connection: keep-alive
Cookie: session=s%3Anot8KTW5FiTLY0VNgrrKksXY96AE2kWT.hrQeyL%2BVjt8ICJjfFqoFdV8JV3lx0IsDntx%2B%2Bc%2FEM98
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/604.4.7 (KHTML, like Gecko) Version/11.0.2 Safari/604.4.7
Accept-Language: zh-cn
Referer: http://localhost:8081/
Accept-Encoding: br, gzip, deflate
返回報(bào)文:
HTTP/1.1 200 OK
Server: nginx/1.6.2
Date: Fri, 29 Dec 2017 03:26:31 GMT
X-Powered-By: Express
Transfer-Encoding: chunked
Connection: Keep-alive
jsonpiFuL4({"serverReceive":{"name":"è??é??","site":"www.huangchengdu.com"},"serverSend":"hello,è??é??.your site is https://www.huangchengdu.com"})
從上面的報(bào)文我們可以返現(xiàn)。請求的callback參數(shù)的值和返回的響應(yīng)體的名稱是一樣的。響應(yīng)提就是一個(gè)普通的函數(shù)。服務(wù)器返回的數(shù)據(jù)作為函數(shù)的參數(shù)。
2 XSS攻擊
XSS的全稱是Cross-site scripting,翻譯過來就是跨站腳本。script可以跨域加載腳本這個(gè)特性,合理利用比如JSONP。如果不合理利用,比如某個(gè)壞人通過某種方式,讓你的瀏覽器去加載惡意的javascrpt腳本,必然就會(huì)導(dǎo)致敏感信息被盜或者財(cái)務(wù)損失。最常見的就是XSS攻擊,其實(shí)就是注入惡意腳本。真是凡事都有利有弊,就看如何使用了。常用的XSS攻擊手段和目的有如下幾種:
- 盜用cookie,獲取敏感信息。
- 利用植入Flash,通過crossdomain權(quán)限設(shè)置進(jìn)一步獲取更高權(quán)限;或者利用Java等得到類似的操作。
- 利用iframe、frame、XMLHttpRequest或上述Flash等方式,以(被攻擊)用戶的身份執(zhí)行一些管理動(dòng)作,或執(zhí)行一些一般的如發(fā)微博、加好友、- 發(fā)私信等操作。
- 利用可被攻擊的域受到其他域信任的特點(diǎn),以受信任來源的身份請求一些平時(shí)不允許的操作,如進(jìn)行不當(dāng)?shù)耐镀被顒?dòng)。
- 在訪問量極大的一些頁面上的XSS可以攻擊一些小型網(wǎng)站,實(shí)現(xiàn)DDoS攻擊的效果。
如果某一個(gè)字符串里面有var a = 1;<script>alert('我是你大爺')</script>;var b = 2;這種類型的字符串。而且我們剛好要通過script標(biāo)簽加載。那么他就會(huì)彈出一個(gè)我是你大爺。避免的方式就是把存在這種可能性的地方都處理過,如果包含類似<script>這種字符的腳本就處理掉或者干脆返回錯(cuò)誤。目前最常見的預(yù)防操作有如下幾種:
- 將重要的cookie標(biāo)記為http only,這樣的話Javascript 中的document.cookie語句就不能獲取到cookie了。
- 只允許用戶輸入我們期望的數(shù)據(jù)。例如:年齡的textbox中,只允許用戶輸入數(shù)字。 而數(shù)字之外的字符都過濾掉。
- 對數(shù)據(jù)進(jìn)行Html Encode處理。
- 過濾或移除特殊的Html標(biāo)簽,例如:<script>,<iframe>,<for<,>for>,"for。
- 過濾JavaScript事件的標(biāo)簽。例如"onclick=","onfocus"等等。
3 CSRF攻擊
這玩意我了解不多,也無法做出模擬操作??缯菊埱髠卧欤ㄓ⒄Z:Cross-site request forgery),也被稱為one-click attack或者session riding,通??s寫為 CSRF 或者 XSRF, 是一種挾制用戶在當(dāng)前已登錄的Web應(yīng)用程序上執(zhí)行非本意的操作的攻擊方法。[1] 跟跨網(wǎng)站腳本(XSS)相比,XSS 利用的是用戶對指定網(wǎng)站的信任,CSRF 利用的是網(wǎng)站對用戶網(wǎng)頁瀏覽器的信任。
我的理解就是,比如你剛?cè)ヌ詫氋I了東西,并且瀏覽器有你的session護(hù)著cookie之類的信息。然后你馬上又進(jìn)入一個(gè)不該去的網(wǎng)站,并且點(diǎn)擊了里面的一個(gè)淘寶鏈接,然后在你不知情的情況下做一些違法操作。這樣阿里后臺(tái)是不知道的,因?yàn)槟銊倓偼ㄟ^合法手段買了東西,從而達(dá)到在你不知情的情況下,而且淘寶也信任你的情況下,畏畏縮縮偷偷摸摸的干壞事。
3.1 SCRF預(yù)防
檢查Referer字段,通過這個(gè)字段來判斷用戶是從那個(gè)地址跳轉(zhuǎn)到當(dāng)前地址的。HTTP頭中有一個(gè)Referer字段,這個(gè)字段用以標(biāo)明請求來源于哪個(gè)地址。在處理敏感數(shù)據(jù)請求時(shí),通常來說,Referer字段應(yīng)和請求的地址位于同一域名下。以上文銀行操作為例,Referer字段地址通常應(yīng)該是轉(zhuǎn)賬按鈕所在的網(wǎng)頁地址,應(yīng)該也位于www.examplebank.com之下。而如果是CSRF攻擊傳來的請求,Referer字段會(huì)是包含惡意網(wǎng)址的地址,不會(huì)位于www.examplebank.com之下,這時(shí)候服務(wù)器就能識(shí)別出惡意的訪問。這種辦法簡單易行,工作量低,僅需要在關(guān)鍵訪問處增加一步校驗(yàn)。但這種辦法也有其局限性,因其完全依賴瀏覽器發(fā)送正確的Referer字段。雖然http協(xié)議對此字段的內(nèi)容有明確的規(guī)定,但并無法保證來訪的瀏覽器的具體實(shí)現(xiàn),亦無法保證瀏覽器沒有安全漏洞影響到此字段。并且也存在攻擊者攻擊某些瀏覽器,篡改其Referer字段的可能。
添加校驗(yàn)token,這個(gè)就最常見了,現(xiàn)在那個(gè)前端網(wǎng)站還不加一個(gè)驗(yàn)證碼啊。不管你如何千變?nèi)f化,你驗(yàn)證碼中是用戶數(shù)據(jù)的吧,而且現(xiàn)在好像越來越流行手機(jī)號碼驗(yàn)證了。CSRF的本質(zhì)在于攻擊者欺騙用戶去訪問自己設(shè)置的地址,所以如果要求在訪問敏感數(shù)據(jù)請求時(shí),要求用戶瀏覽器提供不保存在cookie中,并且攻擊者無法偽造的數(shù)據(jù)作為校驗(yàn),那么攻擊者就無法再執(zhí)行CSRF攻擊。這種數(shù)據(jù)通常是表單中的一個(gè)數(shù)據(jù)項(xiàng)。服務(wù)器將其生成并附加在表單中,其內(nèi)容是一個(gè)偽亂數(shù)。當(dāng)客戶端通過表單提交請求時(shí),這個(gè)偽亂數(shù)也一并提交上去以供校驗(yàn)。正常的訪問時(shí),客戶端瀏覽器能夠正確得到并傳回這個(gè)偽亂數(shù),而通過CSRF傳來的欺騙性攻擊中,攻擊者無從事先得知這個(gè)偽亂數(shù)的值,服務(wù)器端就會(huì)因?yàn)樾r?yàn)token的值為空或者錯(cuò)誤,拒絕這個(gè)可疑請求。