CORS(Cross-Origin Resource Sharing, 跨源資源共享)是W3C出的一個標准,其思想是使用自定義的HTTP頭部讓瀏覽器與服務器進行溝通,從而決定請求或響應是應該成功,還是應該失敗。因此,要想實現CORS進行跨域,需要服務器進行一些設置,同時前端也需要做一些配置和分析。本文簡單的對服務端的配置和前端的一些設置進行分析。
服務端的配置
本文服務端的代碼采用的是node,使用koa,koa-router和koa2-cors。
配置的主要代碼如下:
app.use(cors({ origin: function(ctx) { const regexp = new RegExp('/CORS'); const regexpWith = new RegExp('/CORSWith'); if (regexpWith.test(ctx.url)) { return `http://${packageData.url}:7000`; } else if(regexp.test(ctx.url)) { return '*' } else if(~String(ctx.url).indexOf('/imgs/')) { return `http://${packageData.url}:7000`; } return false; }, exposeHeaders: ['WWW-Authenticate', 'Server-Authorization', 'Date'], maxAge: 100, credentials: true, allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], allowHeaders: ['Content-Type', 'Authorization', 'Accept', 'X-Custom-Header', 'anonymous'], }));
主要是使用了koa2-cors進行了配置,下面對配置項進行簡單介紹:
origin:
設置Access-Control-Allow-Origin,源碼如下:
Access-Control-Allow-Origin表示允許跨域的域名,可以設置為*也可以設置為具體的域,其中,*表示全部,即所有的域名下的請求都允許,但設置為*后,所有的請求都不會攜帶附帶身份憑證(比如cookie);設置為具體的域則表示只有該域下的請求允許,別的域下的請求不被允許,設置為具體的域是請求中攜帶身份憑證的基礎。
exposeHeaders:
設置Access-Control-Expose-Headers,源碼如下:
Access-Control-Expose-Headers表示允許腳本訪問的返回頭,請求成功后,腳本可以在XMLHttpRequest中訪問這些頭的信息,如在上面配置的代碼中設置了Date,在瀏覽器端就可以通過js拿到服務器的時間了,下面是通過XMLHttpRequest實例的getResponseHeader()獲取服務器時間的代碼:
let xhr = new XMLHttpRequest(); xhr.open('GET', `http://${url}/CORS/userInfo/12`, true); xhr.onload = function() { if(xhr.readyState == 4) { try { // 獲取服務器時間 console.log(xhr.getResponseHeader('Date')); } catch(ex) { new Error(ex); } } }; xhr.send();
maxAge:
設置Access-Control-Max-Age,源碼如下:
Access-Control-Max-Age用來指定本次預檢請求的有效期,單位為秒,預檢請求將在下面具體介紹,現在先過。
credentials:
設置Access-Control-Allow-Credentials,源碼如下:
Access-Control-Allow-Credentials為服務端標識瀏覽器請求CORS時是否可以附帶身份憑證,對於附帶身份憑證的請求,服務器不得設置 Access-Control-Allow-Origin 的值為“*”。
allowMethods:
設置Access-Control-Allow-Methods,源碼如下:
Access-Control-Allow-Methods用來設置檢查網絡請求的方式,如GET、POST等。
allowHeaders:
設置Access-Control-Request-Headers,源碼如下:
Access-Control-Request-Headers用來將實際請求所攜帶的首部字段告訴服務器,在這里可以自定義頭部信息,用來對瀏覽器的非簡單請求進行預檢判斷。
前端的配置
前端的配置主要通過簡單請求和非簡單請求,攜帶身份憑證,canvas中畫圖使用的跨域圖片三部分進行講解
簡單請求和非簡單請求
瀏覽器將CORS請求分成兩類:簡單請求(simple request)和非簡單請求(not-so-simple request)。
簡單請求:
簡單請求滿足以下條件:
1. 使用下列方法之一:
- GET
- HEAD
- POST
2.HTTP的頭信息不超出以下幾種字段:
- Accept
- Accept-Language
- Content-Language
- Content-Type:值屬於下列之一:
application/x-www-form-urlencoded
multipart/form-data
text/plain
簡單請求如圖所示:
瀏覽器與服務器之間請求只進行了一次。
非簡單請求:
不滿足簡單請求條件的請求則要先進行預檢請求,即使用OPTIONS方法發起一個預檢請求到服務器,已獲知服務器是否允許該實際請求。
非簡單請求如下所示:
下面是PUT請求第一次返回的結果:
通過PUT請求結果可以看出,當檢測到PUT請求為非簡單請求時,瀏覽器便會發送一個預檢請求,目的是詢問,自定義頭部X-Custom-Header的PUT請求是否被允許,瀏覽器返回了所有可以請求的方法和自定義的頭部(把所有可以的返回是為了避免多次預檢請求),這時候預檢請求成功了,便會發送真正的PUT請求。
關於預檢請求,需要注意一下兩點:
- 預檢請求對js來說是透明的,js獲取不到預檢請求的任何信息。
- 預檢請求並不是每次請求都發生,服務端設置的Access-Control-Max-Age頭部指定了預檢請求的有效期,在有效期內的非簡單請求不需要再次發生預檢請求。
攜帶身份憑證
大部分的請求是需要用戶攜帶着用戶信息的,比如在一個登錄的系統中,用戶會攜帶着相應的cookie或token,但CORS跨域默認是不帶身份憑證的。
如果需要附帶身份憑證,在發送請求時,通過將withCredentials屬性設置為true,可以指定某個請求可以發送憑據。
下面提供針對XMLHttpRequest附帶身份憑證的兼容性寫法:
function createCORSRequest(method, url) { var xhr = new XMLHttpRequest(); xhr.onload = function() { if(xhr.readyState == 4) { try { if((xhr.status >= 200 && xhr.status < 300) || xhr == 304) { console.log(xhr.response); } else { console.log('Request was unsuccessful: ' + xhr.status); } } catch(ex) { new Error(ex); } } }; if('withCredentials' in xhr) { xhr.open(method,url, true); } else if(typeof XDomainRequest != 'undefined') { xhr = new XDomainRequest(); xhr.open(method, url); } else { xhr = null; } return xhr; }
附帶身份憑證對服務端有兩個要求:
- 服務端的Access-Control-Allow-Origin頭部不能設置為*
- 服務端的Access-Control-Allow-Credentials頭部設置為true
canvas中畫圖使用的跨域圖片
盡管不通過 CORS 就可以在畫布中使用圖片,但是這會污染畫布。一旦畫布被污染,你就無法讀取其數據。例如,你不能再使用畫布的 toBlob(), toDataURL() 或 getImageData() 方法,調用它們會拋出安全錯誤。
這種機制可以避免未經許可拉取遠程網站信息而導致的用戶隱私泄露。
對跨域圖片進行修改的話,img需要添加crossOrigin屬性,代碼如下:
function drawCanvas(id, drawId, url) { let canvas = document.getElementById(id); let ctx = canvas.getContext('2d'); var img = document.createElement('img'); img.src = url; img.crossOrigin = 'anonymous'; // 必須等到圖片完全加載后才能對其進行操作。瀏覽器通常會在頁面腳本執行的同時異步加載圖片。如果試圖在圖片未完全加載之前就將其呈現到canvas上,那么canvas將不會顯示任何圖片 if (img.complete) { canvas.width = img.width; canvas.height = img.height; ctx.drawImage(img, 0, 0 ); } else { img.onload = function () { canvas.width = img.width; canvas.height = img.height; ctx.drawImage(img, 0, 0 ); const src = canvas.toDataURL(" image/jpeg", 0.3); $(drawId).attr('src', src); }; } }
服務器針對跨域修改的圖片也要做兩點限制:
- 服務端的Access-Control-Allow-Origin頭部不能設置為*
- 服務端的Access-Control-Request-Headers頭部添加一個自定義頭部,其值為img的crossOrigin的值
常見的圖片跨域修改的錯誤有兩種:
- Uncaught SecurityError: Failed to execute 'toDataURL' on 'HTMLCanvasElement': Tainted canvases may not be exported.
-
Image from origin 'http://127.0.0.1:4000' has been blocked from loading by Cross-Origin Resource Sharing policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://127.0.0.1:7000' is therefore not allowed access.
總結
至此,關於CORS的一些知識點已經分享完了,總結如下:
CORS是瀏覽器和服務器配合完成的跨域請求,個人認為,主要是服務端配置好后,瀏覽器根據服務端配置的自定義頭部和提供的可以進行的CORS的方法來進行跨域操作。
基於以上總結,提供測試Demo:
https://github.com/weiruifeng/fetchTest
參考資料:
HTTP訪問控制(CORS): https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS