Node.js设置CORS跨域请求中多域名白名单的方法
cors
说到cors,相信前端儿都不陌生,这里我就不多说了,具体可以看看。
cors,主要就是配置response响应头中的 access-control-allow-origin 属性为你允许该接口访问的域名。最常见的设置是:
res.header('access-control-allow-origin', '*'); res.header('access-control-allow-credentials', 'true'); // 允许服务器端发送cookie数据
然而,这样的设置是最简单粗暴,同时也是最不安全的。它表示该接口允许所有的域名对它进行跨域请求。然而,在一般实际业务中,都希望该接口只允许对某一个或几个网站开放跨域请求权限,而非全部。
那么,聪明的你肯定想着,多域名白名单还不简单吗,写个正则就好啦?再不行,直接配置 access-control-allow-origin 属性为用逗号分隔的多个域名不就好了吗?
就像下面这样:
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的响应头header中的 access-control-allow-origin 属性不能匹配除 (*) 以外的正则表达式的,域名之间不能也用逗号分隔。也就是说, access-control-allow-origin 的属性值只允许设置为单个确定域名字符串或者 (*)。
既然我们希望允许的是多个域名,也不愿意使用不安全的 * 通配符,难道就真不能配置多域名白名单的cors了吗?
多域名白名单的cors确实是可以实现的。只是有一点曲线救国的味道。
多域名白名单的cors实现原理
具体原理可以参考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; }());
实现原理是这样的:
既然 access-control-allow-origin 属性已经明确不能设置多个域名,那么我们只得放弃这条路了。
最流行也是最有效的方法就是,在服务器端判断请求的header中origin属性值(req.header.origin)是否在我们的域名白名单列表内。如果在白名单列表内,那么我们就把 access-control-allow-origin 设置成当前的origin值,这样就满足了access-control-allow-origin 的单一域名要求,也能确保当前请求通过访问;如果不在白名单列表内,则返回错误信息。
这样,我们就把跨域请求的验证,从浏览器端转移到服务端来了。对origin字符串的验证就变成了相当于常规字符串的验证,我们不仅可以使用数组列表验证,还可以使用正则匹配。
具体代码如下:
// 判断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响应头的origin属性 // 判断请求是否在域名白名单内 if(isoriginallowed(reqorigin, allow_origin)) { // 设置cors为请求的origin值 res.header("access-control-allow-origin", reqorigin); res.header('access-control-allow-credentials', 'true'); // 你的业务代码逻辑代码 ... // ... } else { res.send({ code: -2, msg: '非法请求' }); } });
oh yeah,简直完美~
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流,谢谢大家对的支持。