前端跨域解決方案概括整理

前言

前端跨域的各類文章其實已經不少了,但大部分仍是不太符合我胃口的介紹跨域。看來看去,若是要讓本身理解印象深入,果真仍是得本身敲一敲,並總結概括整理一篇博客出來,以此記錄。javascript

跨域的限制

跨域是爲了阻止用戶讀取到另外一個域名下的內容,Ajax 能夠獲取響應,瀏覽器認爲這不安全,因此攔截了響應。html

跨域解決方案

除非特別說明,不然下方標記的 html 文件默認都運行在 http://127.0.0.1:5500 服務下前端

CORS

CORS 便是指跨域資源共享。它容許瀏覽器向非同源服務器,發出 Ajax 請求,從而克服了 Ajax 只能同源使用的限制。這種方式的跨域主要是在後端進行設置。vue

這種方式的關鍵是後端進行設置,便是後端開啓 Access-Control-Allow-Origin 爲*或對應的 origin就能夠實現跨域。java

瀏覽器將 CORS 請求分紅兩類:簡單請求和非簡單請求。node

只要同時知足如下兩大條件,就屬於簡單請求。react

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

凡是不一樣時知足上面兩個條件,就屬於非簡單請求。webpack

簡單請求nginx

cors.htmlgit

let xhr = new XMLHttpRequest() xhr.open('GET', 'http://localhost:8002/request')
xhr.send(null)

server.js

const express = require('express')
const app = express()

app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', 'http://127.0.0.1:5500') // 設置容許哪一個域訪問
  next()
})

app.get('/request', (req, res) => {
  res.end('server ok')
})

app.listen(8002)

非簡單請求

上面的是簡單請求,若是咱們用非簡單請求的方式,好比請求方法是 PUT,也能夠經過設置實現跨域。

非簡單請求的 CORS 請求,會在正式通訊以前,增長一次 HTTP 查詢請求,稱爲"預檢"請求。

瀏覽器先詢問服務器,服務器收到"預檢"請求之後,檢查了 Origin、Access-Control-Request-Method 和 Access-Control-Request-Headers 字段之後,確認容許跨源請求,瀏覽器纔會發出正式的 XMLHttpRequest 請求,不然就報錯。

let xhr = new XMLHttpRequest()
xhr.open('PUT', 'http://localhost:8002/request')
xhr.send(null)

server.js

const express = require('express')
const app = express()

let whileList = ['http://127.0.0.1:5500'] // 設置白名單
app.use((req, res, next) => {
  let origin = req.headers.origin
  console.log(whitList.includes(origin))
  if (whitList.includes(origin)) {
    res.setHeader('Access-Control-Allow-Origin', origin) // 設置容許哪一個域訪問
    res.setHeader('Access-Control-Allow-Methods', 'PUT') // 設置容許哪一種請求方法訪問
  }
  next()
})

app.put('/request', (req, res) => {
  res.end('server ok')
})

app.listen(8002)

整個過程發送了兩次請求,跨域成功。

固然,還能夠設置其餘參數:

  • Access-Control-Request-Headers

該字段是一個逗號分隔的字符串,指定瀏覽器 CORS 請求會額外發送的頭信息字段

  • Access-Control-Allow-Credentials

表示是否容許發送 Cookie。默認狀況下,Cookie 不包括在 CORS 請求之中。

  • Access-Control-Expose-Headers

CORS 請求時,XMLHttpRequest 對象的 getResponseHeader()方法只能拿到 6 個基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。若是想拿到其餘字段,就必須在 Access-Control-Expose-Headers 裏面指定。

  • Access-Control-Max-Age

用來指定本次預檢請求的有效期,單位爲秒。有效期是 20 天(1728000 秒),即容許緩存該條迴應 1728000 秒(即 20 天),在此期間,不用發出另外一條預檢請求。

Node 中間件代理

實現原理:同源策略是瀏覽器須要遵循的標準,而若是是服務器向服務器請求就沒有跨域一說。

代理服務器,須要作如下幾個步驟:

  1. 接受客戶端請求 。
  2. 將請求轉發給服務器。
  3. 拿到服務器響應數據。
  4. 將響應轉發給客戶端。

此次咱們使用 express 中間件 http-proxy-middleware 來代理跨域, 轉發請求和響應

案例三個文件都在同一級目錄下:

index.html

