Node.js設(shè)置CORS跨域請(qǐng)求中多域名白名單的方法
CORS
說到CORS,相信前端兒都不陌生,這里我就不多說了,具體可以看看這篇文章。
CORS,主要就是配置Response響應(yīng)頭中的 Access-Control-Allow-Origin 屬性為你允許該接口訪問的域名。最常見的設(shè)置是:
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Credentials', 'true'); // 允許服務(wù)器端發(fā)送Cookie數(shù)據(jù)
然而,這樣的設(shè)置是最簡單粗暴,同時(shí)也是最不安全的。它表示該接口允許所有的域名對(duì)它進(jìn)行跨域請(qǐng)求。然而,在一般實(shí)際業(yè)務(wù)中,都希望該接口只允許對(duì)某一個(gè)或幾個(gè)網(wǎng)站開放跨域請(qǐng)求權(quán)限,而非全部。
那么,聰明的你肯定想著,多域名白名單還不簡單嗎,寫個(gè)正則就好啦?再不行,直接配置 Access-Control-Allow-Origin 屬性為用逗號(hào)分隔的多個(gè)域名不就好了嗎?
就像下面這樣:
res.header('Access-Control-Allow-Origin', '*.666.com');
// 或者如下
res.header('Access-Control-Allow-Origin', 'a.666.com,b.666.com,c.666.com');
很遺憾地告訴你,這樣的寫法是無效的。在Node.js中,res的響應(yīng)頭Header中的 Access-Control-Allow-Origin 屬性不能匹配除 (*) 以外的正則表達(dá)式的,域名之間不能也用逗號(hào)分隔。也就是說, Access-Control-Allow-Origin 的屬性值只允許設(shè)置為單個(gè)確定域名字符串或者 (*)。
既然我們希望允許的是多個(gè)域名,也不愿意使用不安全的 * 通配符,難道就真不能配置多域名白名單的CORS了嗎?
多域名白名單的CORS確實(shí)是可以實(shí)現(xiàn)的。只是有一點(diǎn)曲線救國的味道。
多域名白名單的CORS實(shí)現(xiàn)原理
具體原理可以參考cors庫的核心代碼:
(function () {
'use strict';
var assign = require('object-assign');
var vary = require('vary');
var defaults = {
origin: '*',
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
preflightContinue: false,
optionsSuccessStatus: 204
};
function isString(s) {
return typeof s === 'string' || s instanceof String;
}
function isOriginAllowed(origin, allowedOrigin) {
if (Array.isArray(allowedOrigin)) {
for (var i = 0; i < allowedOrigin.length; ++i) {
if (isOriginAllowed(origin, allowedOrigin[i])) {
return true;
}
}
return false;
} else if (isString(allowedOrigin)) {
return origin === allowedOrigin;
} else if (allowedOrigin instanceof RegExp) {
return allowedOrigin.test(origin);
} else {
return !!allowedOrigin;
}
}
function configureOrigin(options, req) {
var requestOrigin = req.headers.origin,
headers = [],
isAllowed;
if (!options.origin || options.origin === '*') {
// allow any origin
headers.push([{
key: 'Access-Control-Allow-Origin',
value: '*'
}]);
} else if (isString(options.origin)) {
// fixed origin
headers.push([{
key: 'Access-Control-Allow-Origin',
value: options.origin
}]);
headers.push([{
key: 'Vary',
value: 'Origin'
}]);
} else {
isAllowed = isOriginAllowed(requestOrigin, options.origin);
// reflect origin
headers.push([{
key: 'Access-Control-Allow-Origin',
value: isAllowed ? requestOrigin : false
}]);
headers.push([{
key: 'Vary',
value: 'Origin'
}]);
}
return headers;
}
function configureMethods(options) {
var methods = options.methods;
if (methods.join) {
methods = options.methods.join(','); // .methods is an array, so turn it into a string
}
return {
key: 'Access-Control-Allow-Methods',
value: methods
};
}
function configureCredentials(options) {
if (options.credentials === true) {
return {
key: 'Access-Control-Allow-Credentials',
value: 'true'
};
}
return null;
}
function configureAllowedHeaders(options, req) {
var allowedHeaders = options.allowedHeaders || options.headers;
var headers = [];
if (!allowedHeaders) {
allowedHeaders = req.headers['access-control-request-headers']; // .headers wasn't specified, so reflect the request headers
headers.push([{
key: 'Vary',
value: 'Access-Control-Request-Headers'
}]);
} else if (allowedHeaders.join) {
allowedHeaders = allowedHeaders.join(','); // .headers is an array, so turn it into a string
}
if (allowedHeaders && allowedHeaders.length) {
headers.push([{
key: 'Access-Control-Allow-Headers',
value: allowedHeaders
}]);
}
return headers;
}
function configureExposedHeaders(options) {
var headers = options.exposedHeaders;
if (!headers) {
return null;
} else if (headers.join) {
headers = headers.join(','); // .headers is an array, so turn it into a string
}
if (headers && headers.length) {
return {
key: 'Access-Control-Expose-Headers',
value: headers
};
}
return null;
}
function configureMaxAge(options) {
var maxAge = options.maxAge && options.maxAge.toString();
if (maxAge && maxAge.length) {
return {
key: 'Access-Control-Max-Age',
value: maxAge
};
}
return null;
}
function applyHeaders(headers, res) {
for (var i = 0, n = headers.length; i < n; i++) {
var header = headers[i];
if (header) {
if (Array.isArray(header)) {
applyHeaders(header, res);
} else if (header.key === 'Vary' && header.value) {
vary(res, header.value);
} else if (header.value) {
res.setHeader(header.key, header.value);
}
}
}
}
function cors(options, req, res, next) {
var headers = [],
method = req.method && req.method.toUpperCase && req.method.toUpperCase();
if (method === 'OPTIONS') {
// preflight
headers.push(configureOrigin(options, req));
headers.push(configureCredentials(options, req));
headers.push(configureMethods(options, req));
headers.push(configureAllowedHeaders(options, req));
headers.push(configureMaxAge(options, req));
headers.push(configureExposedHeaders(options, req));
applyHeaders(headers, res);
if (options.preflightContinue ) {
next();
} else {
res.statusCode = options.optionsSuccessStatus || defaults.optionsSuccessStatus;
res.end();
}
} else {
// actual response
headers.push(configureOrigin(options, req));
headers.push(configureCredentials(options, req));
headers.push(configureExposedHeaders(options, req));
applyHeaders(headers, res);
next();
}
}
function middlewareWrapper(o) {
if (typeof o !== 'function') {
o = assign({}, defaults, o);
}
// if options are static (either via defaults or custom options passed in), wrap in a function
var optionsCallback = null;
if (typeof o === 'function') {
optionsCallback = o;
} else {
optionsCallback = function (req, cb) {
cb(null, o);
};
}
return function corsMiddleware(req, res, next) {
optionsCallback(req, function (err, options) {
if (err) {
next(err);
} else {
var originCallback = null;
if (options.origin && typeof options.origin === 'function') {
originCallback = options.origin;
} else if (options.origin) {
originCallback = function (origin, cb) {
cb(null, options.origin);
};
}
if (originCallback) {
originCallback(req.headers.origin, function (err2, origin) {
if (err2 || !origin) {
next(err2);
} else {
var corsOptions = Object.create(options);
corsOptions.origin = origin;
cors(corsOptions, req, res, next);
}
});
} else {
next();
}
}
});
};
}
// can pass either an options hash, an options delegate, or nothing
module.exports = middlewareWrapper;
}());
實(shí)現(xiàn)原理是這樣的:
既然 Access-Control-Allow-Origin 屬性已經(jīng)明確不能設(shè)置多個(gè)域名,那么我們只得放棄這條路了。
最流行也是最有效的方法就是,在服務(wù)器端判斷請(qǐng)求的Header中Origin屬性值(req.header.origin)是否在我們的域名白名單列表內(nèi)。如果在白名單列表內(nèi),那么我們就把 Access-Control-Allow-Origin 設(shè)置成當(dāng)前的Origin值,這樣就滿足了Access-Control-Allow-Origin 的單一域名要求,也能確保當(dāng)前請(qǐng)求通過訪問;如果不在白名單列表內(nèi),則返回錯(cuò)誤信息。
這樣,我們就把跨域請(qǐng)求的驗(yàn)證,從瀏覽器端轉(zhuǎn)移到服務(wù)端來了。對(duì)Origin字符串的驗(yàn)證就變成了相當(dāng)于常規(guī)字符串的驗(yàn)證,我們不僅可以使用數(shù)組列表驗(yàn)證,還可以使用正則匹配。
具體代碼如下:
// 判斷origin是否在域名白名單列表中
function isOriginAllowed(origin, allowedOrigin) {
if (_.isArray(allowedOrigin)) {
for(let i = 0; i < allowedOrigin.length; i++) {
if(isOriginAllowed(origin, allowedOrigin[i])) {
return true;
}
}
return false;
} else if (_.isString(allowedOrigin)) {
return origin === allowedOrigin;
} else if (allowedOrigin instanceof RegExp) {
return allowedOrigin.test(origin);
} else {
return !!allowedOrigin;
}
}
const ALLOW_ORIGIN = [ // 域名白名單
'*.233.666.com',
'hello.world.com',
'hello..*.com'
];
app.post('a/b', function (req, res, next) {
let reqOrigin = req.headers.origin; // request響應(yīng)頭的origin屬性
// 判斷請(qǐng)求是否在域名白名單內(nèi)
if(isOriginAllowed(reqOrigin, ALLOW_ORIGIN)) {
// 設(shè)置CORS為請(qǐng)求的Origin值
res.header("Access-Control-Allow-Origin", reqOrigin);
res.header('Access-Control-Allow-Credentials', 'true');
// 你的業(yè)務(wù)代碼邏輯代碼 ...
// ...
} else {
res.send({ code: -2, msg: '非法請(qǐng)求' });
}
});
Oh yeah,簡直完美~
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作能帶來一定的幫助,如果有疑問大家可以留言交流,謝謝大家對(duì)腳本之家的支持。
相關(guān)文章
從零開始學(xué)習(xí)Node.js系列教程二:文本提交與顯示方法
這篇文章主要介紹了Node.js文本提交與顯示方法,結(jié)合實(shí)例形式分析了nodejs基于http的文本提交、傳輸與顯示相關(guān)操作技巧,需要的朋友可以參考下2017-04-04
node.js實(shí)現(xiàn)websocket的即時(shí)通訊詳解
這篇文章主要介紹了深入淺出講解websocket的即時(shí)通訊,服務(wù)器可以主動(dòng)向客戶端推送信息,客戶端也可以主動(dòng)向服務(wù)器發(fā)送信息,是真正的雙向平等對(duì)話,屬于服務(wù)器推送技術(shù)的一種,需要的朋友可以參考下2023-05-05
nodejs實(shí)現(xiàn)OAuth2.0授權(quán)服務(wù)認(rèn)證
本篇文章主要介紹了nodejs實(shí)現(xiàn)OAuth2.0授權(quán)服務(wù)認(rèn)證,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-12-12
Nodejs Playwright 2Captcha 驗(yàn)證碼識(shí)別實(shí)現(xiàn)自動(dòng)登陸功能
日常工作當(dāng)中,為了提高工作效率,我們可能會(huì)寫腳本來自動(dòng)執(zhí)行任務(wù),有些網(wǎng)站因?yàn)樾枰脩舻顷?所以腳本的自動(dòng)登陸功能必不可少,這篇文章主要介紹了Nodejs Playwright 2Captcha 驗(yàn)證碼識(shí)別實(shí)現(xiàn)自動(dòng)登陸功能,需要的朋友可以參考下2024-05-05
淺析node中間件及實(shí)現(xiàn)一個(gè)簡單的node中間件
這篇文章主要介紹了淺析node中間件及實(shí)現(xiàn)一個(gè)簡單的node中間件,文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下2022-09-09
詳解node如何將Excel導(dǎo)入數(shù)據(jù)庫
這篇文章主要為大家詳細(xì)介紹了node如何通過腳本實(shí)現(xiàn)將Excel導(dǎo)入mysql數(shù)據(jù)庫里,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解一下2024-11-11

