首發於我的博客前端
這是瀏覽器的同源策略所形成的,同源策略限制了從同一個源加載的文檔或腳本如何與來自另外一個源的資源進行交互。這是一個用於隔離潛在惡意文件的重要安全機制。nginx
必定要注意跨域是瀏覽器的限制,其實你用抓包工具抓取接口數據,是能夠看到接口已經把數據返回回來了,只是瀏覽器的限制,你獲取不到數據。用postman請求接口可以請求到數據。這些再次印證了跨域是瀏覽器的限制。git
瀏覽器將CORS跨域請求分爲簡單請求和非簡單請求;github
若是你使用nginx反向代理解決的跨域問題,則不會有跨域請求這個說法了,由於nginx反向代理就使得先後端是同一個域了,就不存在跨域問題了。json
只要同時知足一下兩個條件,就屬於簡單請求 (1)使用下列方法之一:後端
(2)請求的Heder是api
不一樣時知足上面的兩個條件,就屬於非簡單請求。 瀏覽器對這兩種的處理,是不同的。跨域
對於簡單請求,瀏覽器直接發出CORS請求。具體來講,就是頭信息之中,增長一個Origin字段。 瀏覽器
post
請求,
Content-Type
爲
application/x-www-form-urlencoded
,知足簡單請求的條件;響應頭部返回
Access-Control-Allow-Origin: http://127.0.0.1:3000
; 瀏覽器發現此次跨域請求是簡單請求,就自動在頭信息之中,添加一個
Origin
字段;
Origin
字段用來講明請求來自哪一個源(協議+域名+端口號)。服務端根據這個值,決定是否贊成本次請求。
Access-Control-
開頭Origin
字段的值*
:接受任何域名Access-Control-Allow-Origin
不能設置爲*
,必須指定明確的,與請求網頁一致的域名。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
;
非簡單請求的CORS請求,會在正式通訊以前,增長一次HTTP查詢請求,稱爲「預檢」請求; 瀏覽器先詢問服務器,當前網頁所在的域名是否在服務器的許可名單之中,以及可使用哪些HTTP動詞和頭信息字段,只有獲得確定答覆,瀏覽器纔會發出正式的接口請求,不然就會報錯;
HTTP請求的方法是POST,請求頭Content-Type
字段爲application/json
。瀏覽器發現,這是一個非簡單請求,就自動發出一個預檢
請求,要求服務器確承認以這樣請求。
預檢
請求用的請求方法是OPTIONS,表示這個請求是用來詢問的。頭信息裏面,關鍵字段是Origin
,表示請求來自哪一個域。 除了Origin
,預檢
請求的頭信息包括兩個特殊字段:
POST
Content-Type
;服務器收到預檢
請求之後,檢查了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-Request-Headers
字段,則Access-Control-Allow-Headers
字段是必需的。它也是一個逗號分隔的字符串,代表服務器支持的全部頭信息字段,不限於瀏覽器在預檢
中請求的字段。一旦服務器經過了預檢
請求,之後每次瀏覽器正常的CORS請求,就都跟簡單請求同樣,會有一個Origin
頭信息字段。服務器的迴應,也都會有一個Access-Control-Allow-Origin
頭信息字段;
好比一個簡單的登陸頁面,須要給接口接口傳入 username和password 兩個字段;前端的域名爲 localhost:8900,後端的域名爲 localhost:3200,構成跨域。
'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: '登錄成功'
};
}
複製代碼
'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;
}));
複製代碼
你們都知道前端調用服務端的時候,會調用不少個接口,而且每一個接口處理跨域請求的邏輯是徹底同樣的,咱們能夠把這部分抽離出來,做爲一箇中間件;
將洋蔥的一圈看作是一箇中間件,直線型就是從第一個中間件走到最後一個,可是洋蔥圈就很特殊了,最先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('啓動成功');
});
複製代碼
上述示例代碼地址
'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
這幾個響應頭部;