let xhr = new XMLHttpRequest()
xhr.open('GET', '/api/request')
xhr.onreadystatechange = () => {
  if (xhr.readyState === 4 && xhr.status === 200) {
    console.log('請求成功,結果是:', xhr.responseText) // request success
  }
}
xhr.send(null)

nodeMdServer.js

const express = require('express')
const { createProxyMiddleware } = require('http-proxy-middleware')

const app = express()

// 設置靜態資源
app.use(express.static(__dirname))

// 使用代理
app.use(
  '/api',
  createProxyMiddleware({
    target: 'http://localhost:8002',
    pathRewrite: {
      '^/api': '', // 重寫路徑
    },
    changeOrigin: true,
  })
)

app.listen(8001)

nodeServer.js

const express = require('express')
const app = express()

app.get('/request', (req, res) => {
  res.end('request success')
})

app.listen(8002)

運行http://localhost:8001/index.html,跨域成功

日常 vue/react 項目配置 webpack-dev-server 的時候也是經過 Node proxy 代理的方式來解決的。

Nginx 反向代理

實現原理相似於 Node 中間件代理,須要你搭建一箇中轉 nginx 服務器,用於轉發請求。

這種方式只需修改 Nginx 的配置便可解決跨域問題,前端除了接口換成對應形式,而後先後端不須要修改做其餘修改。

實現思路:經過 nginx 配置一個代理服務器(同域不一樣端口)作跳板機,反向代理要跨域的域名,這樣能夠修改 cookie 中 domain 信息,方便當前域 cookie 寫入,實現跨域登陸。

nginx 目錄下的 nginx.conf 修改以下:

// proxy服務器
server {
    listen 80;
    server_name  www.domain1.com;
    location / {
        proxy_pass   http://www.domain2.com:8080;  # 反向代理
        proxy_cookie_domain www.domain2.com www.domain1.com; # 修改cookie裏域名
        index  index.html index.htm;

        # 當用webpack-dev-server等中間件代理接口訪問nignx時,此時無瀏覽器參與,故沒有同源限制,下面的跨域配置可不啓用
        add_header Access-Control-Allow-Origin http://www.domain1.com;  # 當前端只跨域不帶cookie時,可爲*
        add_header Access-Control-Allow-Credentials true;
    }
}

啓動 Nginx

index.html

var xhr = new XMLHttpRequest()
// 前端開關:瀏覽器是否讀寫cookie
xhr.withCredentials = true
// 訪問nginx中的代理服務器
xhr.open('get', 'http://www.domain1.com:81/?user=admin', true)
xhr.send()

server.js

var http = require('http')
var server = http.createServer()
var qs = require('querystring')
server.on('request', function (req, res) {
  var params = qs.parse(req.url.substring(2))
  // 向前臺寫cookie
  res.writeHead(200, {
    'Set-Cookie': 'l=123456;Path=/;Domain=www.domain2.com;HttpOnly', // HttpOnly:腳本沒法讀取
  })
  res.write(JSON.stringify(params))
  res.end()
})
server.listen(8080)

jsonp

原理:利用了 script 標籤可跨域的特性,在客戶端定義一個回調函數(全局函數),請求服務端返回該回調函數的調用,並將服務端的數據以該回調函數參數的形式傳遞過來,而後該函數就被執行了。該方法須要服務端配合完成。

實現步驟:

  1. 聲明一個全局回調函數,參數爲服務端返回的 data。
  2. 建立一個 script 標籤,拼接整個請求 api 的地址(要傳入回調函數名稱如 ?callback=getInfo ),賦值給 script 的 src 屬性
  3. 服務端接受到請求後處理數據,而後將函數名和須要返回的數據拼接成字符串,拼裝完成是執行函數的形式。(getInfo('server data'))
  4. 瀏覽器接收到服務端的返回結果,調用了聲明的回調函數。

jsonp.html

function getInfo(data) {
  console.log(data) // 告訴你一聲, jsonp跨域成功
}

let script = document.createElement('script')
script.src = 'http://localhost:3000?callback=getInfo' //
document.body.appendChild(script)

server.js

const express = require('express')
const app = express()

app.get('/', (req, res) => {
  let { callback } = req.query
  res.end(`${callback}('告訴你一聲, jsonp跨域成功')`)
})

app.listen(3000)

jQuery 的 $.ajax() 方法當中集成了 JSONP 的實現,在此就不寫出來了。

在開發中可能會遇到多個 JSONP 請求的回調函數名是相同的,並且這種方式用起來也麻煩,故咱們本身封裝一個 jsonp 函數

