同源政策
Ajax請求限制:
Ajax只能向自己的服務(wù)器發(fā)送請求。比如現(xiàn)在有一個A網(wǎng)站、 有一個B網(wǎng)站, A網(wǎng)站中的HTML文件只能向A網(wǎng)站服務(wù)器中發(fā)送Ajax請求,B網(wǎng)站中的HTML文件只能向B網(wǎng)站中發(fā)送Ajax請求,但是A網(wǎng)站是不能向B網(wǎng)站發(fā)送Ajax請求的,同理,B網(wǎng)站也不能向A網(wǎng)站發(fā)送Ajax請求。
什么是同源:
如果兩個頁面擁有相同的協(xié)議、域名和端口,那么這兩個頁面就屬于同一個源,其中只要有一個不相同,就是不同源。
http://www.example.com/dir/page.html
http://www.example.com/dir2/other.html:同源
http://example.com/dir/other.html:不同源(域名不同)
http://v2.www.example.com/dir/other.html:不同源(域名不同)
http://www.example.com:81/dir/other.html:不同源(端口不同)
https://www.example.com/dir/page.html:不同源(協(xié)議不同)
同源政策的目的:
同源政策是為了保證用戶信息的安全,防止惡意的網(wǎng)站竊取數(shù)據(jù)。最初的同源政策是指A網(wǎng)站在客戶端設(shè)置的Cookie,B網(wǎng)站是不能訪問的。
隨著互聯(lián)網(wǎng)的發(fā)展,同源政策也越來越嚴(yán)格,在不同源的情況下,其中有一項規(guī)定就是無法向非同源地址發(fā)送Ajax請求,如果請求,瀏覽器就會報錯。
以下有幾種跨域請求的方法:
1.使用JSONP解決同源限制問題
jsonp是json with padding的縮寫,它不屬于Ajax請求,但它可以模擬Ajax請求。
注意:JSONP不是Ajax,只是模擬Ajax發(fā)送數(shù)據(jù)
①將不同源的服務(wù)器端請求地址寫在script標(biāo)簽的src屬性中
在<script>的src屬性中是不受同源政策的限制的,也就是說它可以寫非同源的網(wǎng)站
<script src="www.example.com"></script> <script src=“https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
②服務(wù)器端響應(yīng)數(shù)據(jù)必須是一個函數(shù)的調(diào)用, 真正要發(fā)送給客戶端的數(shù)據(jù)需要作為函數(shù)調(diào)用的參數(shù)
const data = 'fn({name: "張三", age: "20"})';
res.send(data);
③在客戶端全局作用域下定義函數(shù)fn
function fn (data) { }
④在fn函數(shù)內(nèi)部對服務(wù)器端返回的數(shù)據(jù)進行處理
function fn (data) { console.log(data); }
注意:jsonp解決方案中的請求屬于get請求,因為它是通過script標(biāo)簽中的src屬性發(fā)送的請求,所以它傳遞的參數(shù)也是get請求參數(shù),具體的參數(shù)拼接在請求地址的后面。
JSONP代碼優(yōu)化:
(1)客戶端需要將函數(shù)名稱傳遞到服務(wù)器端。
客戶端寫的函數(shù)如何在服務(wù)器端調(diào)用呢?
注意,客戶端的這個函數(shù)是全局函數(shù),而且必須要寫在最前面。
<script>
function fn (data) {
console.log('客戶端的fn函數(shù)被調(diào)用了')
console.log(data);
}
</script>
<!-- 將非同源服務(wù)器端的請求地址寫在script標(biāo)簽的src屬性中 -->
<script src="http://localhost:3001/test"></script>
// 服務(wù)器端調(diào)用客戶端的fn函數(shù)
app.get('/test', (req, res) => {
const result = 'fn()';
res.send(result);
});
(2)將script請求的發(fā)送變成動態(tài)請求。
但是上面的代碼有三個問題:
① 客戶端函數(shù)是立即調(diào)用的,但我們想要的效果是動態(tài)請求發(fā)送,當(dāng)點擊按鈕之后,創(chuàng)建一個script標(biāo)簽,然后再將函數(shù)名傳遞到服務(wù)器端。
② 添加這個點擊按鈕之后出現(xiàn)了另外一個問題:
點擊一次按鈕,新增一個script標(biāo)簽,多次點擊就會創(chuàng)建很多個script標(biāo)簽,但是我們只需要一個就夠了。
解決方案:當(dāng)script標(biāo)簽將請求地址中的內(nèi)容加載完成以后,需要將它從body內(nèi)部刪除掉
③服務(wù)器端返回的函數(shù)調(diào)用名稱必須與客戶端定義的函數(shù)名稱保持一致。如果客戶端的函數(shù)名稱需要修改,則服務(wù)器端的函數(shù)名稱也必須要跟著修改,開發(fā)人員的溝通成本就比較高。
解決方案:只需要將客戶端函數(shù)的名字作為請求參數(shù)發(fā)送到服務(wù)器端,服務(wù)器端只需要接收到函數(shù)的名字,然后返回函數(shù)調(diào)用即可。
修改后的代碼如下:
<button id="btn">點我發(fā)送請求</button>
<script>
function fn2 (data) {
console.log('客戶端的fn函數(shù)被調(diào)用了')
console.log(data);
}
</script>
<script type="text/javascript">
// 獲取按鈕
var btn = document.getElementById('btn');
// 為按鈕添加點擊事件
btn.onclick = function () {
// 創(chuàng)建script標(biāo)簽
var script = document.createElement('script');
// 設(shè)置src屬性
script.src = 'http://localhost:3001/better?callback=fn2';
// 將script標(biāo)簽追加到頁面中
document.body.appendChild(script);
// 為script標(biāo)簽添加onload事件
script.onload = function () {
// 將body中的script標(biāo)簽刪除掉
document.body.removeChild(script);
}
}
</script>
// 服務(wù)器端調(diào)用客戶端的fn函數(shù)
app.get('/better', (req, res) => {
// 接收客戶端傳遞過來的函數(shù)的名稱
const fnName = req.query.callback;
// 將函數(shù)名稱對應(yīng)的函數(shù)調(diào)用代碼返回給客戶端
const result = fnName + '({name: "張三"})';
res.send(result);
});
(3)封裝jsonp函數(shù),方便請求發(fā)送。
function jsonp (options) {
// 動態(tài)創(chuàng)建script標(biāo)簽
var script = document.createElement('script');
// 為script標(biāo)簽添加src屬性
script.src = options.url;
// 將script標(biāo)簽追加到頁面中
document.body.appendChild(script);
// 為script標(biāo)簽添加onload事件, 等待script標(biāo)簽加載完之后再刪除
script.onload = function() {
// 將body中的script標(biāo)簽刪除掉
document.body.removeChild(script);
}
}
封裝jsonp方法有兩個問題:
① 雖然上面已經(jīng)封裝了jsonp函數(shù)用于發(fā)送請求,但是在客戶端,jsonp函數(shù)的其他地方,還需要另外定義一個全局函數(shù),用于接收服務(wù)器端返回的數(shù)據(jù),現(xiàn)在是發(fā)送一個請求要用到兩個函數(shù),而且兩個函數(shù)是獨立的,這樣的話就破壞了jsonp函數(shù)的封裝性,我們不能一眼就看出來哪個請求跟哪個函數(shù)是關(guān)聯(lián)的。如果可以像Ajax封裝函數(shù)一樣,將用于接收服務(wù)器端返回來的數(shù)據(jù)的函數(shù)當(dāng)作參數(shù)傳遞過去,即將處理請求函數(shù)變成success函數(shù),這樣的話函數(shù)的封裝性就比較好。
但是這樣就出現(xiàn)了另外兩個問題:
這個函數(shù)就不是全局函數(shù)了,服務(wù)器端在返回調(diào)用函數(shù)的時候就找不到這個函數(shù)了
解決方案:要想辦法把它變成一個全局函數(shù),只需要將該函數(shù)掛載在window全局對象下面就可以了。
這個函數(shù)就變成了匿名函數(shù)了,這樣我們在向服務(wù)器端傳遞名字的時候該傳遞什么呢?
解決方法:函數(shù)名字的問題同下面的問題②的解決方案,注意:函數(shù)名字不能是純數(shù)字
② 在真實的情況中可能要發(fā)送多次請求,每一次請求都要對應(yīng)自己的函數(shù)處理返回的結(jié)果,函數(shù)取名字也變成一個問題。如何解決函數(shù)名字的問題呢?只需要讓函數(shù)的名字隨機產(chǎn)生就可以了。
代碼修改如下:
function jsonp (options) {
// 動態(tài)創(chuàng)建script標(biāo)簽
var script = document.createElement('script');
// 拼接字符串的變量
var params = '';
for (var attr in options.data) {
params += '&' + attr + '=' + options.data[attr];
}
// myJsonp0124741
var fnName = 'myJsonp' + Math.random().toString().replace('.', '');
// 它已經(jīng)不是一個全局函數(shù)了
// 我們要想辦法將它變成全局函數(shù)
window[fnName] = options.success;
// 為script標(biāo)簽添加src屬性
script.src = options.url + '?callback=' + fnName + params;
// 將script標(biāo)簽追加到頁面中
document.body.appendChild(script);
// 為script標(biāo)簽添加onload事件
script.onload = function () {
document.body.removeChild(script);
}
}
// 獲取按鈕
var btn = document.getElementById('btn');
// 為按鈕添加點擊事件
btn.onclick = function () {
jsonp({
// 請求地址
url: 'http://localhost:3001/better',
data: {
name: 'lisi',
age: 30
},
success: function (data) {
console.log(data)
}
})
}
(4)服務(wù)器端代碼優(yōu)化之res.jsonp方法。
express框架中提供了一個jsonp方法,jsonp方法內(nèi)部干的其實就是注釋的那些事情:
接收客戶端傳遞過來的參數(shù),將真實的數(shù)據(jù)轉(zhuǎn)換為字符串再把它拼接起來,最終返回給客戶端。
app.get('/better', (req, res) => {
// 接收客戶端傳遞過來的函數(shù)名稱
// const fnName = req.query.callback;
// 將函數(shù)名稱對應(yīng)的函數(shù)調(diào)用代碼返回給客戶端
// const data = JSON.stringify({name: "張三"});
// const result = fnName + '(' + data + ')';
// setTimeout(() => {
// res.send(result);
// }, 1000);
res.jsonp({name: 'lisi', age: 20});
});
2.CORS跨域資源共享
除了jsonp方法可以實現(xiàn)跨域請求,另一種方式就是CORS跨域請求。
它跟jsonp的解決方案是不一樣的,jsonp是繞過了同源限制,發(fā)送的也不是Ajax請求。
而CORS直接允許瀏覽器向跨域的服務(wù)器發(fā)送Ajax請求,從而克服了Ajax只能同源使用的限制。
簡單來說,CORS這種解決方案就是,服務(wù)器端允許你跨域訪問它,你就可以跨域訪問它,服務(wù)器端不允許你跨域訪問它,你就不能訪問它。
這種解決方案主要是再服務(wù)器端做一些配置,客戶端保持原有的Ajax代碼不變即可。
CORS:全稱為Cross-origin resource sharing,即跨域資源共享,它允許瀏覽器向跨域服務(wù)器發(fā)送Ajax請求,克服了Ajax只能同源使用的限制。
origin: http://localhost:3000
Access-Control-Allow-Origin: 'http://localhost:3000' Access-Control-Allow-Origin: '*'
origin存儲的就是A網(wǎng)站的域名信息,包含協(xié)議、域名和端口號。服務(wù)器端會根據(jù)該域名信息來決定是否同意這次的請求。不管是否同意請求,服務(wù)器端都會返回給客戶端一個正常的HTTP響應(yīng)。
瀏覽器端如何判斷服務(wù)器端是否同意這次的請求呢?如果服務(wù)器端同意這次請求,會在響應(yīng)頭中加入Access-Control-Allow-Origin,如果不同意,則不會加。
這個字段的值通常是當(dāng)前訪問服務(wù)器端的客戶端的原信息,或者是返回*號,表示允許所有的客戶端都可以訪問該服務(wù)器端。
具體的代碼要如何實現(xiàn)呢?
客戶端依然使用Ajax代碼,不需要做出任何改變,客戶端需要做的事情瀏覽器會自動幫我們做好。
對于服務(wù)器端而言,我們需要設(shè)置兩項內(nèi)容,一項是允許哪些客戶端訪問服務(wù)器端,另一項是客戶端可以設(shè)置哪些請求方法來訪問服務(wù)器端。是使用get方法還是使用post方法,或者是兩者都可以,這要根據(jù)具體的需求來定。
這兩項信息都需要設(shè)置在響應(yīng)頭中。
express中使用res.header方法設(shè)置響應(yīng)頭。
Node服務(wù)器端設(shè)置響應(yīng)頭示例代碼:
// 在服務(wù)器端設(shè)置一個中間件,攔截所有的請求,然后再對所有的請求設(shè)置這兩個響應(yīng)頭。只需要在所有路由的最上方寫上app.use()
// 注意:必須要調(diào)用next()方法,不然所有的代碼都卡在這里了,就不會再繼續(xù)往下執(zhí)行了。
app.use((req, res, next) => {
// 允許哪些客戶端訪問,*代表所有的客戶端都可以訪問
res.header('Access-Control-Allow-Origin', '*');
// 允許客戶端使用哪些請求方式訪問
res.header('Access-Control-Allow-Methods', 'GET, POST');
next();
})
3.服務(wù)器端解決訪問非同源數(shù)據(jù)
同源政策是瀏覽器給予Ajax技術(shù)的限制,服務(wù)器端是不存在同源政策限制。
第三種跨域請求方法,這種方法也是繞過客戶端的同源政策的限制。
A網(wǎng)站的客戶端向A網(wǎng)站的服務(wù)器端發(fā)送請求,A網(wǎng)站的服務(wù)器端向B網(wǎng)站的服務(wù)器端發(fā)送請求獲取數(shù)據(jù)。
那如何使用A網(wǎng)站的服務(wù)器端向B網(wǎng)站的服務(wù)器端請求數(shù)據(jù)呢?這時我們需要用到node里面的一個第三方模塊request
① 引入該模塊
② 調(diào)用該模塊的函數(shù):第一個參數(shù)是其他服務(wù)器端的請求地址,第二個參數(shù)是一個回調(diào)函數(shù),當(dāng)這個請求返回數(shù)據(jù)的時候,這個回調(diào)函數(shù)就會被調(diào)用。
回調(diào)函數(shù)的第一個參數(shù)是error,如果發(fā)生了錯誤,則error就是一個對象類型,否則就是null。response是服務(wù)器端的響應(yīng)信息,body是響應(yīng)的主體內(nèi)容。
A網(wǎng)站的服務(wù)器端把B網(wǎng)站服務(wù)器響應(yīng)的數(shù)據(jù)返回給A網(wǎng)站的客戶端
<script>
// 獲取按鈕
var btn = document.getElementById('btn');
// 為按鈕添加點擊事件
btn.onclick = function() {
ajax({
type: 'get',
url: 'http://localhost:3000/server',
success: function (data) {
console.log(data);
}
});
}
</script>
app.get('/server', (req, res) => {
// A網(wǎng)站的服務(wù)器端把B網(wǎng)站服務(wù)器響應(yīng)的數(shù)據(jù)返回給A網(wǎng)站的客戶端
request('http://localhost:3001/cross', (err, response, body) => {
res.send(body);
});
});
跨域請求中攜帶cookie的問題:
什么是無狀態(tài)請求:服務(wù)器端不關(guān)系客戶端是誰,只關(guān)心請求,只要請求來了,服務(wù)器端就會對此做出響應(yīng),響應(yīng)完了這次溝通也就結(jié)束了。當(dāng)同一個客戶端向服務(wù)器端再次發(fā)送請求時,服務(wù)器端并不知道客戶端已經(jīng)來過一次了,這就是無狀態(tài)請求。客戶端與服務(wù)器端溝通無記憶功能。
這種特性在早期的網(wǎng)站應(yīng)用中是沒有問題的,因為早期的網(wǎng)站應(yīng)用中只是展示一些文字圖片之類的信息,用戶并不會與網(wǎng)站進行交互。比如現(xiàn)在很多電商網(wǎng)站,用戶必須要進行登錄才能購買商品,因為如果用戶不登錄,網(wǎng)站不知道是誰在購物,商品也不知道該郵寄到哪兒去。
cookie就是服務(wù)器端與客戶端身份識別的一種技術(shù)。
如何進行身份識別呢?
當(dāng)客戶端第一次訪問服務(wù)器端的時候,服務(wù)器端檢測到當(dāng)前這個客戶端我并不認(rèn)識,這時服務(wù)器端在對客戶端做出響應(yīng)的同時,還可以給客戶端發(fā)一個小卡片,這個小卡片可以理解為是服務(wù)器端發(fā)給客戶端的一個身份證,這個身份證就是cookie。當(dāng)客戶端再次發(fā)送請求的時候,這個身份證會隨著請求被自動發(fā)送到服務(wù)器端。服務(wù)器端拿到身份證之后就知道客戶端是誰。這樣就建立了服務(wù)器端與客戶端之間的持久聯(lián)系。
如果想實現(xiàn)跨域登錄功能,這時就需要用到cookie技術(shù),但是由于是跨域請求,cookie不會自動發(fā)送到服務(wù)器端,這樣就無法實現(xiàn)登錄功能了。
如何解決呢?
使用withCredentials屬性:在使用Ajax技術(shù)發(fā)送跨域請求時,默認(rèn)情況下不會在請求中攜帶cookie信息。
withCredentials:指定在涉及到跨域請求時,是否攜帶cookie信息,默認(rèn)值為false
Acss-Contnolollo-Credentias:tue表示允許客戶端發(fā)送請求時攜帶cookie
如果客戶端未攜帶cookie,服務(wù)器端不認(rèn)識,那么即使登錄成功后,用戶狀態(tài)還是處在未登錄狀態(tài)。
所以一定要設(shè)置withCredentials屬性
<div class="container">
<form id="loginForm">
<div class="form-group">
<label>用戶名</label>
<input type="text" name="username" class="form-control" placeholder="請輸入用戶名">
</div>
<div class="form-group">
<label>密碼</label>
<input type="password" name="password" class="form-control" placeholder="請輸入用密碼">
</div>
<input type="button" class="btn btn-default" value="登錄" id="loginBtn">
<input type="button" class="btn btn-default" value="檢測用戶登錄狀態(tài)" id="checkLogin">
</form>
</div>
<script type="text/javascript">
// 獲取登錄按鈕
var loginBtn = document.getElementById('loginBtn');
// 獲取檢測登錄狀態(tài)按鈕
var checkLogin = document.getElementById('checkLogin');
// 獲取登錄表單
var loginForm = document.getElementById('loginForm');
// 為登錄按鈕添加點擊事件
loginBtn.onclick = function () {
// 將html表單轉(zhuǎn)換為formData表單對象
var formData = new FormData(loginForm);
// 創(chuàng)建ajax對象
var xhr = new XMLHttpRequest();
// 對ajax對象進行配置
xhr.open('post', 'http://localhost:3001/login');
// 當(dāng)發(fā)送跨域請求時,攜帶cookie信息
xhr.withCredentials = true;
// 發(fā)送請求并傳遞請求參數(shù)
xhr.send(formData);
// 監(jiān)聽服務(wù)器端給予的響應(yīng)內(nèi)容
xhr.onload = function () {
console.log(xhr.responseText);
}
}
// 當(dāng)檢測用戶狀態(tài)按鈕被點擊時
checkLogin.onclick = function () {
// 創(chuàng)建ajax對象
var xhr = new XMLHttpRequest();
// 對ajax對象進行配置
xhr.open('get', 'http://localhost:3001/checkLogin');
// 當(dāng)發(fā)送跨域請求時,攜帶cookie信息
xhr.withCredentials = true;
// 發(fā)送請求并傳遞請求參數(shù)
xhr.send();
// 監(jiān)聽服務(wù)器端給予的響應(yīng)內(nèi)容
xhr.onload = function () {
console.log(xhr.responseText);
}
}
</script>
總結(jié)
- 上一篇: /etc/alternatives
- 下一篇: 【转】深入分析@Transactiona