一币呵、同源:域名怀愧、端口侨颈、協(xié)議都相同。如果有一個(gè)不同芯义,則是跨域哈垢。
同源策略是瀏覽器的安全策略,用來阻止origin文檔或者是它加載的腳本與另外一個(gè)源的資源進(jìn)行交互扛拨≡欧郑可以阻擋惡意文檔,減少被攻擊的路徑绑警。
同源策略是一種約定求泰,web是構(gòu)建在此基礎(chǔ)之上的,瀏覽器只是真的就它的一種實(shí)現(xiàn)计盒。
當(dāng)在瀏覽器打開兩個(gè)tab頁渴频,分別是百度谷歌,當(dāng)在百度加載腳本時(shí)章郁,將會(huì)檢查這個(gè)腳本是否同源枉氮,如果不是,則會(huì)拒絕訪問暖庄。
同源策略是瀏覽器的行為聊替,為了保護(hù)本地?cái)?shù)據(jù)不被請(qǐng)求回來的數(shù)據(jù)污染,攔截的是客戶端發(fā)出的請(qǐng)求請(qǐng)求回來的數(shù)據(jù)培廓,服務(wù)器響應(yīng)了數(shù)據(jù)惹悄,但是被同源策略攔截。
二肩钠、跨域:非同源資源之間嘗試通信泣港,將產(chǎn)生跨域
同源策略出于安全考慮,限制了以下行為:Cookie价匠、LocalStorage当纱、IndexDB無法讀取,DOM和JS對(duì)象無法獲取踩窖、Ajax請(qǐng)求發(fā)送不出去
但是有三個(gè)標(biāo)簽允許跨域加載資源:<img src=xxx>
坡氯、<link href>
、<script src=xxx>
觸發(fā)跨域:非同源請(qǐng)求洋腮,服務(wù)端設(shè)置cors限制箫柳。
特別注意:
第一:如果是協(xié)議和端口造成的跨域問題,前臺(tái)是無法解決的啥供。
第二:在跨域問題上悯恍,僅僅是通過"URL的首部"來識(shí)別而不會(huì)根據(jù)域名對(duì)應(yīng)的IP地址是否相同來判斷,"URL首部"可以理解為協(xié)議伙狐、域名涮毫、端口瞬欧。
第三:跨域并不是請(qǐng)求發(fā)不出去,請(qǐng)求能發(fā)出去罢防,服務(wù)端能收到請(qǐng)求并正常返回結(jié)果黍判,只是結(jié)果被瀏覽器攔截了。表單方式可以發(fā)起跨域請(qǐng)求是因?yàn)樗粫?huì)獲取新的內(nèi)容篙梢,所以可以發(fā)送請(qǐng)求顷帖,這也說明了跨域并不能完全阻止CSRF,因?yàn)樽柚沟闹皇琼憫?yīng)消息渤滞。
三贬墩、解決跨域方式
1.通過jsonp跨域
-原理:利用<script>標(biāo)簽沒有跨域限制的漏洞,網(wǎng)頁可以得到從其他來源動(dòng)態(tài)產(chǎn)生的JSON數(shù)據(jù)妄呕。JSONP請(qǐng)求一定需要對(duì)方的服務(wù)器做支持才可以陶舞。
-JSONP和AJAX對(duì)比:JSONP和AJAX相同,都是客戶端向服務(wù)端發(fā)送請(qǐng)求绪励,從服務(wù)器端獲取數(shù)據(jù)的方式肿孵。但是AJAX屬于同源策略,JSONP屬于非同源策略(跨域請(qǐng)求)
-JSONP優(yōu)缺點(diǎn):JSONP的有點(diǎn)是簡單疏魏,兼容性好停做,可用于解決主流瀏覽器的跨域數(shù)據(jù)訪問的問題。缺點(diǎn)是僅支持get方法具有局限性大莫,不安全可能會(huì)遭受XSS攻擊蛉腌。
-JSONP的實(shí)現(xiàn)流程:
1.聲明一個(gè)回調(diào)函數(shù)(show),其函數(shù)名當(dāng)作參數(shù)值只厘,傳遞給跨域請(qǐng)求數(shù)據(jù)的服務(wù)器烙丛,函數(shù)形參為要獲取的目標(biāo)數(shù)據(jù)(服務(wù)器返回的data)。
2.創(chuàng)建一個(gè)<script>標(biāo)簽羔味,把跨域的API數(shù)據(jù)接口地址河咽,賦值給script的src,還要在這個(gè)地址中向服務(wù)器傳遞第一步創(chuàng)建好的回調(diào)函數(shù)函數(shù)名(可以通過問號(hào)傳參赋元?callback=show)忘蟹,服務(wù)器接收到請(qǐng)求后,需要進(jìn)行特殊的處理们陆,把傳遞進(jìn)來的函數(shù)名和它需要給你的數(shù)據(jù)拼接成一個(gè)字符串寒瓦,例如傳遞進(jìn)來的函數(shù)名是show情屹,它準(zhǔn)備好的數(shù)據(jù)是show('我不愛你')坪仇。
3.最后服務(wù)器把準(zhǔn)備的數(shù)據(jù)通過HTTP協(xié)議返回給客戶端,客戶端再調(diào)用執(zhí)行之前聲明的回調(diào)函數(shù)(show)垃你,對(duì)返回的數(shù)據(jù)進(jìn)行操作椅文。
在開發(fā)鐘可能會(huì)遇到多個(gè)JSONP請(qǐng)求的回調(diào)函數(shù)名是相同的喂很,這時(shí)候就需要自己封裝一個(gè)JSONP函數(shù)。
function jsonp({url,params,callback}){ //三個(gè)參數(shù)皆刺,地址少辣,參數(shù),回調(diào)函數(shù)名
return new Promise((resolve,reject) =>{//返回一個(gè)promise對(duì)象羡蛾,異步執(zhí)行下一步
let script = document.createElement('script') //獲取script的dom
window[callback] = function(data){ //給window添加用了一個(gè)callback方法
resolve(data)
document.body.removeChild(script) //移除創(chuàng)建的標(biāo)簽
}
params = { ...params , callback } //將params和callback合并
let arrs = []
for(let key in params){
arrs.push(`${key}=${params[key]}`) //遍歷對(duì)象漓帅,依次放入數(shù)組得到wd=b&callback=show的數(shù)據(jù)
}
script.src = `${url}?${arrs.join('&')}`//拼接url和參數(shù)
document.body.appendChild(script) //往dom添加script標(biāo)簽
})
}
jsonp({
url:'http://localhost:3000/say',
params:{wd:'I love you'},
callback:'show'
}).then(data=>{
console.log(data)
})
上面這段代碼相當(dāng)于向http://localhost:3000/say?wd=Iloveyou&callback=show
這個(gè)地址請(qǐng)求數(shù)據(jù),然后后臺(tái)返回show('我不愛你')痴怨,最后會(huì)運(yùn)行show()這個(gè)函數(shù)忙干,打印出我不愛你。
//serve.js
let express = require('express')
let app = express().Route
app.get('/say',function(req,res,next)=>{
let {wd,callback} = req.query
console.log(wd)
console.log(callback)
res.send(`${callback}('我不愛你')`)
})
app,listen(3000)
總結(jié):使用jsonp方法浪藻,前后端都需要操作捐迫,前端需要通過script標(biāo)簽,進(jìn)行跨域請(qǐng)求爱葵,首先把url施戴,params,callback進(jìn)行處理,得到一長串的url+參數(shù)+回調(diào)函數(shù)萌丈,再把它放到script的src中赞哗,后端接收到請(qǐng)求后,首先解構(gòu)賦值得到參數(shù)以及回調(diào)函數(shù)名辆雾,接著返回函數(shù)名+data的字符串拼接懈玻,接著前端因?yàn)槭且粋€(gè)promise對(duì)象,所以會(huì)接著執(zhí)行給callback函數(shù)乾颁,resolve去打印了data涂乌,同時(shí)刪除了script標(biāo)簽。
-jQuery的jsonp形式
JSONP都是GET和異步請(qǐng)求的英岭,不存在其它的請(qǐng)求方式和同步請(qǐng)求湾盒,且jQuery默認(rèn)就會(huì)給JSONP的請(qǐng)求清楚緩存。
$.ajax({
url:'http://myapp.com/jsonServerResponse',
dataType:'jsonp',
type:'get', //可以省略
jsonCallback:'show', //自定義傳送給服務(wù)器的函數(shù)名诅妹,而不是使用jQuery自動(dòng)生成的罚勾,可忽略
jsonp:'callback', //把傳遞函數(shù)名的形參設(shè)定,可忽略吭狡。
sucess:function(data){
console.log(data)
}
})
2.CORS解決跨域
cors需要瀏覽器和后端同時(shí)支持尖殃。IE8和IE9需要通過XDomainRequest來實(shí)現(xiàn)。
瀏覽器會(huì)自動(dòng)進(jìn)行CORS通信划煮,實(shí)現(xiàn)CORS通信的關(guān)鍵是后端送丰。只要后端實(shí)現(xiàn)了CORS,就實(shí)現(xiàn)了跨域弛秋。
服務(wù)端設(shè)置Access-Control-Allow-Origin就可以開啟CORS器躏。該屬性表示哪些域名可以訪問資源俐载,如果設(shè)置了通配符,那么所有網(wǎng)站都可以訪問資源登失。
雖然設(shè)置CORS和前端沒什么關(guān)系遏佣,但是通過這種方式解決跨域問題的話,會(huì)在發(fā)送請(qǐng)求時(shí)出現(xiàn)兩種情況揽浙,分別是簡單請(qǐng)求和復(fù)雜請(qǐng)求状婶。
(1)簡單請(qǐng)求
只要同時(shí)滿足以下兩大條件,就屬于簡單請(qǐng)求
條件1:使用下列方法之一:GET/HEAD/POST
條件2:Content-Type的值僅限于下列三者之一:text/plain馅巷、multipart/form-data太抓、application/x-www-from-urlencoded
請(qǐng)求中的任意XML對(duì)象均沒有注冊(cè)任何事件監(jiān)聽器;XML對(duì)象可以使用XML.HttpRequest.upload屬性訪問令杈。
(2)復(fù)雜請(qǐng)求
不符合簡單請(qǐng)求規(guī)則的便是復(fù)雜請(qǐng)求走敌,復(fù)雜請(qǐng)求的CORS設(shè)置,會(huì)在正式通信之前逗噩,增加一次HTTP查詢請(qǐng)求掉丽,稱為預(yù)檢請(qǐng)求,該請(qǐng)求是option方法的异雁,通過該請(qǐng)求來知道服務(wù)端是否允許跨域請(qǐng)求捶障。
我們用PUT向后臺(tái)請(qǐng)求時(shí),就屬于復(fù)雜請(qǐng)求纲刀,后臺(tái)需做如下配置:
res.setHeader('Access-Control-Allow-Methods','PUT') //允許訪問的方法
res.setHeader('Access-Control-Max-Age',) //預(yù)檢的存活時(shí)間
if(req.method === 'OPTIONS' ){
res.end()
}
app.put('/getData',function(req,res){
console.log(req.headers)
res.end('i don not love you')
})
接下來看一個(gè)完整例子以及CORS請(qǐng)求的相關(guān)字段项炼。
//index.html
let xhr = new XMLHttpRequest() //new一個(gè)XML對(duì)象
document.cookie = 'name = xiamen' //cookies不能跨域
xhr.withCredentials = true //前端設(shè)置是否可以攜帶cookie
xhr.open('PUT','http://localhost:4000/getData',true)
xhr.setRequestHeader('name','xiamen') //設(shè)置請(qǐng)求頭
xhr.onreadystatechange = function (){
if(xhr.readyState === 4){
if((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
console.log(xhr.response)
//得到響應(yīng)頭,后臺(tái)需要設(shè)置Access-Control-Expose-Headers
console.log(xhr.getResponseHeader('name'))
}
}
}
xhr.send()
//server1.js
let express = require('express')
let app = express()
app.use(express.static(__dirname))
app.listen(3000)
//server2.js
let express = require('express')
let app = express()
let whitList = ['http://localhost:3000'] //設(shè)置白名單
app.use(function(req,res,next){
let origin = req.headers.origin
if(whitList.includes(origin)){
res.setHeader('Access-Control-Allow-Origin',origin) //允許哪個(gè)源訪問
res.setHeader('Access-Control-Allow-Headers','name') //允許哪個(gè)頭可以訪問
res.setHeader('Access-Control-Allow-Methods','PUT') //允許的方法
res.setHeader('Access-Control-Allow-Credentials',true)//允許攜帶cookie
res.setHeader('Access-Control-Max-Age',6) //預(yù)檢的存活時(shí)間
res.setHeader('Access-Control-Expose-Headers','name') //允許返回的頭
if(res.method === 'OPTIONS'){
res.end() //OPTIONS請(qǐng)求不做任何處理
}
}
next()
})
app.put('/getData',function(req,res){
console.log(req.headers)
res.setHeader('name','jw') //返回一個(gè)響應(yīng)頭示绊,后臺(tái)需要設(shè)置
res.send('啦啦啦')
})
app.get('/getData',function(req,res){
console.log(req.headers)
res.end('我不愛你')
})
app.use(express.static(__dirname))
app.listen(4000)
上述代碼由http:localhost:3000/index.html
向http://localhost:4000/
跨域請(qǐng)求锭部,正如我們上面說的,后端是實(shí)現(xiàn)CORS通信的關(guān)鍵面褐。
補(bǔ)充
cors跨域解決方式:簡單請(qǐng)求時(shí)拌禾,瀏覽器會(huì)直接發(fā)出CORS請(qǐng)求(在頭信息之中,增加一個(gè)origin字段)展哭。origin字段('Origin: http://api.bob.com')用來說明本次請(qǐng)求來自哪個(gè)源(協(xié)議+端口+域名)湃窍,服務(wù)器根據(jù)這個(gè)值,決定是否同意這次請(qǐng)求匪傍。如果后端接收到的Origin指定的源您市,不在許可的范圍,服務(wù)器會(huì)返回一個(gè)正常的HTTP回應(yīng)役衡。瀏覽器發(fā)現(xiàn)茵休,這個(gè)回應(yīng)的頭信息沒有包含Access-Control-Allow-Origin
字段,就知道出錯(cuò)了,于是拋出錯(cuò)誤泽篮,被XML的onerror函數(shù)捕獲。注意的是柑船,這種錯(cuò)誤無法通過狀態(tài)碼識(shí)別帽撑,因?yàn)镠TTP回應(yīng)的狀態(tài)碼有額能是200。
如果Origin指定的域名在許可范圍內(nèi)鞍时,服務(wù)器返回的響應(yīng)亏拉,會(huì)多出幾個(gè)頭信息字段。
Access-Control-Allow-Origin:http://api.bob.com
//必須的逆巍,是請(qǐng)求時(shí)的origin字段的值或是*及塘,表示接收任何域名的請(qǐng)求
Access-Control-Allow-Credentials:true
//是否瀏覽器允許發(fā)送Cookie
Access-Control-Expose-Headersw:FooBar
//可選,CORS請(qǐng)求時(shí)锐极,XML對(duì)象的getResponseHeader()方法只能拿到6個(gè)基本字段:`Cache-Control`笙僚、`Content-Language`、`Content-Type`灵再、`Last-Modified`肋层、`Pragma`、`Expires`翎迁。如果想拿到其他字段栋猖,就必須在Acess-Control-Expose-Headers里面指定、上面例子中指定汪榔,getResponseHeader('FooBar')可以返回FooBar字段的值蒲拉。
Content-Type:text/html;charset=utf-8
withCredentials:CORS請(qǐng)求默認(rèn)不發(fā)送cookie和HTTP認(rèn)證信息,如果要把Cookie發(fā)到服務(wù)器痴腌,一方面需要服務(wù)器同意雌团,指定Access-Control-Allow-Credentials
字段為true,另外一方面士聪,開發(fā)者必須在AJAX請(qǐng)求中打開withCredentials屬性
var xhr = new XMLHttpRequest()
xhr.withCredentials = true
所以需要兩端一個(gè)配合才能實(shí)現(xiàn)cookie的傳送辱姨,但是有時(shí)候,就算我們沒有在前端設(shè)置這個(gè)值戚嗅,瀏覽器也會(huì)發(fā)送雨涛,我們可以將其設(shè)置為false進(jìn)行關(guān)閉。
如果需要發(fā)送cookie懦胞,Access-Control-Allow-Origin就不能設(shè)置為*號(hào)替久,必須指定明確,與網(wǎng)頁請(qǐng)求一直的域名躏尉,cookie依然遵循同源策略蚯根,只有用服務(wù)器域名設(shè)置的Cookie才會(huì)上傳,其他域名的Cookie并不會(huì)上傳。
復(fù)雜請(qǐng)求(非簡單請(qǐng)求)
比如請(qǐng)求方法是PUT或者是DELETE颅拦,或者Content-Type的字段是application/json等對(duì)服務(wù)器由特殊要求的請(qǐng)求蒂誉。
在正式通信前,增加一次HTTP查詢請(qǐng)求距帅,成為預(yù)檢請(qǐng)求右锨。
預(yù)檢請(qǐng)求中,瀏覽器先詢問服務(wù)器碌秸,當(dāng)前網(wǎng)頁的域名是否在服務(wù)器的許可名單中绍移,以及可以使用哪些HTTP動(dòng)詞和頭信息字段,只有得到肯定答復(fù)讥电,瀏覽器才會(huì)發(fā)出正式的XML請(qǐng)求蹂窖,否則就會(huì)報(bào)錯(cuò)。
我們來看一段瀏覽器的js腳本
var url = 'http://api.alice.com/cors'
var xhr = new XMLHttpRequest()
xhr.open('PUT',url,true) //第三個(gè)參數(shù)是是否異步
xhr.setRequestHeader('X-Custom-Header','value')
xhr.send()
上面代碼中恩敌,HTTP請(qǐng)求的方法是PUT瞬测,并且發(fā)送一個(gè)自定義頭信息X-Custom-Header,瀏覽器發(fā)現(xiàn)纠炮,這不是一個(gè)簡單的請(qǐng)求涣楷,就自動(dòng)發(fā)出一個(gè)‘預(yù)檢’請(qǐng)求,要求服務(wù)器確認(rèn)可以這樣請(qǐng)求抗碰,下面是這個(gè)預(yù)檢的請(qǐng)求頭信息狮斗。
OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com //域名信息
Access-Control-Request-Method: PUT //請(qǐng)求方法
Access-Control-Request-Headers: X-Custom-Header //請(qǐng)求頭鐘的特殊字段
Host: api.alice.com
Accept-Language: en-US //接收語言
Connection: keep-alive
User-Agent: Mozilla/5.0...
預(yù)檢請(qǐng)求用的方法是OPTIONS,表示這個(gè)請(qǐng)求是用來詢問的弧蝇,頭信息里面碳褒,關(guān)鍵字段是Origin,表示請(qǐng)求來自哪個(gè)源看疗。
除了Origin字段沙峻,‘預(yù)檢’請(qǐng)求的頭信息還包含兩個(gè)特殊的字段。
(1)Access-Control-Request-Method
必須的方法两芳,列舉瀏覽器會(huì)用到哪些方法
(2)Access-Control-Request-Headers
該字段是一個(gè)逗號(hào)分隔的字符串摔寨,指定瀏覽器CORS請(qǐng)求會(huì)額外發(fā)送的頭信息字段。
預(yù)檢請(qǐng)求的回應(yīng)
服務(wù)器收到預(yù)檢請(qǐng)求后怖辆,檢查了Origin斯议、Access-Control-Request-Method和Access-Control-Request-Headers字段后孝情,確認(rèn)允許跨域請(qǐng)求捐凭,就可以做出回應(yīng)
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT //時(shí)間
Server: Apache/2.0.61 (Unix) //服務(wù)器
Access-Control-Allow-Origin: http://api.bob.com //允許跨域的域名
Access-Control-Allow-Methods: GET, POST, PUT //允許的方法
Access-Control-Allow-Headers: X-Custom-Header //允許的額外頭信息
Content-Type: text/html; charset=utf-8 //內(nèi)容格式
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain
如果服務(wù)器否定了預(yù)檢請(qǐng)求奴潘,會(huì)返回一個(gè)正常的HTTP回應(yīng),但是滅有任何CORS相關(guān)的頭信息字段特咆。這是季惩,瀏覽器就會(huì)認(rèn)定,服務(wù)器不同意預(yù)檢請(qǐng)求,因此觸發(fā)錯(cuò)誤画拾,被XML的onerror捕獲啥繁,將會(huì)報(bào)錯(cuò)如下信息
XMLHttpRequest cannot load http://api.alice.com.
Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.
當(dāng)請(qǐng)求通過預(yù)檢之后,請(qǐng)求將和簡單請(qǐng)求一樣青抛,通過Access-Control-Allow-origin判斷瀏覽器發(fā)送過來的請(qǐng)求是否允許旗闽,允許,則做出正常的回應(yīng)脂凶。
CORS與JSONP的比較:
CORS和JSONP的使用目的相同宪睹,但是比jsonp更加強(qiáng)大愁茁。
JSONP只支持GET請(qǐng)求蚕钦,CORS支持所有類型的HTTP請(qǐng)求。JSONP的優(yōu)勢(shì)在于支持老瀏覽器鹅很,以及可以向不支持CORS的網(wǎng)站發(fā)請(qǐng)求嘶居。
3.iframe、hash
4.CORS(Cross-Origin-Resource-Sharing)
5.服務(wù)器跨域促煮,服務(wù)器中轉(zhuǎn)代理
6.其它