function jsonp({ url, params, callback }) {
  return new Promise((resolve, reject) => {
    let script = document.createElement('script')
    // 定義全局回調函數
    window[callback] = function (data) {
      resolve(data)
      document.body.removeChild(script) // 調用完畢即刪除
    }

    params = { callback, ...params } // {callback: "getInfo", name: "jacky"}
    let paramsArr = []
    for (const key in params) {
      paramsArr.push(`${key}=${params[key]}`)
    }
    script.src = `${url}?${paramsArr.join('&')}` // http://localhost:3000/?callback=getInfo&name=jacky
    document.body.appendChild(script)
  })
}

jsonp({
  url: 'http://localhost:3000',
  params: {
    name: 'jacky',
  },
  callback: 'getInfo',
}).then(res => {
  console.log(res) // 告訴你一聲, jsonp跨域成功
})

服務端解構的時候能夠取出參數

app.get('/', (req, res) => {
  let { callback, name } = req.query
  res.end(`${callback}('告訴你一聲, jsonp跨域成功')`)
})

優勢:兼容性好

缺點:因爲 script 自己的限制,該跨域方式僅支持 get 請求,且不安全可能遭受 XSS 攻擊

postMessage

postMessage 是 HTML5 XMLHttpRequest Level 2 中的 API,且是爲數很少能夠跨域操做的 window 屬性之一,它可用於解決如下方面的問題:

  1. 頁面和其打開的新窗口的數據傳遞
  2. 多窗口之間消息傳遞
  3. 頁面與嵌套的 iframe 消息傳遞
  4. 上面三個場景的跨域數據傳遞

總之,它能夠容許來自不一樣源的腳本採用異步方式進行有限的通訊,能夠實現跨文本檔、多窗口、跨域消息傳遞。

otherWindow.postMessage(message, targetOrigin, [transfer]);
  • otherWindow:其餘窗口的一個引用,好比 iframe 的 contentWindow 屬性、執行 window.open 返回的窗口對象、或者是命名過或數值索引的 window.frames。
  • message: 將要發送到其餘 window 的數據。
  • targetOrigin:經過窗口的 origin 屬性來指定哪些窗口能接收到消息事件,其值能夠是字符串"*"(表示無限制)或者一個 URI。在發送消息的時候,若是目標窗口的協議、主機地址或端口這三者的任意一項不匹配 targetOrigin 提供的值,那麼消息就不會被髮送;只有三者徹底匹配,消息纔會被髮送。
  • transfer(可選):是一串和 message 同時傳遞的 Transferable 對象. 這些對象的全部權將被轉移給消息的接收方,而發送一方將再也不保有全部權。

此次咱們把兩個 html 文件掛到兩個 server 下,採起 fs 讀取的方式引入,運行兩個 js 文件

postMessage1.html

<body>
  <iframe src="http://localhost:8002" frameborder="0" id="frame" onLoad="load()"></iframe>
  <script>
    function load() {
      let frame = document.getElementById('frame')
      frame.contentWindow.postMessage('你好,我是postMessage1', 'http://localhost:8002') //發送數據
      window.onmessage = function (e) {
        //接受返回數據
        console.log(e.data) // 你好,我是postMessage2
      }
    }
  </script>
</body>

postMsgServer1.js

const express = require('express')
const fs = require('fs')
const app = express()

app.get('/', (req, res) => {
  const html = fs.readFileSync('./postMessage1.html', 'utf8')
  res.end(html)
})

app.listen(8001, (req, res) => {
  console.log('server listening on 8001')
})

postMessage2.html

<body>
  <script>
    window.onmessage = function (e) {
      console.log(e.data) // 你好,我是postMessage1
      e.source.postMessage('你好,我是postMessage2', e.origin)
    }
  </script>
</body>

postMsgServer2.js

const express = require('express')
const fs = require('fs')
const app = express()

app.get('/', (req, res) => {
  const html = fs.readFileSync('./postMessage2.html', 'utf8')
  res.end(html)
})

app.listen(8002, (req, res) => {
  console.log('server listening on 8002')
})

websocket

WebSocket 是一種網絡通訊協議。它實現了瀏覽器與服務器全雙工通訊,同時容許跨域通信,長鏈接方式不受跨域影響。因爲原生 WebSocket API 使用起來不太方便,咱們通常都會使用第三方庫如 ws。

Web 瀏覽器和服務器都必須實現 WebSockets 協議來創建和維護鏈接。因爲 WebSockets 鏈接長期存在,與典型的 HTTP 鏈接不一樣,對服務器有重要的影響。

