Node.js設(shè)置CORS跨域請(qǐng)求中多域名白名單的方法
CORS
說(shuō)到CORS,相信前端兒都不陌生,這里我就不多說(shuō)了,具體可以看看這篇文章。
CORS,主要就是配置Response響應(yīng)頭中的 Access-Control-Allow-Origin 屬性為你允許該接口訪(fǎng)問(wèn)的域名。最常見(jiàn)的設(shè)置是:
res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Credentials', 'true'); // 允許服務(wù)器端發(fā)送Cookie數(shù)據(jù)
然而,這樣的設(shè)置是最簡(jiǎn)單粗暴,同時(shí)也是最不安全的。它表示該接口允許所有的域名對(duì)它進(jìn)行跨域請(qǐng)求。然而,在一般實(shí)際業(yè)務(wù)中,都希望該接口只允許對(duì)某一個(gè)或幾個(gè)網(wǎng)站開(kāi)放跨域請(qǐng)求權(quán)限,而非全部。
那么,聰明的你肯定想著,多域名白名單還不簡(jiǎn)單嗎,寫(xiě)個(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');
很遺憾地告訴你,這樣的寫(xiě)法是無(wú)效的。在Node.js中,res的響應(yīng)頭Header中的 Access-Control-Allow-Origin 屬性不能匹配除 (*) 以外的正則表達(dá)式的,域名之間不能也用逗號(hào)分隔。也就是說(shuō), Access-Control-Allow-Origin 的屬性值只允許設(shè)置為單個(gè)確定域名字符串或者 (*)。
既然我們希望允許的是多個(gè)域名,也不愿意使用不安全的 * 通配符,難道就真不能配置多域名白名單的CORS了嗎?
多域名白名單的CORS確實(shí)是可以實(shí)現(xiàn)的。只是有一點(diǎn)曲線(xiàn)救國(guó)的味道。
多域名白名單的CORS實(shí)現(xiàn)原理
具體原理可以參考cors庫(kù)的核心代碼:
(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值,這樣就滿(mǎn)足了Access-Control-Allow-Origin 的單一域名要求,也能確保當(dāng)前請(qǐng)求通過(guò)訪(fǎng)問(wèn);如果不在白名單列表內(nèi),則返回錯(cuò)誤信息。
這樣,我們就把跨域請(qǐng)求的驗(yàn)證,從瀏覽器端轉(zhuǎn)移到服務(wù)端來(lái)了。對(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)直完美~
總結(jié)
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作能帶來(lái)一定的幫助,如果有疑問(wèn)大家可以留言交流,謝謝大家對(duì)腳本之家的支持。
相關(guān)文章
kafka調(diào)試中遇到Connection to node -1 could not be established. Br
這篇文章主要介紹了kafka調(diào)試中遇到Connection to node -1 could not be established. Broker may not be available的解決方法,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2019-09-09如何使用docker直接運(yùn)行不同版本nodejs命令
這篇文章主要介紹了如何使用docker直接運(yùn)行不同版本nodejs命令,在不支持高版本Node.js的舊版操作系統(tǒng)上,可以通過(guò)Docker容器技術(shù)解決兼容性問(wèn)題,需要的朋友可以參考下2024-10-10使用Koa實(shí)現(xiàn)一個(gè)獲取視頻播放地址的接口
在本節(jié)課中,我們將學(xué)習(xí)如何使用 Koa 實(shí)現(xiàn)一個(gè)獲取視頻播放地址的接口,我們將創(chuàng)建一個(gè)控制器,通過(guò)視頻 ID 獲取播放地址,并設(shè)置相應(yīng)的路由,最后,我們將使用 Postman 進(jìn)行測(cè)試,感興趣的朋友可以參考下2024-05-05Node.js和MongoDB實(shí)現(xiàn)簡(jiǎn)單日志分析系統(tǒng)
這篇文章主要介紹了Node.js和MongoDB實(shí)現(xiàn)簡(jiǎn)單日志分析系統(tǒng),本文給出了服務(wù)器端、客戶(hù)端、圖表生成、Shell自動(dòng)執(zhí)行等功能的實(shí)現(xiàn)代碼,需要的朋友可以參考下2015-04-04Node.js中開(kāi)發(fā)樹(shù)形結(jié)構(gòu)接口的實(shí)現(xiàn)
本文介紹了Node.js中開(kāi)發(fā)樹(shù)形結(jié)構(gòu)接口的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2024-12-12nodeJs編寫(xiě)錯(cuò)誤處理中間件問(wèn)題
這篇文章主要介紹了nodeJs編寫(xiě)錯(cuò)誤處理中間件問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-12-12