欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Node.js设置CORS跨域请求中多域名白名单的方法

程序员文章站 2022-07-04 21:46:08
cors 说到cors,相信前端儿都不陌生,这里我就不多说了,具体可以看看。 cors,主要就是配置response响应头中的 access-control-allow...

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,简直完美~

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流,谢谢大家对的支持。