關於跨域那些事兒
在工作中,難免會遇到跨域的問題,就像你高高興興的帶著老婆吃著火鍋,啊不對,是匆匆忙忙的在搬磚,突然瀏覽器告訴你跨域了,意不意外?
既然遇到了,就只能解決他,平時一頓亂操作,也能解決問題,但一直沒有好好的來總結。
俗話說的好,知己知彼、百戰不殆。我們來看看什麽是跨域?誰在搞事情?
原來是瀏覽器在搞事情,不過,瀏覽器表示它不想背這個鍋:
同源策略限制了從同一個源加載的文檔或腳本如何與來自另一個源的資源進行交互。這是一個用於隔離潛在惡意文件的重要安全機制。
這是官方的解釋,比較拗口,意思就是,為了網頁更加安全而設計的安全策略,這個同源策略規定:
如果兩個頁面的協議,端口(如果有指定)和域名都相同,則兩個頁面具有相同的源,訪問不受限制;否則則為跨源(跨域),限制訪問。
瀏覽器為什麽要搞個同源策略呢?
這是為了防止CSRF攻擊(Cross-site request forgery),中文名稱:跨站請求偽造。誰不怕坐著火車出了城,突然就被麻匪給劫了不是。
既然跨域必須存在,而我們又必須繞不過它,那我們該如何解決呢?
1、JSONP-----需要後端接口配合
利用script、img這樣沒有跨域限制的標簽,來發起請求。
第一版的JSONP
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="utf-8"> 5 </head>6 <body> 7 <script type=‘text/javascript‘> 8 // 後端返回直接執行的方法,相當於執行這個方法,由於後端把返回的數據放在方法的參數裏,所以這裏能拿到res。 9 window.jsonpCb = function (res) { 10 console.log(res) 11 } 12 </script> 13 <script src=‘http://localhost:9871/api/jsonp?msg=helloJsonp&cb=jsonpCb‘type=‘text/javascript‘></script> 14 </body> 15 </html>
封裝版:
1 /** 2 * JSONP請求工具 3 * @param url 請求的地址 4 * @param data 請求的參數 5 * @returns {Promise<any>} 6 */ 7 const request = ({url, data}) => { 8 return new Promise((resolve, reject) => { 9 // 處理傳參成xx=yy&aa=bb的形式 10 const handleData = (data) => { 11 const keys = Object.keys(data) 12 const keysLen = keys.length 13 return keys.reduce((pre, cur, index) => { 14 const value = data[cur] 15 const flag = index !== keysLen - 1 ? ‘&‘ : ‘‘ 16 return `${pre}${cur}=${value}${flag}` 17 }, ‘‘) 18 } 19 // 動態創建script標簽 20 const script = document.createElement(‘script‘) 21 // 接口返回的數據獲取 22 window.jsonpCb = (res) => { 23 document.body.removeChild(script) 24 delete window.jsonpCb 25 resolve(res) 26 } 27 script.src = `${url}?${handleData(data)}&cb=jsonpCb` 28 document.body.appendChild(script) 29 }) 30 } 31 // 使用方式 32 request({ 33 url: ‘http://localhost:9871/api/jsonp‘, 34 data: { 35 // 傳參 36 msg: ‘helloJsonp‘ 37 } 38 }).then(res => { 39 console.log(res) 40 })
2、空iframe和form配合
因為script標簽加載資源的方式就是GET。所以JSONP只能發GET請求,那麽我們發送POST請求這麽辦呢?
1 const requestPost = ({url, data}) => { 2 // 首先創建一個用來發送數據的iframe. 3 const iframe = document.createElement(‘iframe‘) 4 iframe.name = ‘iframePost‘ 5 iframe.style.display = ‘none‘ 6 document.body.appendChild(iframe) 7 const form = document.createElement(‘form‘) 8 const node = document.createElement(‘input‘) 9 // 註冊iframe的load事件處理程序,如果你需要在響應返回時執行一些操作的話. 10 iframe.addEventListener(‘load‘, function () { 11 console.log(‘post success‘) 12 }) 13 14 form.action = url 15 // 在指定的iframe中執行form 16 form.target = iframe.name 17 form.method = ‘post‘ 18 for (let name in data) { 19 node.name = name 20 node.value = data[name].toString() 21 form.appendChild(node.cloneNode()) 22 } 23 // 表單元素需要添加到主文檔中. 24 form.style.display = ‘none‘ 25 document.body.appendChild(form) 26 form.submit() 27 28 // 表單提交後,就可以刪除這個表單,不影響下次的數據發送. 29 document.body.removeChild(form) 30 } 31 // 使用方式 32 requestPost({ 33 url: ‘http://localhost:9871/api/iframePost‘, 34 data: { 35 msg: ‘helloIframePost‘ 36 } 37 })
3、CORS
CORS是一個W3C標準,全稱是"跨域資源共享"(Cross-origin resource sharing)跨域資源共享 CORS 詳解。看名字就知道這是處理跨域問題的標準做法。
CORS需要瀏覽器和服務器同時支持,服務器需做如下設置:
3.1、Access-Control-Allow-Origin 該字段必填。它的值要麽是請求時Origin字段的具體值,要麽是一個*,表示接受任意域名的請求。例如:
ctx.set(‘Access-Control-Allow-Origin‘, ‘http://localhost:9099‘)
3.2、Access-Control-Allow-Credentials
該字段可選。它的值是一個布爾值,表示是否允許發送Cookie.默認情況下,不發生Cookie,即:false。對服務器有特殊要求的請求,比如請求方法是PUT或DELETE,或者Content-Type字段的類型是application/json,這個值只能設為true。如果服務器不要瀏覽器發送Cookie,刪除該字段即可。例如
ctx.set(‘Access-Control-Allow-Credentials‘, true)
3.3、Access-Control-Allow-Methods 該字段必填。它的值是逗號分隔的一個具體的字符串或者*,表明服務器支持的所有跨域請求的方法。註意,返回的是所有支持的方法,而不單是瀏覽器請求的那個方法。這是為了避免多次"預檢"請求。例如:
ctx.set(‘Access-Control-Request-Method‘, ‘PUT,POST,GET,DELETE,OPTIONS‘)
3.4、Access-Control-Expose-Headers 該字段可選。CORS請求時,XMLHttpRequest對象的getResponseHeader()方法只能拿到6個基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必須在Access-Control-Expose-Headers裏面指定。例如:
ctx.set(‘Access-Control-Allow-Headers‘, ‘Origin, X-Requested-With, Content-Type, Accept, t‘)
3.5、Access-Control-Max-Age
該字段可選,用來指定本次預檢請求的有效期,單位為秒。在有效期間,不用發出另一條預檢請求。
前端:
fetch(`http://localhost:9871/api/cors?msg=helloCors`, { // 需要帶上cookie credentials: ‘include‘, // 這裏添加額外的headers來觸發非簡單請求 headers: { ‘t‘: ‘extra headers‘ } }).then(res => { console.log(res) })
4、代理
我們使用Nginx作為我們的代理服務器。Nginx配置
server{ # 監聽9099端口 listen 9099; # 域名是localhost server_name localhost; #凡是localhost:9099/api這個樣子的,都轉發到真正的服務端地址http://localhost:9871 location ^~ /api { proxy_pass http://localhost:9871; } }
Nginx配置好之後,前端什麽也不用幹,正常請求就行,Nginx會把請求轉發到真正的地址。
現在,跨域對你來說,應該不是事兒了吧,可以開開心心的吃火鍋了。
關於跨域那些事兒