跨域是我們在項目中經常遇到的,前後端數據交互經常碰到請求跨域,首先我們來想一下為什麼會有跨域這個詞的出現?本文帶你來探討一下以下幾個問題:
- 跨域是什麼?
- 為什麼要跨域?
- 跨域的幾種方式?
- ...
什麼是跨域?
跨域是指的瀏覽器不能執行其它網站的腳本,它是由瀏覽器的同源策略造成,是瀏覽器對JavaScript實施的安全限制。
跨域實際上指從一個域的網頁去請求另一個域的資源,比如:從 http://www.baidu.com 網站去請求http://www.google.com 網站的資源。
什麼是同源策略?
同源策略 指的是 域名,協議,端口 三者都相同~
什麼是同源?
要知道URL由協議,域名,端口以及路徑組成,若兩個URL的協議、域名和端口相同,則表示他們同源。
相反,只要協議,域名,端口有任何一個的不同,就被當作是跨域。
限制同源策略內容
- Cookie、LocalStorage、IndexedDB等存儲性內容
- DOM節點
- Ajax請求發送後,結果被瀏覽器攔截了
允許跨域加載資源
這下邊三個含有 src 標籤的是允許跨域加載資源的
<img src=XXX>
<link href=XXX>
<script src=XXX>
跨域的場景
九種跨域解決方案
- jsonp
- cors
- postMessage
- document.domain
- window.name
- location.hash
- https-proxy
- nginx
- websocket
jsonp
什麼是jsonp
jsonp全稱是JSON with Padding,是為了解決跨域請求資源而產生的解決方案,是一種依賴開發人員創造出的一種非官方跨域數據交互協議。
Jsonp的原理
- 利用script標籤的src屬性來實現跨域
- 通過將前端方法作為參數傳遞到服務器端,然後由服務器端注入參數之後再返回,實現服務器端向客户端通信
- 由於使用script標籤的src屬性,因此只支持get方法
Jsonp和Ajax對比
- Jsonp和Ajax相同,都是客户端向服務器端發送請求,從服務器端獲取數據的方式
- Ajax屬於同源策略
- Jsonp屬於非同源策略(跨域請求)
Jsonp的優缺點
優點:
- 它不像XMLHttpRequest對象實現的Ajax請求那樣受到同源策略的限制,JSONP可以跨越同源策略
- 它的兼容性更好,在更加古老的瀏覽器中都可以運行,不需要XMLHttpRequest或ActiveX的支持
- 在請求完畢後可以通過調用callback的方式回傳結果
缺點:
- 它只支持GET請求而不支持POST等其它類型的HTTP請求
- 它只支持跨域HTTP請求這種情況,不能解決不同域的兩個頁面之間如何進行JavaScript調用的問題
- jsonp在調用失敗的時候不會返回各種HTTP狀態碼
- 缺點是安全性,萬一假如提供jsonp的服務存在頁面注入漏洞,即它返回的javascript的內容被人控制的
Jsonp的實現流程
- 聲明一個回調函數,把函數名(show)當做參數值
- 要傳遞給跨域請求的數據的服務器,函數形參為要獲取目標數據
- 創建一個script標籤,把那個跨域的API數據接口地址,賦值給script的src,還要在這個地址中向服務器傳遞該函數名
- 服務器接收到請求後,需要進行處理:把傳遞的參數名和它需要的數據拼接成一個字符串
- 最後服務器把準備的數據通過HTTP協議返回給客户端,客户端再調用執行之前聲明的回調函數(show),對返回的數據進行操作
具體代碼實現如下:
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
function jsonp({url,params,cb}){
return new Promise((resolve,reject)=>{
let script = document.createElement('script');
window[cb]=function(data){
resolve(data);
document.body.removeChild(script);
}
params={...params,cb}//wd=b&cb=show
let arrs = [];
for(let key in params){
arrs.push(`${key}=${params[key]}`);
}
script.src = `${url}?${arrs.join('&')}`;
document.body.appendChild(script)
})
}
//只能發送get請求,不支持post put delete
//不安全xss攻擊 不採用
jsonp({
url:'http://localhost:3000/say',
params:{wd:'早上好'},
cb:'show'
}).then(data=>{
console.log(data)
})
</script>
</body>
</html>
serve.js
let express = require('express');
let app = express();
app.get('/say',function (req,res){
let {wd,cb} = req.query;
console.log(wd);
res.end(`${cb}('晚上好')`)
})
app.listen(3000)
注意: 需要安裝npm install express, 然後在終端裏面輸入node serve.js, 再把index.html在瀏覽器上邊console欄查看返回結果
JQuery的jsonp跨域請求
如果從 192.168.19.1發ajax請求到 192.168.19.6 會產生跨域問題, 利用jquery的jsonp參數可輕鬆這個問題。
注意:Jsonp都是GET和異步請求
function get() {
$.ajax({
type: "GET",
url: 'http://192.168.19.6:8080/jsgd/bill.jsp?userCode=?&date='+ new Date(),
dataType:"jsonp",
jsonp:"jsonpcallback",
success: function(msg){
$('#callcenter').html(msg.text);
}
});
}
cors
什麼是cors
cors全稱"跨域資源共享"(Cross-origin resource sharing), 是一種ajax跨域請求資源的方式。
兼容性
- cors需要瀏覽器和服務器同時支持,才可以實現跨域的請求
- 這個方法幾乎所有的瀏覽器都支持,但是ie必須是10以上
- ie8和9需要通過XDomainRequest來實現
請求類型
cors分為簡單請求和複雜請求兩類
簡單請求
請求方式使用下列方法之一:
GET
HEAD
POST
Content-Type的值僅限於下列三者之一:
text/plain
multipart/form-data
application/x-www-form-urlencoded
注意:對於簡單的請求,瀏覽器會直接發送cors請求,具體來説就是在header中加入origin請求頭字段。在響應頭回服務器設置相關的cors請求,響應頭字段為允許跨域請求的源。請求時瀏覽器在請求頭的Origin中説明請求的源,服務器收到後發現允許該源跨域請求,則會成功返回。
複雜請求
使用了下面任一HTTP方法
PUT
DELETE
CONNECT
OPTIONS
TRACE
PATCH
Content-Type的值不屬於下列之一:
application/x-www-form-urlencoded
multipart/form-data
text/plain
當符合複雜請求的條件時,瀏覽器會自動先發送一個options請求。如果發現瀏覽器支持該請求,則會將真正的請求發送到後端。如果瀏覽器發現服務端不支持該請求,則會在控制枱拋出錯誤。
cors字段介紹
- Access-Control-Allow-Methods
這個字段是必要的,它的值是逗號分割的一個字符串,表明服務器支持的所有跨域請求的方式
-
Access-Control-Allow-Headers
如果瀏覽器請求包括這個字段,則這個字段也是必須的,它也是一個逗號分隔的字符串,表明服務器支持的所有頭信息字段,不限於瀏覽器在“預檢"中請求的字段
- Access-Control-Allow-Credentials
這個字段與簡單請求時的含義相同
- Access-Control-Allow-Credentials
- Access-Control-Max-Age
該字段可選,用來指定本次預檢請求的有效期,單位為秒。上面結果中,有效期是20天(1728000秒),即允許緩存該條迴應1728000秒(即20天),在此期間,不用發出另一條預檢請求
流程實現
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
hello
</body>
</html>
serve.js
let express = require('express');
let app = express();
app.use(express.static(__dirname));
app.listen(3000)
以當前這個作為靜態文件目錄,先要在終端裏面node serve.js服務器打開,訪問localhost:3000就可以把 hello 顯示出來。
這是一個完整的複雜請求例子:
index.js
let xhr = new XMLHttpRequest()
document.cookie = 'name=xiaoming'
xhr.withCredentials = true
xhr.open('PUT', 'http://localhost:4000/getData', true)
xhr.setRequestHeader('name', 'xiaoming')
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
console.log(xhr.response)
//得到響應頭,後台需設置Access-Control-Expose-Headers
console.log(xhr.getResponseHeader('name'))
}
}
}
xhr.send()
serve.js
let express = require('express');
let app = express();
app.use(express.static(__dirname));
app.listen(3000)
serve2.js
let express = require('express')
let app = express()
let whitList = ['http://localhost:3000']
app.use(function(req, res, next) {
let origin = req.headers.origin
if (whitList.includes(origin)) {
// 設置哪個源可以訪問我
res.setHeader('Access-Control-Allow-Origin', origin)
// 允許攜帶哪個頭訪問我
res.setHeader('Access-Control-Allow-Headers', 'name')
// 允許哪個方法訪問我
res.setHeader('Access-Control-Allow-Methods', 'PUT')
// 允許攜帶cookie
res.setHeader('Access-Control-Allow-Credentials', true)
// 預檢的存活時間
res.setHeader('Access-Control-Max-Age', 6)
// 允許返回的頭
res.setHeader('Access-Control-Expose-Headers', 'name')
if (req.method === 'OPTIONS') {
res.end() // OPTIONS請求不做任何處理
}
}
next()
})
app.put('/getData', function(req, res) {
console.log(req.headers)
res.setHeader('name', 'ming') //返回一個響應頭,後台需設置
res.end('早上好')
})
app.get('/getData', function(req, res) {
console.log(req.headers)
res.end('早上好')
})
app.use(express.static(__dirname))
app.listen(4000)
Cors與Jsonp比較
- cors比Jsonp更強大
- Jsonp只支持Get請求,cors支持所有類型的HTTP請求
- 使用cors,可以使用XMLHttpRequest發起請求和獲取數據,比Jsonp有更好的錯誤處理
- Jsonp的優勢在於支持老式瀏覽器和可以向cors的網絡請求數據
- cors與Jsonp相比,更方便可靠
postMessage
什麼是postMessage
postMessage方法允許來自不同源的腳本採用異步方式進行有限的通信,可以實現跨文本檔、多窗口、跨域消息傳遞。
postMessage語法
otherWindow.postMessage(message, targetOrigin, [transfer])
- otherWindow:其它窗口(目標窗口)的引用,比如iframe的contentWindow屬性、執行window.open返回的窗口對象、或者是命名過或數值索引的window.frames
- message:將要發送到其他window的數據,這個數據會自動被序列化,數據格式也不受限制(字符串,對象都可以)
- targetOrigin:目標窗口的源,可以是字符串*表示無限制,或URL,需要協議端口號和主機都匹配才會發送
- transfer(可選):是一串和message同時傳遞的Tranferable對象,這些對象的所有權將 被轉移給消息接收方,而發送一方將不再保有所有權
兼容性
高級瀏覽器Internet Explorer 8+, chrome,Firefox , Opera 和 Safari 都將支持這個功能
流程實現
a.html 向 b.html傳遞 "早上好",然後 a.html 傳回"今天天氣真好"
a.html
<iframe src="http://localhost:4000/b.html" frameborder="0" id="frame" onload="load()"></iframe>
<script>
function load(){
let frame = document.getElementById('frame');
frame.contentWindow.postMessage('早上好','http://localhost:4000');
window.onmessage=function(e){
console.log(e.data)
}
}
</script>
b.html
<script>
window.onmessage = function(e){
console.log(e.data);
e.source.postMessage('今天天氣不錯',e.origin)
}
</script>
a.js
let express = require('express')
let app = express();
app.use(express.static(__dirname));
app.listen(3000)
b.js
let express = require('express')
let app = express();
app.use(express.static(__dirname));
app.listen(4000)
Window.name
什麼是Window.name
window.name 是一個 window 對象的內置屬性,name 屬性可以設置或返回存放窗口的名稱的一個字符串。
該屬性的特徵
在頁面在瀏覽器端展示的時候,我們總能在控制枱拿到一個全局變量window,該變量有一個name屬性,有以下的特徵:
- 每個窗口都有獨立的window.name與之對應
- 在一個窗口的生命週期中(被關閉前),窗口載入的所有頁面同時共享一個window.name,每個頁面window.name都有讀寫的權限
- window.name一直存在與當前窗口,即使是新的頁面載入也不會改變window.name的值
- window.name可以存儲不超過2M的數據,數據個數按需自定義
流程實現
- a.html和b.html是同域
http://localhost:3000 - c.html是獨立的
http://localhost:4000 - a獲取c的數據
- a先引用c
- c把值放到
window.name,把a引用的地址改為b
a.html
<iframe src="http://localhost:4000/c.html" frameborder="0" onload="load()" id="iframe"></iframe>
<script>
let first = true
function load(){
if(first){
let iframe = document.getElementById('iframe');
iframe.src="http://localhost:3000/b.html";
first = false;
}else{
console.log(iframe.contentWindow.name)
}
}
<script>
b.html
<body>
早上好
</body>
c.html
<script>
window.name='今天天氣不錯'
</script>
a.js
let express = require('express')
let app = express();
app.use(express.static(__dirname));
app.listen(3000)
b.js
let express = require('express')
let app = express();
app.use(express.static(__dirname));
app.listen(4000)
location.hash
什麼是location.hash
location是javascript裏面的內置對象,如location.href就管理頁面的url,用loaction.href=url就可以直接將頁面重定向url,而location.hash則可以用來獲取或設置頁面的標籤值,hash屬性是一個可讀可寫的字符串,該字符串是URL的錨部分(從#號開始的部分)
location.hash的簡單應用
#的含義
#代表網頁中的位置,其右邊的字符,就是該位置的標識符,例如:
http://www.juejin.com/index.html#drafts
就是代表index.html的drafts位置,瀏覽器讀取這個URL後,會自動將print位置滾動至可視區域
HTTP請求不包括#
#是用來指導瀏覽器的動作的,對服務器端完全無用,所以,HTTP請求中不包括#
例如:
http://www.juejin.com/index.html#drafts
瀏覽器實際發出的請求是這樣的:
GET/index.html HTTP/1.1
Host:www.juejin.com
可以看到,只是請求的index.html,沒有#drafts部分
#後的字符
在第一個#出現的任何字符,都會被瀏覽器解讀為位置標識符,這意味着,這些字符不會被髮送到服務器端
改變#不觸發網頁重構
單單改變#後的部分,瀏覽器只會滾動到相應的位置,不會重新加載網頁
改變#會改變瀏覽器的訪問歷史
每一次改變#後的部分,都會在瀏覽器的訪問歷史中增加一個記錄,使用"後退"按鈕,就可以回到上一個位置
讀取#值
window.location.hash這個屬性可讀可寫。讀取時,可以用來判斷網頁狀態是否改變;寫入時,則會在不重載網頁的前提下,創造一條訪問歷史記錄
onhashchange事件
當#值發生變化時,就會觸發這個事件。IE8+、Firefox 3.6+、Chrome 5+、Safari 4.0+支持該事件
Google抓取#的機制
默認情況下,Google的網絡忽視URL的#部分
流程實現
路徑後面的hash值可以用來通信。目的:a.html 想訪問 c.html跨域相互通信。
- a.html給c.html傳一個hash值,需要通過中間的b.html來實現
- c.html收到hash值後 c.html把hash值傳遞給b.html
- b.html將結果放到a.html的hash值中
a.html
<iframe src="http://localhost:4000/c.html#goodmorning"></iframe>
<script>
window.onhashchange = function () {
console.log(location.hash);
}
</script>
b.html
<script>
window.parent.parent.location.hash = location.hash
</script>
c.html
<script>
console.log(location.hash);
let iframe = document.createElement('iframe');
iframe.src = 'http://localhost:3000/b.html#goodevening';
document.body.appendChild(iframe);
</script>
a.js
let express = require('express')
let app = express();
app.use(express.static(__dirname));
app.listen(3000)
b.js
let express = require('express')
let app = express();
app.use(express.static(__dirname));
app.listen(4000)
domain
什麼是domain
主要用於主域相同的域之間的數據通信,注意 僅限主域相同,子域不同的跨域應用場景。
實現的原理:兩個頁面都通過js強制設置 document.domain 為基礎主域,就實現了同域
説明
這個方法只能用於二級域名相同的情況下,比如:
www.baidu.com
hhh.baidu.com
這就適用於domain方法
流程實現
a.html
<iframe src="http://b.ming.cn:3000/b.html" frameborder="0" onload="load()" id="frame"></iframe>
<script>
document.domain = 'ming.cn'
function load() {
console.log(frame.contentWindow.a);
}
</script>
b.html
<div>
早上好啊
</div>
<script type="text/javascript">
document.domain = 'ming.cn'
var a = 99999;
</script>
a.js
let express = require('express')
let app = express();
app.use(express.static(__dirname));
app.listen(3000, () => {
console.log('server run at 3000')
})
b.js
let express = require('express')
let app = express();
app.use(express.static(__dirname));
app.listen(4000, () => {
console.log('server run at 4000')
})
這個就可以通過http://a.ming.cn:3000/a.html獲取到頁面http://a.ming.cn:3000/b.htm中的a的值99999
注意:這裏我把我電腦上邊的hosts修改了一下,不然不能出來效果
WebSocket
什麼是WebSocket
WebSocket是一種網絡通信協議,它實現了瀏覽器與服務器全雙工通信,同時允許跨域通訊。原生WebSocket API使用起來不太方便,我們使用Socket.io,它很好地封裝了webSocket接口,提供了更簡單、靈活的接口,也對不支持webSocket的瀏覽器提供了向下兼容。
WebSocket如何工作
Web瀏覽器和服務器都必須實現 WebSockets 協議來建立和維護連接。由於 WebSockets 連接長期存在,與典型的HTTP連接不同,對服務器有重要的影響。
注意:基於多線程或多進程的服務器無法適用於WebSockets,因為它旨在打開連接,儘可能快地處理請求,然後關閉連接。任何實際的WebSockets服務器端實現都需要一個異步服務器。
流程實現
a.html
<script>
//高級api 不兼容 socket.io(一般使用它)
let socket = new WebSocket('ws://localhost:3000');
socket.onopen=function(){
socket.send('早上好啊')
}
socket.onmessage = function(e){
console.log(e.data);
}
</script>
a.js
let express = require('express')
let app = express();
let WebSocket = require('ws')
let wss = new WebSocket.Server({port:3000})
wss.on('connection',function(ws){
ws.on('message',function(data){
console.log(data)
ws.send('今天天氣真好')
})
})
總結
以上就是整理的一些跨域的方法,我覺得一般用cors,jsonp等常見的方法就可以了,不過遇到了一些特殊情況,我們也要做到有很多方法是可以選擇的,相信這篇文字會對大家有幫助!
歡迎大家加入❤️❤️❤️
參考文章
珠峯架構課