什么是跨域
跨域是指一個域下的文檔或腳本試圖去請求另一個域下的資源,這里跨域是廣義的。
廣義的跨域:
- 資源跳轉:A鏈接、重定向、表單提交
- 資源嵌入:
<link>、<script>、<img>、<frame>等dom標簽,還有樣式中background:url()、@font-face()等文件外鏈
- 腳本請求:js發起的ajax請求、dom和js對象的跨域操作等
其實我們通常所說的跨域是狹義的,是由瀏覽器同源策略限制的一類請求場景。
一些跨域場景
還是上文的例子,例如:http://store.company.com/dir/page.html 請求以下地址的資源
URL | 結果 | 原因 |
---|---|---|
https://store.company.com/secure.html | 失敗 | 不同協議 ( https 和 http ) |
http://store.company.com:81/dir/etc.html | 失敗 | 不同端口 ( http:// 80是默認的) |
http://news.company.com/dir/other.html | 失敗 | 不同域名 ( news 和 store ) |
失敗的原因就是瀏覽器同源策略的限制,也就是所說的狹義的跨域
特別說明
第一:如果是協議和端口造成的跨域問題“前台”是無能為力的。
第二:在跨域問題上,僅僅是通過“URL的首部”來識別而不會根據域名對應的IP地址是否相同來判斷。“URL的首部”可以理解為“協議, 域名和端口必須匹配”。
這里你或許有個疑問:請求跨域了,那么請求到底發出去沒有?
跨域並不是請求發不出去,請求能發出去,服務端能收到請求並正常返回結果,只是結果被瀏覽器攔截了。你可能會疑問明明通過表單的方式可以發起跨域請求,為什么 Ajax 就不會?因為歸根結底,跨域是為了阻止用戶讀取到另一個域名下的內容,Ajax 可以獲取響應,瀏覽器認為這不安全,所以攔截了響應。但是表單並不會獲取新的內容,所以可以發起跨域請求。同時也說明了跨域並不能完全阻止 CSRF,因為請求畢竟是發出去了。
這里引自浪里行舟
跨域解決方案
- 通過jsonp跨域
- 跨域資源共享(CORS)
- nginx代理跨域
- nodejs中間件代理跨域
- WebSocket協議跨域
JSONP
- JSONP(JSON with Padding) 是 json 的一種"使用模式",
- 是應用JSON的一種新方法,是一種跨域解決方案
- 可以讓網頁從別的域名(網站)那獲取資料,即跨域讀取數據。
JSONP 由兩部分組成:回調函數和數據。
- 回調函數是當響應到來時應該在頁面中調用的函數,
- 而數據就是傳入回調函數中的json數據
JSONP原理
<script>
帶有 src 屬性可以跨域訪問,網頁可以得到從其他來源動態產生的 JSON 數據。JSONP請求需要請求資源所在服務器配合。
JSONP 優缺點
- 優點是簡單兼容性好,可用於解決主流瀏覽器的跨域數據訪問的問題。
- 缺點是僅支持get方法具有局限性,不安全可能會遭受XSS攻擊。
一個簡單的例子
當訪問http://localhost:3000/11-jsonp.html
可以拿到https://www.runoob.com/try/ajax/jsonp.php?jsoncallback=callbackFunction
提供的數據,並進行處理。
備注:
- https://www.runoob.com/try/ajax/jsonp.php?jsoncallback=callbackFunction訪問得到的數據是
callbackFunction(["customername1","customername2"])
准備工作
- 在本地建立一個 jsonp 文件夾
- 新建 11-jsonp.html
- 新建 100-server1.js
100-server1.js
let express = require('express');
let app = express();
app.use(express.static(__dirname));
app.listen(3000);
11-jsonp.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<script type="text/javascript">
var script = document.createElement('script');
script.type = 'text/javascript';
// 傳參一個回調函數名給后端,方便后端返回時執行這個在前端定義的回調函數
script.src = 'https://www.runoob.com/try/ajax/jsonp.php?jsoncallback=callbackFunction';
document.head.appendChild(script);
// 回調執行函數
const callbackFunction = function(data) {
console.log(data)
}
</script>
</body>
</html>
運行:
- 在 jsonp 目錄下,git bash
- 如果沒有安裝 express,首先
npm install express
- node 100-server1.js
- 在瀏覽器中輸入
http://localhost:3000/11-jsonp.html
- 打開瀏覽器的控制台,可以看到輸出
["customername1", "customername2"]
如圖
解釋:
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = 'https://www.runoob.com/try/ajax/jsonp.php?jsoncallback=callbackFunction';
document.head.appendChild(script);
以上這段代碼相當於在 <head> 標簽內增加
<script type="text/javascript" src="https://www.runoob.com/try/ajax/jsonp.php?jsoncallback=callbackFunction"></script>
如圖
<script>
帶有 src 屬性可以跨域訪問,所以可以拿到 src 屬性值所指的地址,拿到數據,並進行處理callbackFunction
可以自定義,需要創建與之相同的處理函數jsoncallback
是與后端商量好的接口
進階1-封裝 jsonp 函數
新建 12-jsonp.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<script type="text/javascript">
// https://www.runoob.com/try/ajax/jsonp.php?jsoncallback=callbackFunction
function jsonp({
url,
jsoncallback
}) {
return new Promise((resolve, reject) => {
let script = document.createElement('script')
window[jsoncallback] = function(data) {
resolve(data)
document.body.removeChild(script)
}
script.src = `${url}?jsoncallback=${jsoncallback}`
document.body.appendChild(script)
})
}
jsonp({
url: 'https://www.runoob.com/try/ajax/jsonp.php',
jsoncallback: 'callbackFunction'
}).then(data => {
console.log(data)
})
</script>
</body>
</html>
進階2-隨機產生函數名
jsoncallback 如果一樣, 會被覆蓋掉;為了解決這個問題,可以隨機產生函數名
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<script type="text/javascript">
// https://www.runoob.com/try/ajax/jsonp.php?jsoncallback=linXXXXX
function jsonp({
url,
jsoncallback
}) {
return new Promise((resolve, reject) => {
let script = document.createElement('script')
window[jsoncallback] = function(data) {
resolve(data)
document.body.removeChild(script)
}
script.src = `${url}?jsoncallback=${jsoncallback}`
console.log(script.src)
document.body.appendChild(script)
})
}
// 每次請求之前,產生一個隨機的函數名
// 目的是,服務端接收到請求之后,返回一個 callbackName([JOSN格式的數據])
let callbackName = 'lin' + Math.floor(Math.random() * 100000);
console.log(callbackName)
jsonp({
url: 'https://www.runoob.com/try/ajax/jsonp.php',
jsoncallback: callbackName
}).then(data => {
console.log(data)
})
</script>
</body>
</html>
進階3-請求地址中攜帶參數
在這個例子中,對之前的流程進行一些改變。當訪問http://localhost:3000/14-jsonp.html
可以拿到http://localhost:4001/say?wd=hello&jsoncallback=linXXXXX
提供的數據,並進行處理。
准備工作
- 新建 14-jsonp.html
- 新建 101-server2.js
14-jsonp.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<script type="text/javascript">
// http://localhost:4001/say?wd=hello&jsoncallback=linXXXXX
function jsonp({
url,
params,
jsoncallback
}) {
return new Promise((resolve, reject) => {
let script = document.createElement('script')
window[jsoncallback] = function(data) {
resolve(data)
document.body.removeChild(script)
}
if(params){
params = { ...params,
jsoncallback
} // wd=hello&jsoncallback=linXXXXX
let arrs = []
for (let key in params) {
arrs.push(`${key}=${params[key]}`)
}
script.src = `${url}?${arrs.join('&')}`
}else{
script.src = `${url}?jsoncallback=${jsoncallback}`
}
console.log(script.src)
document.body.appendChild(script)
})
}
// 每次請求之前,產生一個隨機的函數名
// 目的是,服務端接收到請求之后,返回一個 callbackName([JOSN格式的數據])
let callbackName = 'lin' + Math.floor(Math.random() * 100000);
console.log(callbackName)
jsonp({
url: 'http://localhost:4001/say',
params: {
wd: 'hello'
},
jsoncallback: callbackName
}).then(data => {
console.log(data)
})
</script>
</body>
</html>
101-server2.js
let express = require('express')
let app = express()
app.get('/say', function(req, res) {
let {
wd,
jsoncallback
} = req.query
console.log(wd) // hello
console.log(jsoncallback) // linXXXXX
res.end(`${jsoncallback}(["customername1","customername2"])`)
})
app.listen(4001)
運行:
- node 100-server1.js
- node 101-server2.js
- 在瀏覽器中輸入
http://localhost:3000/14-jsonp.html
- 打開瀏覽器的控制台,可以看到輸出
總結
- 創建一個回調函數,其函數名(如 linXXXXX )當做參數值,要傳遞給跨域請求數據的服務器,函數形參為要獲取目標數據(服務器返回的data);
- 創建一個 script 標簽,把需要請求資源的地址,賦值給 script 的 src , 還要在這個地址中向服務器傳遞該函數名;
- 服務器接收到請求后,需要把傳遞進來的函數名和它需要傳遞的數據進行拼接 如:
linXXXXX(["customername1","customername2"])
。 - 最后服務器把准備的數據通過 HTTP 協議返回給客戶端,客戶端再調用執行之前聲明的回調函數 linXXXXX,對返回的數據進行處理。
跨域資源共享 CORS
CORS[Cross-Origin Resource Sharing] 是主流的跨域解決方案。目前,所有瀏覽器都支持該功能(IE8+:IE8/9需要使用 XDomainRequest 對象來支持CORS)。分為簡單請求和非簡單請求。
簡單請求
何為簡單請求
- 請求方法為 GET/HEAD/POST 之一
- 僅能使用 CORS 安全的頭部:Accept、Accept-Language、Content-Language、Content-Type
- Content-Type 值只能是: text/plain、multipart/form-data、application/x-www-form-urlencoded 三者其中之一
簡單請求的跨域訪問
- 請求中攜帶 Origin 頭部告知來自哪個域
- 響應中攜帶 Access-Control-Allow-Origin 頭部表示允許哪些域
- 瀏覽器放行
test
- 【效果】開啟
http://localhost:3000/4-index.html
頁面,跨域訪問http://localhost:4000/getData
,並拿到數據。
代碼實現
4-index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<script type="text/javascript">
let xhr = new XMLHttpRequest()
xhr.open('GET', 'http://localhost:4000/getData', true)
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
console.log(xhr.response)
}
}
}
xhr.send()
</script>
</body>
</html>
4-server1.js
let express = require('express');
let app = express();
app.use(express.static(__dirname));
app.listen(3000);
4-server2.js
let express = require('express')
let app = express()
app.use(function(req, res, next) {
// 設置允許的域
res.setHeader('Access-Control-Allow-Origin', 'http://localhost:3000')
next()
})
app.get('/getData', function(req, res) {
console.log(req.headers)
res.end('goodbye')
})
app.use(express.static(__dirname))
app.listen(4000)
- 開啟2個 本機 cors 目錄下的 git bash,分別運行
node 4-server1.js
和node 4-server2.js
- 在瀏覽器中輸入
http://localhost:3000/4-index.html
- 在瀏覽器控制台中可以看到輸出 goodbye
4-server2.js 運行終端中輸出
{
host: 'localhost:4000',
connection: 'keep-alive',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ' +
'AppleWebKit/537.36 (KHTML, like Gecko) ' +
'Chrome/81.0.4044.138 Safari/537.36',
accept: '*/*',
origin: 'http://localhost:3000',
'sec-fetch-site': 'same-site',
'sec-fetch-mode': 'cors',
'sec-fetch-dest': 'empty',
referer: 'http://localhost:3000/4-index.html',
'accept-encoding': 'gzip, deflate, br',
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8'
}
非簡單請求
- 非簡單請求是那種對服務器有特殊要求的請求,比如請求方法是PUT或DELETE,或者Content-Type字段的類型是application/json。
- 非簡單請求的CORS請求,會在正式通信之前,增加一次HTTP查詢請求,稱為"預檢"請求(preflight)。
- 瀏覽器先詢問服務器,當前網頁所在的域名是否在服務器的許可名單之中,以及可以使用哪些HTTP動詞和頭信息字段。只有得到肯定答復,瀏覽器才會發出正式的XMLHttpRequest請求,否則就報錯。
預檢請求頭部
- Origin(RFC6454):一個頁面的資源可能來自於多個域名,在 AJAX 等子請求中標明來 源於某個域名下的腳本,以通過服務器的安全校驗
- Access-Control-Request-Method
- 在 preflight 預檢請求 (OPTIONS) 中,告知服務器接下來的請求會使用哪些方法
- Access-Control-Request-Headers
- 在 preflight 預檢請求 (OPTIONS) 中,告知服務器接下來的請求會傳遞哪些頭部
預檢響應頭部
- Access-Control-Allow-Origin
- 告知瀏覽器允許哪些域訪問當前資源,*表示允許所有域。為避免緩存錯亂,響應中需要攜帶 Vary: Origin
- Access-Control-Allow-Methods
- 在 preflight 預檢請求的響應中,告知客戶端后續請求允許使用的方法
- Access-Control-Allow-Headers
- 在 preflight 預檢請求的響應中,告知客戶端后續請求允許攜帶的頭部
- Access-Control-Max-Age
- 在 preflight 預檢請求的響應中,告知客戶端該響應的信息可以緩存多久
- Access-Control-Expose-Headers
- 告知瀏覽器哪些響應頭部可以供客戶端使用,默認情況下只有 Cache-Control、Content-Language、 Content-Type、Expires、Last-Modified、Pragma 可供使用
- Access-Control-Allow-Credentials
- 告知瀏覽器是否可以將 Credentials 暴露給客戶端使用,Credentials 包含 cookie、authorization 類頭部、 TLS證書等。
test
- 【效果】開啟
http://localhost:3000/5-index.html
頁面,跨域訪問http://localhost:4000/getData
,並拿到數據。
代碼實現
5-index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<script type="text/javascript">
let xhr = new XMLHttpRequest()
document.cookie = 'name=lin' // cookie不能跨域
xhr.withCredentials = true // 前端設置是否帶cookie
xhr.open('PUT', 'http://localhost:4000/getData', true)
xhr.setRequestHeader('name', 'lin')
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()
</script>
</body>
</html>
5-server1.js
let express = require('express');
let app = express();
app.use(express.static(__dirname));
app.listen(3000);
5-server2.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')
// 告知瀏覽器是否可以將 Credentials 暴露給客戶端使用,Credentials 包含 cookie、authorization 類頭部、 TLS證書等
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', 'js') //返回一個響應頭,后台需設置
res.end('goodbye')
})
app.get('/getData', function(req, res) {
console.log(req.headers)
res.end('goodbye')
})
app.use(express.static(__dirname))
app.listen(4000)
- 開啟2個 本機 cors 目錄下的 git bash,分別運行
node 5-server1.js
和node 5-server2.js
- 在瀏覽器中輸入
http://localhost:3000/5-index.html
- 在瀏覽器控制台中可以看到輸出
5-server2.js 運行終端中輸出
{
host: 'localhost:4000',
connection: 'keep-alive',
'content-length': '0',
name: 'lin',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ' +
'AppleWebKit/537.36 (KHTML, like Gecko) ' +
'Chrome/81.0.4044.138 Safari/537.36',
accept: '*/*',
origin: 'http://localhost:3000',
'sec-fetch-site': 'same-site',
'sec-fetch-mode': 'cors',
'sec-fetch-dest': 'empty',
referer: 'http://localhost:3000/5-index.html',
'accept-encoding': 'gzip, deflate, br',
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
cookie: 'name=lin'
}
思考
- 什么是跨域?
- 為什么會有跨域?
- 什么是同源策略?
- 為什么會有同源策略?
- 跨域的幾種解決方案