socket.html(http://127.0.0.1:5500/socket.html

let socket = new WebSocket('ws://localhost:8001')
socket.onopen = function () {
  socket.send('向服務端發送數據')
}
socket.onmessage = function (e) {
  console.log(e.data) // 服務端傳給你的數據
}

運行nodeServer.js

const express = require('express')
const WebSocket = require('ws')
const app = express()

let wsServer = new WebSocket.Server({ port: 8001 })
wsServer.on('connection', function (ws) {
  ws.on('message', function (data) {
    console.log(data) // 向服務端發送數據
    ws.send('服務端傳給你的數據')
  })
})

document.domain + iframe

這種方式只能用於二級域名相同的狀況下。

好比 a.test.com 和 b.test.com 就屬於二級域名,它們都是 test.com 的子域

只須要給頁面添加 document.domain ='test.com' 表示二級域名都相同就能夠實現跨域。

好比:頁面 a.test.com:3000/test1.html 獲取頁面 b.test.com:3000/test2.html 中 a 的值

test1.html

<body>
  <iframe
    src="http://b.test.com:3000/test2.html"
    frameborder="0"
    onload="load()"
    id="iframe"
  ></iframe>
  <script>
    document.domain = 'test.com'
    function load() {
      console.log(iframe.contentWindow.a)
    }
  </script>
</body>

test2.html

document.domain = 'test.com'
var a = 10

window.name + iframe

瀏覽器具備這樣一個特性:同一個標籤頁或者同一個 iframe 框架加載過的頁面共享相同的 window.name 屬性值。在同個標籤頁裏,name 值在不一樣的頁面加載後也依舊存在,這些頁面上 window.name 屬性值都是相同的。利用這些特性,就能夠將這個屬性做爲在不一樣頁面之間傳遞數據的介質。

因爲安全緣由,瀏覽器始終會保持 window.name 是 string 類型。

打開http://localhost:8001/a.html

<body>
  <iframe
    src="http://localhost:8002/c.html"
    frameborder="0"
    onload="load()"
    id="iframe"
  ></iframe>
  <script>
    let first = true
    function load() {
      if (first) {
        // 第1次onload(跨域頁)成功後,切換到同域代理頁面
        let iframe = document.getElementById('iframe')
        iframe.src = 'http://localhost:8001/b.html'
        first = false
      } else {
        // 第2次onload(同域b.html頁)成功後,讀取同域window.name中數據
        console.log(iframe.contentWindow.name) // 我是c.html裏的數據
      }
    }
  </script>
</body>
<body>
  <script>
    window.name = '我是c.html裏的數據'
  </script>
</body>

c 頁面給 window.name 設置了值, 即使 c 頁面銷燬,但 name 值不會被銷燬;a 頁面依舊可以獲得 window.name。

location.hash + iframe

實現原理: a.html 欲與 c.html 跨域相互通訊,經過中間頁 b.html 來實現。 三個頁面,不一樣域之間利用 iframe 的 location.hash 傳值,相同域之間直接 js 訪問來通訊。

具體實現步驟:一開始 a.html 給 c.html 傳一個 hash 值,而後 c.html 收到 hash 值後,再把 hash 值傳遞給 b.html,最後 b.html 將結果放到 a.html 的 hash 值中。

一樣的,a.html 和 b.html 是同域的,都是http://localhost:8001,也就是說 b 的 hash 值能夠直接複製給 a 的 hash。c.html 爲http://localhost:8002下的

a.html

<body>
  <iframe src="http://localhost:8002/c.html#jackylin" style="display: none;"></iframe>
  <script>
    window.onhashchange = function () {
      // 檢測hash的變化
      console.log(456, location.hash) // #monkey
    }
  </script>
</body>

b.html

window.parent.parent.location.hash = location.hash
// b.html將結果放到a.html的hash值中,b.html可經過parent.parent訪問a.html頁面

c.html

console.log(location.hash) //  #jackylin
let iframe = document.createElement('iframe')
iframe.src = 'http://localhost:8001/b.html#monkey'
document.body.appendChild(iframe)

總結

  • CORS 支持全部的 HTTP 請求,是跨域最主流的方案
  • 平常工做中,用得比較多的跨域方案是 CORS 和 Node 中間件及 Nginx 反向代理
  • 不論是 Node 中間件代理仍是 Nginx 反向代理,主要是經過同源策略對服務器不加限制。

參考


相關文章
相關標籤/搜索