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