一、什么是跨域?
指的是瀏覽器不能執(zhí)行其他網(wǎng)站的腳本,簡單來說是瀏覽器同源政策的限制,瀏覽器針對于ajax的限制。
同源政策
兩個頁面擁有相同的 協(xié)議,端口,域名 就是同源,如果有一個不相同就是不同源。
同源政策產(chǎn)生的目的
保護用戶信息安全,防止一些網(wǎng)站盜取用戶信息。
常見跨域場景
在這里插入圖片描述
二、跨域解決方案
1、通過jsonp跨域
2、跨域資源共享(CORS)
3、document.domain + iframe跨域
4、location.hash + iframe
5、window.name + iframe跨域
6、postMessage跨域
7、nginx代理跨域
8、nodejs中間件代理跨域(非vue、vue(webpack–proxy代理))
9、WebSocket協(xié)議跨域
方案一:jsonp (json with padding)
原理:(回調(diào)函數(shù))
1.動態(tài)創(chuàng)建一個script標簽(無跨域限制)
2.通過srcipt標簽里的src屬性 進行 跨域訪問 屬性里面寫 跨域的地址+ callback屬性作為函數(shù)傳遞給后端
3.服務端通過接收客戶端傳過來的callback屬性值對應的函數(shù)名
4.要發(fā)生過去的數(shù)據(jù) 通過函數(shù)的參數(shù)傳遞過去 (JSON格式)
5.將函數(shù)名對應調(diào)用的執(zhí)行代碼返回給客戶端(服務器端響應數(shù)據(jù)一定是函數(shù)代碼的調(diào)用,當瀏覽器對script將響應內(nèi)容加載完成后,會自動調(diào)用函數(shù))
6.并且在服務器端的執(zhí)行函數(shù)是字符串,字符串包裹著函數(shù)調(diào)用的代碼,如果沒有包裹,則就在服務端立即執(zhí)行。
缺點:只能發(fā)送get請求,易受到 XSS攻擊
客戶端代碼
// 1.封裝一個jsonp函數(shù);
jsonp({
// method: 'GET',// 所有的jsonp請求都是get請求,所以這個屬性可以不寫了
// data: , // 寫了以后太繁瑣,取消
url: 'http://www.localhost:3006/api/jsonp',
success: function (res) {
console.log(res)
}
})
// 封裝
function jsonp(obj) {
// 1.創(chuàng)建一個script標簽; 2.改變src 3.給函數(shù)起名字,定義為全局函數(shù);
var script = document.createElement("script"); // 不要用innerHTML, 他不會自動發(fā)送請求
// 3.給函數(shù)起名字,定義為全局函數(shù);
var fnName = "haha_123123";
// window.aaa就是把aaa設置為全局變量!
window[fnName] = obj.success;
// 2.改變src,添加到head中
script.src = obj.url + "?callback=" + fnName;
// 把script標簽添加到head標簽中,就會發(fā)送src的請求了
document.head.appendChild(script);
// 代碼執(zhí)行完畢,把script標簽刪除
script.onload = function () {
document.head.removeChild(script);
window[fnName] = undefined;
}
}
服務端代碼
app.get("/api/jsonp", (req, res) => {
// 獲取函數(shù)名,設置對象,發(fā)送給客戶端
const fnName = req.query.callback;
// 定義發(fā)送給客戶端的對象轉換為json字符串
var objStr = JSON.stringify({
name: '張三',
age: 18
});
// 字符串類型的執(zhí)行函數(shù)
res.send(`${fnName}(${objStr})`);
});
vue解決跨域問題- vue-jsonp
// 第一步:在vue項目中安裝上vue-jsonp
npm i vue-jsonp --save
// 第二步:在vue項目中main.js導入
import { VueJsonp } from 'vue-jsonp'
Vue.use(VueJsonp)
// 使用示例:
this.$jsonp(url, {
id: '1',
output: "jsonp"
}).then(res => {
console.log('res', res)
}).catch(err => {
console.log(err)
})
方案二:CORS (CROSS-Origin Resource Sharing 跨域資源共享)
原理:服務器設置
同源政策默認阻止跨域獲取資源,但是CORS給了web服務器權限,即服務器可以選擇,允許訪問他們的資源 。
CORS需要瀏覽器和服務器同時支持,才可以實現(xiàn)跨域請求,目前幾乎所有瀏覽器都支持CORS,IE則不能低于IE10。CORS的整個過程都由瀏覽器自動完成,前端無需做任何設置,跟平時發(fā)送ajax請求并無差異。so,實現(xiàn)CORS的關鍵在于服務器,只要服務器實現(xiàn)CORS接口,就可以實現(xiàn)跨域通信。
缺點:忽略 cookie,瀏覽器版本有一定要求
屬于跨源 AJAX 請求的根本解決方法。相比 JSONP 只能發(fā)GET請求,CORS 允許任何類型的請求。
分類:簡單請求和復雜請求
條件:
1、請求方式:HEAD、GET、POST
2、請求頭信息:
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type 對應的值是以下三個中的任意一個
application/x-www-form-urlencoded
multipart/form-data
text/plain
注意:同時滿足以上兩個條件時,則是簡單請求,否則為復雜請求
簡單請求和復雜請求的區(qū)別:
簡單請求:一次請求
復雜請求:兩次請求,在發(fā)送數(shù)據(jù)之前會先發(fā)第一次請求做‘預檢’,只有‘預檢’通過后才再發(fā)送一次請求用于數(shù)據(jù)傳輸。
關于預檢:
在發(fā)送真正的請求之前,會默認發(fā)送一個options請求,做預檢,預檢成功后才發(fā)送真正的請求。
- 請求方式:OPTIONS
- “預檢”其實做檢查,檢查如果通過則允許傳輸數(shù)據(jù),檢查不通過則不再發(fā)送真正想要發(fā)送的消息
- 如何“預檢”
=> 如果復雜請求是PUT等請求,則服務端需要設置允許某請求,否則“預檢”不通過
Access-Control-Request-Method
=> 如果復雜請求設置了請求頭,則服務端需要設置允許某請求頭,否則“預檢”不通過
Access-Control-Request-Headers
實現(xiàn):設置三個響應頭
1.Access-Control-Allow-Origin 字段 (指定了哪些域名或ip地址可以跨域 也可以寫* 代表允許所有地址;允許跨域訪問的域名:若有端口需寫全(協(xié)議+域名+端口),若沒有端口末尾不用加’/')
response.setHeater('Access-Control-Allow-Origin',' http://XXX.cn || * || ip地址 ');
2.CORS默認支持9個請求頭,如果在9個請求頭之外 要單獨設置,多個請求頭之間要用英文逗號隔開。
// 提示OPTIONS預檢時,后端需要設置的兩個常用自定義頭
response.setHeater('Access-Control-Allow-Headers','Content-Type,X-Requested-With');
3.默認情況下CORS僅支持客戶端發(fā)起的GET、POST、HEAD請求,如果希望客戶端發(fā)送PUT、DELETE請求需要在服務器端設置
response.setHeater('Access-Control-Allow-Method','PUT');
// 允許前端帶認證cookie:啟用此項后,上面的域名不能為'*',必須指定具體的域名,否則瀏覽器會提示
response.setHeader("Access-Control-Allow-Credentials", "true");
和跨域CORS有關的幾個請求頭和響應頭請求頭
請求頭:
Origin:當前請求源,和響應頭里的Access-Control-Allow-Origin 對標, 是否允許當前源訪問,Origin是不可修改的
Access-Control-Request-Headers:本次真實請求的額外請求頭,和響應頭里的Access-Control-Allow-Headers對標,是否允許真實請求的請求頭
Access-Control-Request-Method:本次真實請求的額外方法,和響應頭里的Access-Control-Allow-Methods對標,是否允許真實請求使用的請求方法
響應頭:
1.Access-Control-Allow-Credentials:
2.Access-Control-Allow-Headers
3.Access-Control-Allow-Methods
4.Access-Control-Allow-Origin
5.Access-Control-Expose-Headers:
在CORS中,默認的,只允許客戶端讀取下面六個響應頭(在axios響應對象的headers里能看到):
6. Access-Control-Max-Age:設置預檢請求的有效時長,就是服務器允許的請求方法和請求頭做個緩存。
跟據(jù)頭信息和請求方式,瀏覽器進行了優(yōu)化,對請求進行劃分,分為簡單請求和非簡單請求
簡單請求和非簡單請求不做概述
簡單請求 : 滿足瀏覽器請求會直接發(fā)送 并在請求頭中攜帶Origin 表示本次來自哪個源
如果不滿足簡單請求 , 會在發(fā)送請求之前發(fā)送一個預檢請求 大小為0kb 請求方式為options 收到服務器響應后,分析它是否支持跨域,如果支持跨域則直接發(fā)送,如果不支持 報錯!
JSONP和CORS的區(qū)別:
?JSONP:服務端不用修改,需要改前端。發(fā)jsonp請求?
?JSONP:只能發(fā)GET請求?
?CORS:前端的代碼不用修改,服務端的代碼需要修改。如果是簡單請求的話在服務端加上一個響應頭。?
?CORS:可以發(fā)任意請求
方案三:document.domain + iframe跨域
此方案僅限主域相同,子域不同的跨域應用場景。
通過 iframe 是瀏覽器非同源標簽,加載內(nèi)容中轉,傳到當前頁面的屬性中。
實現(xiàn)原理:兩個頁面都通過js強制設置document.domain為基礎主域,就實現(xiàn)了同域。
缺點:同一一級域名;相同協(xié)議;相同端口
1.)父窗口:(http://www.domain.com/a.html)
<iframe id="iframe" src="http://child.domain.com/b.html"></iframe>
<script>
document.domain = 'domain.com';
var user = 'admin';
</script>
2.)子窗口:(http://child.domain.com/b.html)
<script>
document.domain = 'domain.com';
// 獲取父窗口中變量
alert('get js data from parent ---> ' + window.parent.user);
</script>
方案四: location.hash + iframe跨域
實現(xiàn)原理: a欲與b跨域相互通信,通過中間頁c來實現(xiàn)。 三個頁面,不同域之間利用iframe的location.hash傳值,相同域之間直接js訪問來通信。
具體實現(xiàn):A域:a.html -> B域:b.html -> A域:c.html,a與b不同域只能通過hash值單向通信,b與c也不同域也只能單向通信,但c與a同域,所以c可通過parent.parent訪問a頁面所有對象。
<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>
var iframe = document.getElementById('iframe');
// 向b.html傳hash值
setTimeout(function() {
iframe.src = iframe.src + '#user=admin';
}, 1000);
// 開放給同域c.html的回調(diào)方法
function onCallback(res) {
alert('data from c.html ---> ' + res);
}
</script>
2.)b.html:(http://www.domain2.com/b.html)
<iframe id="iframe" src="http://www.domain1.com/c.html" style="display:none;"></iframe>
<script>
var iframe = document.getElementById('iframe');
// 監(jiān)聽a.html傳來的hash值,再傳給c.html
window.onhashchange = function () {
iframe.src = iframe.src + location.hash;
};
</script>
3.)c.html:(http://www.domain1.com/c.html)
<script>
// 監(jiān)聽b.html傳來的hash值
window.onhashchange = function () {
// 再通過操作同域a.html的js回調(diào),將結果傳回
window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', ''));
};
</script>
方案五:window.name + iframe跨域
window.name屬性的獨特之處:name值在不同的頁面(甚至不同域名)加載后依舊存在,并且可以支持非常長的 name 值(2MB)。
缺點:頁面的屬性值有大小限制
1.)a.html:(http://www.domain1.com/a.html)
var proxy = function(url, callback) {
var state = 0;
var iframe = document.createElement('iframe');
// 加載跨域頁面
iframe.src = url;
// onload事件會觸發(fā)2次,第1次加載跨域頁,并留存數(shù)據(jù)于window.name
iframe.onload = function() {
if (state === 1) {
// 第2次onload(同域proxy頁)成功后,讀取同域window.name中數(shù)據(jù)
callback(iframe.contentWindow.name);
destoryFrame();
} else if (state === 0) {
// 第1次onload(跨域頁)成功后,切換到同域代理頁面
iframe.contentWindow.location = 'http://www.domain1.com/proxy.html';
state = 1;
}
};
document.body.appendChild(iframe);
// 獲取數(shù)據(jù)以后銷毀這個iframe,釋放內(nèi)存;這也保證了安全(不被其他域frame js訪問)
function destoryFrame() {
iframe.contentWindow.document.write('');
iframe.contentWindow.close();
document.body.removeChild(iframe);
}
}
// 請求跨域b頁面數(shù)據(jù)
proxy('http://www.domain2.com/b.html', function(data){
alert(data);
});
2.)proxy.html:(http://www.domain1.com/proxy…
中間代理頁,與a.html同域,內(nèi)容為空即可。
3.)b.html:(http://www.domain2.com/b.html)
<script> window.name = 'This is domain2 data!'; </script>
總結:通過iframe的src屬性由外域轉向本地域,跨域數(shù)據(jù)即由iframe的window.name從外域傳遞到本地域。這個就巧妙地繞過了瀏覽器的跨域訪問限制,但同時它又是安全操作。
方案六:postMessage跨域
postMessage是HTML5 XMLHttpRequest Level 2中的 API,且是為數(shù)不多可以跨域操作的window屬性之一,它可用于解決以下方面的問題:
a.) 頁面和其打開的新窗口的數(shù)據(jù)傳遞
b.) 多窗口之間消息傳遞
c.) 頁面與嵌套的iframe消息傳遞
d.) 上面三個場景的跨域數(shù)據(jù)傳遞
用法:postMessage(data,origin)方法接受兩個參數(shù)
data: html5規(guī)范支持任意基本類型或可復制的對象,但部分瀏覽器只支持字符串,所以傳參時最好用JSON.stringify()序列化。
origin: 協(xié)議+主機+端口號,也可以設置為"*“,表示可以傳遞給任意窗口,如果要指定和當前窗口同源的話設置為”/"。
缺點:瀏覽器版本要求,部分瀏覽器要配置放開跨域限制
1.)a.html:(http://www.domain1.com/a.html)
<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>
var iframe = document.getElementById('iframe');
iframe.onload = function() {
var data = {
name: 'aym'
};
// 向domain2傳送跨域數(shù)據(jù)
iframe.contentWindow.postMessage(JSON.stringify(data), 'http://www.domain2.com');
// 接受domain2返回數(shù)據(jù)
window.addEventListener('message', function(e) {
alert('data from domain2 ---> ' + e.data);
}, false);
</script>
2.)b.html:(http://www.domain2.com/b.html)
<script>
// 接收domain1的數(shù)據(jù)
window.addEventListener('message', function(e) {
alert('data from domain1 ---> ' + e.data);
var data = JSON.parse(e.data);
if (data) {
data.number = 16;
// 處理后再發(fā)回domain1
window.parent.postMessage(JSON.stringify(data), 'http://www.domain1.com');
}
}, false);
</script>
方案七:nginx代理跨域
前端向發(fā)送請求,經(jīng)過代理,請求需要的服務器資源
缺點:需要額外的代理服務器
1、nginx配置解決iconfont跨域
瀏覽器跨域訪問js、css、img等常規(guī)靜態(tài)資源被同源策略許可,但iconfont字體文件(eot|otf|ttf|woff|svg)例外,此時可在nginx的靜態(tài)資源服務器中加入以下配置。
location / {
add_header Access-Control-Allow-Origin *;
}
2、nginx反向代理接口跨域
跨域原理: 同源策略是瀏覽器的安全策略,不是HTTP協(xié)議的一部分。服務器端調(diào)用HTTP接口只是使用HTTP協(xié)議,不會執(zhí)行JS腳本,不需要同源策略,也就不存在跨越問題。
實現(xiàn)思路:通過nginx配置一個代理服務器(域名與domain1相同,端口不同)做跳板機,反向代理訪問domain2接口,并且可以順便修改cookie中domain信息,方便當前域cookie寫入,實現(xiàn)跨域登錄。
nginx具體配置:
#proxy服務器
server {
listen 81;
server_name www.domain1.com;
location / {
proxy_pass http://www.domain2.com:8080; #反向代理
proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
index index.html index.htm;
# 當用webpack-dev-server等中間件代理接口訪問nignx時,此時無瀏覽器參與,故沒有同源限制,下面的跨域配置可不啟用
add_header Access-Control-Allow-Origin http://www.domain1.com; #當前端只跨域不帶cookie時,可為*
add_header Access-Control-Allow-Credentials true;
}
}
方案八:WebSocket協(xié)議跨域
WebSocket protocol是HTML5一種新的協(xié)議(WebSocket協(xié)議本質上是一個基于tcp的協(xié)議)。
它實現(xiàn)了瀏覽器與服務器全雙工通信,能更好的節(jié)省服務器資源和帶寬并達到實時通訊的目的,同時允許跨域通訊,是server push技術的一種很好的實現(xiàn)。
Websocket是一個持久化的協(xié)議
WebSocket的原理
WebSocket約定了一個通信的規(guī)范,通過一個握手的機制,客戶端和服務器之間能建立一個類似tcp的連接,從而方便它們之間的通信
在WebSocket出現(xiàn)之前,web交互一般是基于http協(xié)議的短連接或者長連接
WebSocket是一種全新的協(xié)議,不屬于http無狀態(tài)協(xié)議,協(xié)議名為"ws"
WebSocket與http的關系
相同點:
都是基于tcp的,都是可靠性傳輸協(xié)議
都是應用層協(xié)議
不同點:
WebSocket是雙向通信協(xié)議,模擬Socket協(xié)議,可以雙向發(fā)送或接受信息
HTTP是單向的
WebSocket是需要瀏覽器和服務器握手進行建立連接的
而http是瀏覽器發(fā)起向服務器的連接,服務器預先并不知道這個連接
聯(lián)系:
WebSocket在建立握手時,數(shù)據(jù)是通過HTTP傳輸?shù)?。但是建立之后,在真正傳輸時候是不需要HTTP協(xié)議的
總結(總體過程):
首先,客戶端發(fā)起http請求,經(jīng)過3次握手后,建立起TCP連接;http請求里存放WebSocket支持的版本號等信息,如:Upgrade、Connection、WebSocket-Version等;
然后,服務器收到客戶端的握手請求后,同樣采用HTTP協(xié)議回饋數(shù)據(jù);
最后,客戶端收到連接成功的消息后,開始借助于TCP傳輸信道進行全雙工通信。
WebSocket有以下特點:
是真正的全雙工方式,建立連接后客戶端與服務器端是完全平等的,可以互相主動請求。而HTTP長連接基于HTTP,是傳統(tǒng)的客戶端對服務器發(fā)起請求的模式。
HTTP長連接中,每次數(shù)據(jù)交換除了真正的數(shù)據(jù)部分外,服務器和客戶端還要大量交換HTTP header,信息交換效率很低。
Websocket協(xié)議通過第一個request建立了TCP連接之后,之后交換的數(shù)據(jù)都不需要發(fā)送 HTTP header就能交換數(shù)據(jù),這顯然和原有的HTTP協(xié)議有區(qū)別所以它需要對服務器和客戶端都進行升級才能實現(xiàn)(主流瀏覽器都已支持HTML5)
原生WebSocket API 使用起來不太方便,我們使用Socket.io,它很好地封裝了webSocket接口,提供了更簡單、靈活的接口,也對不支持webSocket的瀏覽器提供了向下兼容。
缺點:瀏覽器一定版本要求,服務器需要支持 websocket 協(xié)議
1.)前端代碼:文章來源:http://www.zghlxwxcb.cn/news/detail-684098.html
<script src="https://cdn.bootcss.com/socket.io/2.2.0/socket.io.js"></script>
<script>
// var socket = io('http://www.domain2.com:8080');
// 創(chuàng)建Socket.IO實例,建立連接
var socket= new io.Socket('localhost',{
port: 8080
});
socket.connect();
// 連接成功處理
socket.on('connect', function() {
console.log('Client has connected to the server!');
});
// 監(jiān)聽服務端消息
socket.on('message', function(msg) {
console.log('data from server: ---> ' + msg);
});
// 監(jiān)聽服務端關閉
socket.on('disconnect', function() {
console.log('Server socket has closed.');
});
document.getElementsByTagName('input')[0].onblur = function() {
// 通過Socket發(fā)送一條消息到服務器
socket.send(this.value);
};
</script>
2.)Nodejs socket后臺:文章來源地址http://www.zghlxwxcb.cn/news/detail-684098.html
var http = require('http');
var socket = require('socket.io');
// 啟http服務
var server = http.createServer(function(req, res) {
res.writeHead(200, {
'Content-type': 'text/html'
});
res.end();
});
server.listen('8080');
console.log('Server is running at port 8080...');
// 監(jiān)聽socket連接
socket.listen(server).on('connection', function(client) {
// 接收信息
client.on('message', function(msg) {
client.send('hello:' + msg);
console.log('data from client: ---> ' + msg);
});
// 斷開處理
client.on('disconnect', function() {
console.log('Client socket has closed.');
});
到了這里,關于js跨域的解決方案的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!