一次跨域問題引起的思考

翊溪發表於2018-03-28

問題

今天在聯調介面的時候,發現了一個奇怪的錯誤Request header field Authorization is not allowed by Access-Control-Allow-Headers in preflight response

一次跨域問題引起的思考
剛開始以為只是一次簡單的跨域訪問,後來在網上查閱資料,發現這是“預檢請求”
一次跨域問題引起的思考
這裡傳送的是一次OPTIONS請求,獲得瀏覽器支援的請求型別,那麼何時會進行預檢請求呢?

只有在瀏覽器傳送非簡單請求的情況下,才會傳送預檢請求

那麼什麼樣叫做非簡單請求,什麼又是簡單請求呢?

只要同時滿足以下兩大條件,就屬於簡單請求。

  1. 請求方法是以下三種方法之一:
    • HEAD
    • GET
    • POST
  2. HTTP的頭資訊不超出以下幾種欄位:
    • Accept
    • Accept-Language
    • Content-Language
    • Last-Event-ID
    • Content-Type:只限於三個值application/x-www-form-urlencoded、multipart/form-data、text/plain

凡是不同時滿足上面兩個條件,就屬於非簡單請求。

這是該問題所引出的相關名詞,本著好奇的原則,繼續往下走...

跨域

跨域是針對瀏覽器端而產生的,伺服器端的通訊並不會產生這樣的問題,這就引出了第一個解決跨域的思路----代理伺服器(瀏覽器請求本地起的伺服器—同源伺服器,再由後者請求外部服務)並且跨域並非瀏覽器限制了發起跨站請求,而是跨站請求可以正常發起,但是返回結果被瀏覽器攔截了。

代理伺服器

我們利用node可以實現代理伺服器,筆者是利用了http-proxy-middleware

var proxy = require('http-proxy-middleware');

var apiProxy = proxy('/api', {target: 'http://www.example.org', changeOrigin: true});
複製程式碼

在這裡changeOrigin: true引起了我的興趣,思考為什麼會存在該項配置?這裡提到了虛擬主機與獨立主機的概念

在我們ping baidu.com的時候會得到IP地址,瀏覽器通過這個IP地址是可以訪問網站的(獨立主機),在我們ping apple.com的時候得到的IP地址是不能通過瀏覽器的方式訪問的(共享主機或者虛擬主機) 虛擬主機是一種在單一主機或主機群上,實現多網域服務的方法,可以執行多個網站或服務的技術,因此它可以共享伺服器的記憶體、CPU等資源

虛擬主機主要有以下幾種型別

  1. 網址名稱對應(Name-based)
  2. IP地址對應(IP-based)
  3. Port埠號對應

http-proxy-middleware中使用的changeOrigin配置項正式由於使用了網址名稱對應的虛擬主機。

跨域資源共享(CORS)

在本篇部落格中,該種方式會進行詳細介紹,利用AJAX進行跨域資源共享的時候,我們就會引出文章剛開始所遇到的問題,簡單請求和非簡單請求。

  • 簡單請求: 在這裡為了模仿跨域的請求利用到express框架,先建立兩個檔案,一個後端介面檔案backend.js。

'use strict';
var express = require('express'),
	app = express();

app.get('/auth/:id/', function (req, res) { 
	res.send({ id:req.params.id }); 
});

app.listen(3000);
複製程式碼

然後利用backend.js指向一個html檔案,在html裡面傳送ajax請求,模仿跨域(埠號不同)

'use strict';

var express = require('express'),
	app = express();

app.use(express.static("./"))
app.get('/', function (req, res) {
	res.render(__dirname + '/index.html');
});

app.listen(4000);
複製程式碼

最後是html檔案,引入了axios

<!DOCTYPE html>
<html>
<head>
	<title></title>
	<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>
<body>

</body>
<script type="text/javascript">
	axios.get('http://localhost:3000/auth/1')
	  .then(function (response) {
	    console.log(response.data);
	  })
	  .catch(function (error) {
	    console.log(error);
	  });
</script>
</html>
複製程式碼

當發起請求時,我們發現

