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 相关文章推荐
表单的一些基本用法与技巧
Jul 15 Javascript
js 页面传参数时 参数值含特殊字符的问题
Dec 13 Javascript
JavaScript原生对象之Date对象的属性和方法详解
Mar 13 Javascript
javascript实现表单提交后,提交按钮不可用的方法
Apr 18 Javascript
jquery.cookie.js实现用户登录保存密码功能的方法
Apr 15 Javascript
基于javascript编写简单日历
May 02 Javascript
基于JS如何实现类似QQ好友头像hover时显示资料卡的效果(推荐)
Jun 09 Javascript
window.close(); 关闭浏览器窗口js代码的总结介绍
Jul 14 Javascript
微信小程序自定义toast弹窗效果的实现代码
Nov 15 Javascript
vue实现编辑器键盘抬起时内容跟随光标距顶位置向上滚动效果
May 28 Javascript
vue axios请求成功却进入catch的原因分析
Sep 08 Javascript
原生JavaScript实现刮刮乐
Sep 29 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去除换行符的方法小结(PHP_EOL变量的使用)
2013/02/16 PHP
php中strstr、strrchr、substr、stristr四个函数的区别总结
2014/09/22 PHP
了解PHP的返回引用和局部静态变量
2015/06/04 PHP
Yii2.0 Basic代码中路由链接被转义的处理方法
2016/09/21 PHP
php之可变变量的实例详解
2017/09/12 PHP
PHP数组式访问接口ArrayAccess用法分析
2017/12/28 PHP
利用Dojo和JSON建立无限级AJAX动态加载的功能模块树
2007/03/24 Javascript
30个让人兴奋的视差滚动(Parallax Scrolling)效果网站
2012/03/04 Javascript
jquery.messager.js插件导致页面抖动的解决方法
2013/07/14 Javascript
常用jQuery选择器总结
2014/07/11 Javascript
JS替换字符串中空格方法
2015/04/17 Javascript
javascript实现框架高度随内容改变的方法
2015/07/23 Javascript
jQuery插件实现无缝滚动特效
2015/11/24 Javascript
Bootstrap树形组件jqTree的简单封装
2016/01/25 Javascript
JavaScript toUpperCase()方法使用详解
2016/08/26 Javascript
vue双向绑定简要分析
2017/03/23 Javascript
简单实现JS上传图片预览功能
2017/04/14 Javascript
ES6的解构赋值实例详解
2019/05/06 Javascript
ES6 Iterator接口和for...of循环用法分析
2019/07/31 Javascript
axios异步提交表单数据的几种方法
2019/08/11 Javascript
使用vue-cli4.0快速搭建一个项目的方法步骤
2019/12/04 Javascript
基于Angular 8和Bootstrap 4实现动态主题切换的示例代码
2020/02/11 Javascript
Python中扩展包的安装方法详解
2017/06/14 Python
Python 字符串与二进制串的相互转换示例
2018/07/23 Python
Python基于matplotlib画箱体图检验异常值操作示例【附xls数据文件下载】
2019/01/07 Python
python对文件目录的操作方法实例总结
2019/06/24 Python
详解有关PyCharm安装库失败的问题的解决方法
2020/02/02 Python
Python3使用Selenium获取session和token方法详解
2021/02/16 Python
python 基于DDT实现数据驱动测试
2021/02/18 Python
美国修容界大佬创建的个人美妆品牌:Kevyn Aucoin Beauty
2018/12/12 全球购物
人事主管的岗位职责
2013/11/16 职场文书
报告会主持词
2014/04/02 职场文书
竞选部长演讲稿
2014/04/26 职场文书
2015年设计师个人工作总结
2015/04/25 职场文书
Java 深入探究讲解简单工厂模式
2022/04/07 Java/Android
分享几个实用的CSS代码块
2022/06/10 HTML / CSS