跨域問題詳解
在JavaScript中,有一個很重要的安全性限制,被稱為“Same-Origin Policy”(同源策略)。這一策略對於JavaScript代碼能夠訪問的頁面內容做了很重要的限制,即JavaScript只能訪問與包含它的文檔在同一域下的內容。
??跨越是瀏覽器進行安全限制的一種方法,如果瀏覽器禁用了這種安全限制就不會出現跨域問題產生跨域的原因(以下三者都滿足):
- 只要調用方訪問被調用方的域名、端口、IP不一樣
- 瀏覽器沒有禁用安全限制
- 采用了XMLHttpRequest的請求
JavaScript這個安全策略在進行多iframe或多窗口編程、以及Ajax編程時顯得尤為重要。根據這個策略,在baidu.com下的頁面中包含的JavaScript代碼,不能訪問在google.com域名下的頁面內容;甚至不同的子域名之間的頁面也不能通過JavaScript代碼互相訪問。對於Ajax的影響在於,通過XMLHttpRequest實現的Ajax請求,不能向不同的域提交請求,例如,在abc.example.com下的頁面,不能向def.example.com提交Ajax請求,等等。
為什麽瀏覽器要實現同源限制?我們舉例說明:
比如一個黑客,他利用iframe把真正的銀行登錄頁面嵌到他的頁面上,當你使用真實的用戶名和密碼登錄時,如果沒有同源限制,他的頁面就可以通過javascript讀取到你的表單中輸入的內容,這樣用戶名和密碼就輕松到手了。又比如你登錄了OSC,同時瀏覽了惡意網站,如果沒有同源限制,該惡意網站就可以構造AJAX請求頻繁在OSC發廣告帖。
一、跨域問題發生場景
- 特別註意兩點:
1、如果是協議和端口造成的跨域問題“前臺”是無能為力的
2、在跨域問題上,域僅僅是通過“URL的首部”來識別而不會去嘗試判斷相同的ip地址對應著兩個域或兩個域是否在同一個ip上。比如上面的,http://www.a.com/a.js和http://70.32.92.74/b.js。雖然域名和域名的ip對應,不過還是被認為是跨域。
*“URL的首部”指window.location.protocol +window.location.host。其中,window.location.protocol:指含有URL第一部分的字符串,如http: ,window.location.host:指包含有URL中主機名:端口號部分的字符串.如//www.cenpok.net/server/
二、跨域問題的解決方案
??解決跨域的思路
- 被調用方解決: 被調用方解決-支持跨域(根據http協議關於跨域方面的要求,增加響應頭信息,告訴瀏覽器允許被跨域調用)(因為在發生跨域請求時首先調用方發送一個預檢請求(OPTIONS請求),這個請求就會被帶上允許跨越的請求頭信息)
- 調用方解決:使用代理做調用解決跨域問題-隱藏跨域(利用nginx的反向代理,使訪問同一個域名不同的資源路徑會代理到不同的服務器上,每個跨域的請求都會帶上origin請求頭字段,因為訪問的資源都是同域名下的,所以不會產生跨越問題)
1、JSONP跨域
? JSONP(JSON with Padding)是數據格式JSON的一種“使用模式”,可以讓網頁從別的網域要數據。根據 XmlHttpRequest 對象受到同源策略的影響,而利用 <script>元素的這個開放策略,網頁可以得到從其他來源動態產生的JSON數據,而這種使用模式就是所謂的 JSONP。用JSONP抓到的數據並不是JSON,而是任意的JavaScript,用 JavaScript解釋器運行而不是用JSON解析器解析。所有,通過Chrome查看所有JSONP發送的Get請求都是js類型,而非XHR。
①原理
我們知道,在頁面上有三種資源是可以與頁面本身不同源的。它們是:js腳本,css樣式文件,圖片,像淘寶等大型網站,肯定會將這些靜態資源放入cdn中,然後在頁面上連接,如下所示,所以它們是可以鏈接訪問到不同源的資源的。
1 <script type="text/javascript" src="某某cdn地址" ></script>
2 <link type="text/css" rel="stylesheet" href="某個cdn地址" />
3 <img src="某個cdn地址" alt=""/>
而jsonp就是利用了script標簽的src屬性是沒有跨域的限制的,從而達到跨域訪問的目的。因此它的最基本原理就是:動態添加一個<script>標簽來實現。
②實現方法:
這裏是使用ajax來請求的,看起來和ajax沒啥區別,其實還是有區別的。ajax的核心是通過XmlHttpRequest獲取非本頁內容,而jsonp的核心則是動態添加<script>標簽來調用服務器提供的js腳本。
$.ajax({
url:"http://www.baidu.com/service",
dataType:‘jsonp‘,
data:‘‘,
jsonp:‘callback‘,
success:function(data) {
// some code
}
});
上面的代碼中,callback是必須的,callback是什麽值要跟後臺拿。獲取到的jsonp數據格式如下:
callback({
"code": "CA1998",
"price": 1780,
"tickets": 5
});
③JSONP的不足之處:
- 只能使用get方法,不能使用post方法:我們知道 script,link, img 等等標簽引入外部資源,都是 get 請求的,那麽就決定了 jsonp 一定是 get 的。但有時候我們使用的 post 請求也成功,為啥呢?這是因為當我們指定dataType:‘jsonp‘,不論你指定:type:"post" 或者type:"get",其實質上進行的都是 get 請求!
- 沒有關於 JSONP 調用的錯誤處理。如果動態腳本插入有效,就執行調用;如果無效,就靜默失敗。失敗是沒有任何提示的。例如,不能從服務器捕捉到 404 錯誤,也不能取消或重新開始請求。不過,等待一段時間還沒有響應的話,就不用理它了。
2、跨域資源共享 CORS
? Cross-Origin Resource Sharing(CORS)跨域資源共享是一份瀏覽器技術的規範,提供了 Web 服務從不同域傳來沙盒腳本的方法,以避開瀏覽器的同源策略,確保安全的跨域數據傳輸。現代瀏覽器使用CORS在API容器如XMLHttpRequest來減少HTTP請求的風險來源。與 JSONP 不同,CORS 除了 GET 要求方法以外也支持其他的 HTTP 要求。
瀏覽器將CORS請求分成兩類:簡單請求(simple request)和非簡單請求(not-so-simple request)。
①簡單請求
只要同時滿足以下兩大條件,就屬於簡單請求。
(1)請求方法是以下三種方法之一:
1 HEAD
2 GET
3 POST
(2)HTTP的頭信息不超出以下幾種字段:
1 Accept
2 Accept-Language
3 Content-Language
4 Last-Event-ID
5 Content-Type:只限於三個值application/x-www-form-urlencoded、multipart/form-data、text/plain
後端
1 // 處理成功失敗返回格式的工具
2 const {successBody} = require(‘../utli‘)
3 class CrossDomain {
4 static async cors (ctx) {
5 const query = ctx.request.query
6 // *時cookie不會在http請求中帶上
7 ctx.set(‘Access-Control-Allow-Origin‘, ‘*‘)
8 ctx.cookies.set(‘tokenId‘, ‘2‘)
9 ctx.body = successBody({msg: query.msg}, ‘success‘)
10 }
11 }
12 module.exports = CrossDomain
前端什麽也不用幹,就是正常發請求就可以,如果需要帶cookie的話,前後端都要設置一下,下面那個非簡單請求例子會看到。
1 fetch(`http://localhost:9871/api/cors?msg=helloCors`).then(res => {
2 console.log(res)
3 })
②非簡單請求
非簡單請求會發出一次預檢測請求,返回碼是204,預檢測通過才會真正發出請求,這才返回200。這裏通過前端發請求的時候增加一個額外的headers來觸發非簡單請求。
後端
1 // 處理成功失敗返回格式的工具
2 const {successBody} = require(‘../utli‘)
3 class CrossDomain {
4 static async cors (ctx) {
5 const query = ctx.request.query
6 // 如果需要http請求中帶上cookie,需要前後端都設置credentials,且後端設置指定的origin
7 ctx.set(‘Access-Control-Allow-Origin‘, ‘http://localhost:9099‘)
8 ctx.set(‘Access-Control-Allow-Credentials‘, true)
9 // 非簡單請求的CORS請求,會在正式通信之前,增加一次HTTP查詢請求,稱為"預檢"請求(preflight)
10 // 這種情況下除了設置origin,還需要設置Access-Control-Request-Method以及Access-Control-Request-Headers
11 ctx.set(‘Access-Control-Request-Method‘, ‘PUT,POST,GET,DELETE,OPTIONS‘)
12 ctx.set(‘Access-Control-Allow-Headers‘, ‘Origin, X-Requested-With, Content-Type, Accept, t‘)
13 ctx.cookies.set(‘tokenId‘, ‘2‘)
14
15 ctx.body = successBody({msg: query.msg}, ‘success‘)
16 }
17 }
18 module.exports = CrossDomain
一個接口就要寫這麽多代碼,如果想所有接口都統一處理,有什麽更優雅的方式呢?見下面的代碼
1 const path = require(‘path‘)
2 const Koa = require(‘koa‘)
3 const koaStatic = require(‘koa-static‘)
4 const bodyParser = require(‘koa-bodyparser‘)
5 const router = require(‘./router‘)
6 const cors = require(‘koa2-cors‘)
7 const app = new Koa()
8 const port = 9871
9 app.use(bodyParser())
10 // 處理靜態資源 這裏是前端build好之後的目錄
11 app.use(koaStatic(
12 path.resolve(__dirname, ‘../dist‘)
13 ))
14 // 處理cors
15 app.use(cors({
16 origin: function (ctx) {
17 return ‘http://localhost:9099‘
18 },
19 credentials: true,
20 allowMethods: [‘GET‘, ‘POST‘, ‘DELETE‘],
21 allowHeaders: [‘t‘, ‘Content-Type‘]
22 }))
23 // 路由
24 app.use(router.routes()).use(router.allowedMethods())
25 // 監聽端口
26 app.listen(9871)
27 console.log(`[demo] start-quick is starting at port ${port}`)
前端
1 fetch(`http://localhost:9871/api/cors?msg=helloCors`, {
2 // 需要帶上cookie
3 credentials: ‘include‘,
4 // 這裏添加額外的headers來觸發非簡單請求
5 headers: {
6 ‘t‘: ‘extra headers‘
7 }
8 }).then(res => {
9 console.log(res)
10 })
3、反向代理
想一下,如果我們請求的時候還是用前端的域名,然後有個東西幫我們把這個請求轉發到真正的後端域名上,不就避免跨域了嗎?這時候,Nginx出場了。
Nginx配置
1 server{
2 # 監聽9099端口
3 listen 9099;
4 # 域名是localhost
5 server_name localhost;
6 #凡是localhost:9099/api這個樣子的,都轉發到真正的服務端地址http://localhost:9871
7 location ^~ /api {
8 proxy_pass http://localhost:9871;
9 }
10 }
前端就不用幹什麽事情了,除了寫接口,也沒後端什麽事情了
1 // 請求的時候直接用回前端這邊的域名http://localhost:9099,這就不會跨域,然後Nginx監聽到凡是localhost:9099/api這個樣子的,都轉發到真正的服務端地址http://localhost:9871
2 fetch(‘http://localhost:9099/api/iframePost‘, {
3 method: ‘POST‘,
4 headers: {
5 ‘Accept‘: ‘application/json‘,
6 ‘Content-Type‘: ‘application/json‘
7 },
8 body: JSON.stringify({
9 msg: ‘helloIframePost‘
10 })
11 })
Nginx轉發的方式似乎很方便!但這種使用也是看場景的,如果後端接口是一個公共的API,比如一些公共服務獲取天氣什麽的,前端調用的時候總不能讓運維去配置一下Nginx,如果兼容性沒問題(IE 10或者以上),CROS才是更通用的做法吧。
跨域問題詳解