一次跨域問題引起的思考
一次跨域問題引起的思考
即使在返回結果是200的情況下,仍然時跨域提醒,這就與前面的觀點是一致的 跨域並非瀏覽器限制了發起跨站請求,而是跨站請求可以正常發起,但是返回結果被瀏覽器攔截了。 伺服器會返回一個正常的HTTP迴應。瀏覽器發現,這個迴應的頭資訊沒有包含Access-Control-Allow-Origin欄位(詳見下文),就知道出錯了,從而丟擲一個錯誤。 那麼解決方法也很簡單,在後端加上允許跨域請求。

app.all('*', function(req, res, next) { 
	res.header("Access-Control-Allow-Origin", "*");
	res.header("Content-Type", "application/json;charset=utf-8");
	next(); 
});
複製程式碼

這樣就可以正常訪問

  • 非簡單請求 依舊利用上面的程式碼,在傳送請求POST時,當頭部Content-Typeapplication/json時,程式碼如下:
<!DOCTYPE html>
<html>
<head>
	<title></title>
	<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>
<body>

</body>
<script type="text/javascript">
	axios.post('http://localhost:3000/auth', {
		name: 'gao'
	}, {
		headers: {
			'Content-Type': 'application/json'
		}
	})
	  .then(function (response) {
	    console.log(response.data);
	  })
	  .catch(function (error) {
	    console.log(error);
	  });
</script>
</html>
複製程式碼

會傳送如圖所示的預檢請求:

一次跨域問題引起的思考

一次跨域問題引起的思考
當頭部Content-Typeapplication/x-www-form-urlencoded時,程式碼如下:

<!DOCTYPE html>
<html>
<head>
	<title></title>
	<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>
<body>

</body>
<script type="text/javascript">
	axios.post('http://localhost:3000/auth', {
		name: 'gao'
	}, {
		headers: {
			'Content-Type': 'application/x-www-form-urlencoded'
		}
	})
	  .then(function (response) {
	    console.log(response.data);
	  })
	  .catch(function (error) {
	    console.log(error);
	  });
</script>
</html>
複製程式碼

會發現,瀏覽器不會傳送預檢請求並且返回成功

一次跨域問題引起的思考
那麼如何保證在傳送預檢請求時,瀏覽器不會報錯呢?

後端程式碼如下:

'use strict';
var express = require('express'),
	app = express();

app.all('*', function(req, res, next) { 
	res.header("Access-Control-Allow-Origin", "*");
	res.header("Access-Control-Allow-Headers", "content-type");
	res.header("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,OPTIONS"); 
	res.header("Content-Type", "application/json;charset=utf-8");
	next(); 
});

app.get('/auth/:id/', function (req, res) { 
	res.send({ id:req.params.id }); 
});

app.post('/auth', function (req, res) { 
	res.send({ id:req.params.id }); 
});


app.listen(3000);
複製程式碼

會發現加入了這兩行程式碼即可。

res.header("Access-Control-Allow-Headers", "content-type"); // 這裡的content-type是預設的,根據客戶端請求來設定
res.header("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,OPTIONS"); 
複製程式碼

此時POST會傳送兩次請求:

一次跨域問題引起的思考

一次跨域問題引起的思考
當預檢請求通過後,再次傳送POST請求。

其他

像其他的技術,包括JSONP、LocalStorage、cookie、postMessage在此不多做介紹,可以參考阮大的部落格進行參考。

奇淫技巧

在有些時候,後端的介面由於各種不能滿足我們所需要的跨域支援,那麼就需要前端自己下功夫,我們思考一下,既然瀏覽器由於安全策略阻止了返回的結果,那麼就可以在瀏覽器的安全策略上進行修改。

設定Chrome瀏覽器的 disable-web-security, 實現跨域訪問後端的介面。

windows

"C:\Users\UserName\AppData\Local\Google\Chrome\Application\chrome.exe" --disable-web-security --user-data-dir

mac

open -a "Google Chrome" --args --disable-web-security --user-data-dir

linux

chromium-browser --disable-web-security

參考文章

相關文章