前端常見跨域解決方案


什么是跨域

跨域是指一個域下的文檔或腳本試圖去請求另一個域下的資源,這里跨域是廣義的。

廣義的跨域:

  • 資源跳轉: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提供的數據,並進行處理。

備注:

准備工作

  • 在本地建立一個 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"]

如圖

11-2

解釋:

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>

如圖

11-1

  • <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
  • 打開瀏覽器的控制台,可以看到輸出

11-4

總結

  • 創建一個回調函數,其函數名(如 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 頭部表示允許哪些域
  • 瀏覽器放行

簡單請求-1

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.jsnode 4-server2.js
  • 在瀏覽器中輸入http://localhost:3000/4-index.html
  • 在瀏覽器控制台中可以看到輸出 goodbye

簡單請求-2

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請求,否則就報錯。

非簡單請求-1

預檢請求頭部

  • 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.jsnode 5-server2.js
  • 在瀏覽器中輸入http://localhost:3000/5-index.html
  • 在瀏覽器控制台中可以看到輸出

非簡單請求-2

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'
}

思考

  • 什么是跨域?
  • 為什么會有跨域?
  • 什么是同源策略?
  • 為什么會有同源策略?
  • 跨域的幾種解決方案

參考資料


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM