摘要:服務端根據這個值,決定是否同意本次請求。預檢請求預檢請求用的請求方法是,表示這個請求是用來詢問的。該字段也可以設為星號,表示同意任意跨源請求。這是為了避免多次預檢請求。
首發于個人博客目錄
跨域
簡單請求和復雜請求
服務端如何設置CORS
@koa/cors是怎么實現的
跨域 為什么會有跨域問題?這是瀏覽器的同源策略所造成的,同源策略限制了從同一個源加載的文檔或腳本如何與來自另一個源的資源進行交互。這是一個用于隔離潛在惡意文件的重要安全機制。
一定要注意跨域是瀏覽器的限制,其實你用抓包工具抓取接口數據,是可以看到接口已經把數據返回回來了,只是瀏覽器的限制,你獲取不到數據。用postman請求接口能夠請求到數據。這些再次印證了跨域是瀏覽器的限制。如何解決跨域?
jsonp: 帶有src屬性的標簽都可以用來, 但是只能處理GET請求
document.domain + iframe跨域
location.hash + iframe
window.name + iframe
postMessage跨域
Nginx配置反向代理
CORS(跨域資源共享):支持所有類型的HTTP請求
相信大家對于以上的解決方法都很熟悉,這里不再對每一種方法展開講解,接下來主要講一下CORS;
簡單請求和非簡單請求瀏覽器將CORS跨域請求分為簡單請求和非簡單請求;
如果你使用nginx反向代理解決的跨域問題,則不會有跨域請求這個說法了,因為nginx反向代理就使得前后端是同一個域了,就不存在跨域問題了。
只要同時滿足一下兩個條件,就屬于簡單請求
(1)使用下列方法之一:
head
get
post
(2)請求的Heder是
Accept
Accept-Language
Content-Language
Content-Type: 只限于三個值:
application/x-www-form-urlencoded
multipart/form-data
text/plain
不同時滿足上面的兩個條件,就屬于非簡單請求。
瀏覽器對這兩種的處理,是不一樣的。
對于簡單請求,瀏覽器直接發出CORS請求。具體來說,就是頭信息之中,增加一個Origin字段。
上面這個例子,post請求,Content-Type為application/x-www-form-urlencoded,滿足簡單請求的條件;響應頭部返回Access-Control-Allow-Origin: http://127.0.0.1:3000;
瀏覽器發現這次跨域請求是簡單請求,就自動在頭信息之中,添加一個Origin字段;Origin字段用來說明請求來自哪個源(協議+域名+端口號)。服務端根據這個值,決定是否同意本次請求。
Access-Control-Allow-Origin:必選
請求頭Origin字段的值
*:接受任何域名
Access-Control-Allow-Credentials:可選,
true: 表示允許發送cookie,此時Access-Control-Allow-Origin不能設置為*,必須指定明確的,與請求網頁一致的域名。
不設置該字段:不需要瀏覽器發送cookie
Access-Control-Expose-Headers:可選
響應報頭指示哪些報頭可以公開為通過列出他們的名字的響應的一部分。默認情況下,只顯示6個簡單的響應標頭:
Cache-Control
Content-Language
Content-Type
Expires
Last-Modified
Pragma
如果想要讓客戶端可以訪問到其他的首部信息,可以將它們在 Access-Control-Expose-Headers 里面列出來。
withCredentials 屬性CORS請求默認不發送Cookie和HTTP認證信息,如果要把Cookie發到服務器,一方面需要服務器同意,設置響應頭Access-Control-Allow-Credentials: true,另一方面在客戶端發出請求的時候也要進行一些設置;
// XHR var xhr = new XMLHttpRequest(); xhr.open("GET", "http://example.com/", true); xhr.withCredentials = true; xhr.send(null); // Fetch fetch(url, { credentials: "include" })非簡單請求
非簡單請求就是那種對服務器有特殊要求的請求,比如請求方法為PUT或DELETE,或者Content-Type字段為application/json;
1. 預檢請求和回應非簡單請求的CORS請求,會在正式通信之前,增加一次HTTP查詢請求,稱為“預檢”請求;
瀏覽器先詢問服務器,當前網頁所在的域名是否在服務器的許可名單之中,以及可以使用哪些HTTP動詞和頭信息字段,只有得到肯定答復,瀏覽器才會發出正式的接口請求,否則就會報錯;
HTTP請求的方法是POST,請求頭Content-Type字段為application/json。瀏覽器發現,這是一個非簡單請求,就自動發出一個預檢請求,要求服務器確認可以這樣請求。
1.1預檢請求預檢請求用的請求方法是OPTIONS,表示這個請求是用來詢問的。頭信息里面,關鍵字段是Origin,表示請求來自哪個域。
除了Origin,預檢請求的頭信息包括兩個特殊字段:
Access-Control-Request-Method: 必選,用來列出瀏覽器的CORS請求會用到哪些HTTP方法,上例是POST
Access-Control-Request-Headers:該字段是一個用逗號分割的字符串,執行瀏覽器CORS請求會額外發送的頭信息字段,上例是Content-Type;
1.2預檢回應服務器收到預檢請求以后,檢查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段以后,確認允許跨域請求,就可以做出回應。
上面的HTTP回應中,關鍵的是Access-Control-Allow-Origin字段,表示http://127.0.0.1:3000可以請求數據。該字段也可以設為星號,表示同意任意跨源請求。
如果瀏覽器否定了“預檢”請求,就會返回一個正常的HTTP回應,但是沒有任何CORS相關的頭信息字段,這時,瀏覽器就會認定,服務器不同意預檢請求,因此觸發一個錯誤,被XMLHttpRequest對象的onerror回調函數捕獲h。
服務器回應的其他CORS字段
Access-Control-Allow-Methods:必需;它的值是逗號分隔的一個字符串,表明服務器支持的所有跨域請求的方法。注意,返回的是所有支持的方法,而不單是瀏覽器請求的方法。這是為了避免多次預檢請求。
Access-Control-Allow-Headers:如果瀏覽器請求頭里包括Access-Control-Request-Headers字段,則Access-Control-Allow-Headers字段是必需的。它也是一個逗號分隔的字符串,表明服務器支持的所有頭信息字段,不限于瀏覽器在預檢中請求的字段。
Access-Control-Allow-Credentials:與簡單請求時含義相同。
Access-Control-Allow-Max-Age: 可選,用來指定本次預檢請求的有效期。單位為秒。在有效期內,不用發出另一條預檢請求
2.正常請求和回應一旦服務器通過了預檢請求,以后每次瀏覽器正常的CORS請求,就都跟簡單請求一樣,會有一個Origin頭信息字段。服務器的回應,也都會有一個Access-Control-Allow-Origin頭信息字段;
服務端如何設置CORS 多帶帶接口多帶帶處理比如一個簡單的登錄頁面,需要給接口接口傳入 username和password 兩個字段;前端的域名為 localhost:8900,后端的域名為 localhost:3200,構成跨域。
1. 如果設置請求頭"Content-Type": "application/x-www-form-urlencoded",這種情況則為簡單請求;會有跨域問題,直接設置 響應頭 Access-Control-Allow-Origin為*, 或者具體的域名;注意如果設置響應頭Access-Control-Allow-Credentials為true,表示要發送cookie,則此時Access-Control-Allow-Origin的值不能設置為星號,必須指定明確的,與請求網頁一致的域名。
const login = ctx => { const req = ctx.request.body; const userName = req.userName; ctx.set("Access-Control-Allow-Origin", "*"); ctx.response.body = { data: {}, msg: "登陸成功" }; }2. 如果設置請求頭"Content-Type": "application/json",這種情況則為非簡單請求
處理OPTIONS請求,服務端可以多帶帶寫一個路由,來處理login的OPTIONS的請求
app.use(route.options("/login", ctx => { ctx.set("Access-Control-Allow-Origin", "*"); ctx.set("Access-Control-Allow-Headers", "Content-Type"); ctx.status = 204; }));
大家都知道前端調用服務端的時候,會調用很多個接口,并且每個接口處理跨域請求的邏輯是完全一樣的,我們可以把這部分抽離出來,作為一個中間件;
寫一個中間件進行處理 首先了解一下koa中間件的“洋蔥圈”模型將洋蔥的一圈看做是一個中間件,直線型就是從第一個中間件走到最后一個,但是洋蔥圈就很特殊了,最早use的中間件在洋蔥最外層,開始的時候會按照順序走到所有中間件,然后按照倒序再走一遍所有的中間件,相當于每個中間件都會進入兩次,這就給了我們更多的操作空間。
const Koa = require("koa"); const app = new Koa(); app.use((ctx, next) => { console.log("a - 1"); next(); console.log("a - 2"); }) app.use((ctx, next) => { console.log("b - 1"); next(); console.log("b - 2"); }) app.use((ctx, next) => { console.log("c - 1"); next(); console.log("c - 2"); }) app.listen(3200, () => { console.log("啟動成功"); });
輸出
a - 1 b - 1 c - 1 c - 2 b - 2 a - 2
Koa官方文檔上把外層的中間件稱為“上游”,內層的中間件為“下游”。
一般的中間件都會執行兩次,調用next之前為一次,調用next時把控制按順序傳遞給下游的中間件。當下游不再有中間件或者中間件沒有執行 next 函數時,就將依次恢復上游中間件的行為,讓上游中間件執行 next之后的代碼;
const Koa = require("koa"); const app = new Koa(); const route = require("koa-route"); var bodyParser = require("koa-bodyparser"); app.use(bodyParser()); // 處理post請求的參數 const login = ctx => { const req = ctx.request.body; const userName = req.userName; const expires = Date.now() + 3600000; // 設置超時時間為一小時后 var payload = { iss: userName, exp: expires }; const Token = jwt.encode(payload, secret); ctx.response.body = { data: Token, msg: "登陸成功" }; } // 將公共邏輯方法放到中間件中處理 app.use((ctx, next)=> { const headers = ctx.request.headers; if(ctx.method === "OPTIONS") { ctx.set("Access-Control-Allow-Origin", "*"); ctx.set("Access-Control-Allow-Headers", "Authorization"); ctx.status = 204; } else { next(); } }) app.use(route.post("/login", login)); app.listen(3200, () => { console.log("啟動成功"); });
上述示例代碼地址
@koa/cors是怎么實現的"use strict"; const vary = require("vary"); /** * CORS middleware * * @param {Object} [options] * - {String|Function(ctx)} origin `Access-Control-Allow-Origin`, default is request Origin header * - {String|Array} allowMethods `Access-Control-Allow-Methods`, default is "GET,HEAD,PUT,POST,DELETE,PATCH" * - {String|Array} exposeHeaders `Access-Control-Expose-Headers` * - {String|Array} allowHeaders `Access-Control-Allow-Headers` * - {String|Number} maxAge `Access-Control-Max-Age` in seconds * - {Boolean} credentials `Access-Control-Allow-Credentials` * - {Boolean} keepHeadersOnError Add set headers to `err.header` if an error is thrown * @return {Function} cors middleware * @api public */ module.exports = function (options) { const defaults = { allowMethods: "GET,HEAD,PUT,POST,DELETE,PATCH", }; // 默認的配置項和使用時設置的options進行一個融合 options = Object.assign({}, defaults, options); // 因為函數的一些參數,exposeHeaders,allowMethods,allowHeaders的形式既可以是String,也可以是Array類型, // 如果是Array類型,也轉換為用逗號分隔的字符串。 if (Array.isArray(options.exposeHeaders)) { options.exposeHeaders = options.exposeHeaders.join(","); } if (Array.isArray(options.allowMethods)) { options.allowMethods = options.allowMethods.join(","); } if (Array.isArray(options.allowHeaders)) { options.allowHeaders = options.allowHeaders.join(","); } if (options.maxAge) { options.maxAge = String(options.maxAge); } options.credentials = !!options.credentials; options.keepHeadersOnError = options.keepHeadersOnError === undefined || !!options.keepHeadersOnError; return async function cors(ctx, next) { // If the Origin header is not present terminate this set of steps. // The request is outside the scope of this specification. const requestOrigin = ctx.get("Origin"); // Always set Vary header // https://github.com/rs/cors/issues/10 ctx.vary("Origin"); // 如果請求頭不存在 origin,則直接跳出該中間件,執行下一個中間件 if (!requestOrigin) return await next(); // 對origin參數的不同類型做一個處理 let origin; if (typeof options.origin === "function") { origin = options.origin(ctx); if (origin instanceof Promise) origin = await origin; if (!origin) return await next(); } else { origin = options.origin || requestOrigin; } const headersSet = {}; function set(key, value) { ctx.set(key, value); headersSet[key] = value; } /** * 非OPTIONS請求的處理 * */ if (ctx.method !== "OPTIONS") { // Simple Cross-Origin Request, Actual Request, and Redirects set("Access-Control-Allow-Origin", origin); if (options.credentials === true) { set("Access-Control-Allow-Credentials", "true"); } if (options.exposeHeaders) { set("Access-Control-Expose-Headers", options.exposeHeaders); } if (!options.keepHeadersOnError) { return await next(); } try { return await next(); } catch (err) { const errHeadersSet = err.headers || {}; const varyWithOrigin = vary.append(errHeadersSet.vary || errHeadersSet.Vary || "", "Origin"); delete errHeadersSet.Vary; err.headers = Object.assign({}, errHeadersSet, headersSet, { vary: varyWithOrigin }); throw err; } } else { // Preflight Request // If there is no Access-Control-Request-Method header or if parsing failed, // do not set any additional headers and terminate this set of steps. // The request is outside the scope of this specification. if (!ctx.get("Access-Control-Request-Method")) { // this not preflight request, ignore it return await next(); } ctx.set("Access-Control-Allow-Origin", origin); if (options.credentials === true) { ctx.set("Access-Control-Allow-Credentials", "true"); } if (options.maxAge) { ctx.set("Access-Control-Max-Age", options.maxAge); } if (options.allowMethods) { ctx.set("Access-Control-Allow-Methods", options.allowMethods); } let allowHeaders = options.allowHeaders; if (!allowHeaders) { allowHeaders = ctx.get("Access-Control-Request-Headers"); } if (allowHeaders) { ctx.set("Access-Control-Allow-Headers", allowHeaders); } ctx.status = 204; } }; };
以上是 @koa/cors V3.0.0的源碼實現,如果你真正理解的CORS,看源碼的邏輯就會非常輕松。
主要是分兩個邏輯來處理,有預檢請求的和沒有預檢請求的。
對于非OPTIONS請求的處理,要根據情況加上 Access-Control-Allow-Origin,Access-Control-Allow-Credentials,Access-Control-Expose-Headers這三個響應頭部;
對于OPTIONS請求(預檢請求)的處理,要根據情況加上 Access-Control-Allow-Origin,Access-Control-Allow-Credentials,Access-Control-Max-Age,Access-Control-Allow-Methods,Access-Control-Allow-Headers這幾個響應頭部;
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/104954.html
摘要:介紹使用搭建服務并連接返回前端數據項目初始化首先保證你的環境已經就緒創建項目文件夾創建文件夾,在文件夾中右鍵在此處運行命令行運行安裝依賴創建服務在項目文件中創建一個文件在項目跟目錄運行瀏覽器地址輸入返回表示一次對話的上下文包括請求 介紹 使用koa搭建node服務 并連接mongodb返回前端數據git https://gitee.com/wjj0720/koa... 項目初始化 首...
摘要:用搭建前端項目用搭建后臺,給前端提供數據訪問接口項目結構用搭建的項目,紅色框中是新建的文件夾用于存放剩下的文件在寫項目中慢慢增加,最初就是這樣的之后將項目跑起來,看一下有沒有問題這里就當作沒有問題前端這里選用和搭配這里采用的是的完整 koa2+vue 用vue-cli搭建前端項目 用koa2搭建后臺,給前端提供數據訪問接口 項目結構 showImg(https://segmentf...
摘要:起因運營人員需要將后臺的表格導出成,由于后端的同學忙于其他事情,想著是不是可以自己做一個服務來生成。另外再搭配就可以提供一個允許跨域請求的服務。這樣一個簡單的接口就寫完了,只要調用傳入和就可以生成文檔。 起因 運營人員需要將后臺的表格導出成Excel,由于后端的同學忙于其他事情,想著是不是可以自己做一個服務來生成。了解到有node-xlsx這樣的工具以后就開工了。 框架 后臺選用了ko...
摘要:上說你可以使用構造函數創建一個新的對象。使用對象完成與請求的通信。服務端使用重要的點在于不能直接使用這樣返回給前端會直接報錯。前端的代碼要注意的第三個參數設置成將請求設置為異步,然后由于超時會取消請求,所以這里根本不需要來顯式的取消請求 MDN上說: 你可以使用AbortController.AbortController()構造函數創建一個新的AbortController對象。 使...
閱讀 3948·2021-09-24 10:24
閱讀 1386·2021-09-22 16:01
閱讀 2713·2021-09-06 15:02
閱讀 1013·2019-08-30 13:01
閱讀 1002·2019-08-30 10:52
閱讀 632·2019-08-29 16:36
閱讀 2231·2019-08-29 12:51
閱讀 2333·2019-08-28 18:29