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


Posted in Javascript onMarch 28, 2017

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

总结

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

Javascript 相关文章推荐
判断浏览器的javascript版本的代码
Sep 03 Javascript
基于jQuery的实现简单的分页控件
Oct 10 Javascript
js控制web打印(局部打印)方法整理
May 29 Javascript
jQuery应用之jQuery链用法实例
Jan 19 Javascript
Javascript 完美运动框架(逐行分析代码,让你轻松了运动的原理)
Jan 23 Javascript
微信小程序 缓存(本地缓存、异步缓存、同步缓存)详解
Jan 17 Javascript
js使用html2canvas实现屏幕截取的示例代码
Aug 28 Javascript
vue2.x select2 指令封装详解
Oct 12 Javascript
Vue axios设置访问基础路径方法
Sep 19 Javascript
JavaScript实现指定数量的并发限制的示例代码
Mar 10 Javascript
Js和VUE实现跑马灯效果
May 25 Javascript
js中Map和Set的用法及区别实例详解
Feb 15 Javascript
TypeScript入门-基本数据类型
Mar 28 #Javascript
Restify中接入Socket.io报Error:Can’t set headers的错误解决
Mar 28 #Javascript
Vue.js 60分钟快速入门教程
Mar 28 #Javascript
bootstrap table实现单击单元格可编辑功能
Mar 28 #Javascript
Web前端框架Angular4.0.0 正式版发布
Mar 28 #Javascript
JavaScript实现经纬度转换成地址功能
Mar 28 #Javascript
js实现多行文本框统计剩余字数功能
Mar 28 #Javascript
You might like
PHP stream_context_create()作用和用法分析
2011/03/29 PHP
php中设置index.php文件为只读的方法
2013/02/06 PHP
php递归创建目录的方法
2015/02/02 PHP
yii添删改查实例
2015/11/16 PHP
PHP封装XML和JSON格式数据接口操作示例
2019/03/06 PHP
laravel多条件查询方法(and,or嵌套查询)
2019/10/09 PHP
Swoole扩展的6种模式深入详解
2021/03/04 PHP
javascript 遍历验证所有文本框的值
2009/08/27 Javascript
jquery插件制作 提示框插件实现代码
2012/08/17 Javascript
各种页面定时跳转(倒计时跳转)代码总结
2013/10/24 Javascript
JavaScript动态添加style节点的方法
2015/06/09 Javascript
JS模拟Dialog弹出浮动框效果代码
2015/10/16 Javascript
JavaScript编写带旋转+线条干扰的验证码脚本实例
2016/05/30 Javascript
javascript使用 concat 方法对数组进行合并的方法
2016/09/08 Javascript
使用json来定义函数,在里面可以定义多个函数的实现方法
2016/10/28 Javascript
Vue2.0权限树组件实现代码
2017/08/29 Javascript
对angular 实时更新模板视图的方法$apply详解
2018/10/09 Javascript
javascript判断一个变量是数组还是对象
2019/04/10 Javascript
Element-ui中元素滚动时el-option超出元素区域的问题
2019/05/30 Javascript
python3下使用cv2.imwrite存储带有中文路径图片的方法
2018/05/10 Python
解决pandas无法在pycharm中使用plot()方法显示图像的问题
2018/05/24 Python
OpenCV+Python识别车牌和字符分割的实现
2019/01/31 Python
python实现批量nii文件转换为png图像
2019/07/18 Python
python实现登录密码重置简易操作代码
2019/08/14 Python
Tensorflow中tf.ConfigProto()的用法详解
2020/02/06 Python
解决Jupyter无法导入已安装的 module问题
2020/04/17 Python
使用pandas读取表格数据并进行单行数据拼接的详细教程
2021/03/03 Python
iHerb中文官网:维生素、保健品和健康产品
2018/11/01 全球购物
不打扫卫生检讨书
2014/02/12 职场文书
幼儿教师演讲稿
2014/05/06 职场文书
杜甫草堂导游词
2015/02/03 职场文书
商场圣诞节活动总结
2015/05/06 职场文书
学校艾滋病宣传活动总结
2015/05/09 职场文书
导游词之沈阳植物园
2019/11/30 职场文书
微软Win11什么功能最惊艳? Windows11新功能特性汇总
2021/11/21 数码科技
sentinel支持的redis高可用集群配置详解
2022/04/01 Redis