内容


通过 Cloudant 代理执行授权

通过另一道防线保护应用程序

Comments

SQL 数据库拥有细粒度的访问控制。(例如,请参阅 IBM Knowledge Center 内的文章“行和列访问控制”。)与之相反,Cloudant® 数据库中的用户可以拥有整个数据库的读和/或写权限或完全没有这些权限。这一限制移除了对数据库中信息的一道保护屏障,要求更加信任应用程序编程人员。

在本教程中,您将学习如何在 Node.js 中创建一个 Cloudant 代理。因为该代理的代码是您编写的,所以您可以在其中包含想要执行的任何安全检查。

构建您的应用程序需要做的准备工作

  • 掌握 IBM Cloud、Node.js、Cloudant 和 JavaScript 基本知识
  • 具有一个 IBM Cloud Lite 帐户(免费!)

运行应用程序获取代码

样本应用程序

样本应用程序是一个银行账户系统。共有 3 个账户:alice、bill 和 carol。每个账户都有一定的余额。期望的行为是,允许用户将钱转到另一个账户,但不能转给自己。此应用程序是使用 Cloudant 和 OpenWhisk 实现的。要了解如何编写这样一个应用程序,请参阅“为联网环境构建一个智能锁”中的第 1、2、4 和 5 小节。您也可以访问与该教程相关的源代码

请注意,这种策略显式禁止一些操作,并隐式允许其他所有操作,它仅适用于此处,因为该应用程序非常简单。在生产系统中,最佳实践是拥有一个策略,它允许显式执行被许可的操作,并隐式拒绝其他所有操作。

第 1 步.捕获并转发 HTTPS 流量

因为一些参数被编码到路径名称中,所以代理无法作为 OpenWhisk API 应用程序来运行 — 因此我选择将它编写为 Cloud Foundry Node.js 应用程序。我在文章“向第三方应用程序添加自己的授权代理”中描述了实现此操作的方法。

该文章中描述的用例与本文中讨论的用例有一个重要区别。Cloudant 使用了 HTTP 基本验证。这意味着验证信息包含在客户端发送给代理的标头中。在将请求发送到服务器之前,需要从标头中移除该信息:

headers["authorization"] = null;

在创建被代理的请求时,添加服务器的验证信息。添加的行是第 7–10 行。

	// The options that go in the HTTP header
	var proxiedReqOpts = {
	      host: cloudantCred.host,
	      path: req.path + query,
	      method: req.method,
	      headers: headers,
	      auth: {
	      	type: "basic",
	      	username: cloudantCred.username,
	      	password: cloudantCred.password,	      	
	      }
	};

执行授权的最简单方法是使用一个独立中间件调用。 使用一个获取 HTTP 请求的函数,比如 app.all("*", function(req, res, next) { … });。第三个参数(在这里名为 next)是一个函数,可以调用它来返回请求,以进行后续处理。

            // The authorization logic
app.all("*", /* @callback */ function(req, res, next) {
.
.
.
	next();
});

如果请求未授权,那么将响应代码设置为 401,并使用 Unauthorized 来响应。

if (   unauthorized   ) {
		res.status(401).send('Unauthorized');
		return ; // No need to continue this function
	}

第 2 步.获取相关字段

下一步是识别请求中的字段来制定授权决策。

日志记录

具有一个临时日志,有助于查看请求和代码如何解析请求。为此,可以创建一个日志字符串并在要求时进行显示:

var log = "";
app.get("/log", /* @callback */ function(req, res) {
	res.send(log);	
});

任何时候您都能以 HTML 形式向日志中添加信息。例如,在不同的请求之间空出一行就很有用:

log += "<hr />";

用户和密码

用户和密码可用作 HTML 标头中的授权参数。提供它们的方式有点复杂,但可使用此代码来检索:

	if (req.headers.authorized !== null) {
		var origAuth = new Buffer(req.headers.authorization.replace("Basic ", ""), 'base64').toString('ascii');  	
		var arr = origAuth.split(":");
		user = arr[0];
		password = arr[1];
	}

请求字段

	log += "<h2>User: " + user + "</h2>";
	log += req.method + " " + req.path;
	if (req.body !== undefined)
		log += "<h4>Body:</h4>" + req.body;

查看日志— 这些是一个事务的结果:

日志结果
日志结果

如您所见,该应用程序首先读取一个用户的账户,更新该用户的余额,然后对另一个用户重复此操作。对于 GET 请求,用户 ID 包含在路径中。对于 POST 请求,用户 ID 作为 _id 属性包含在请求正文中,而且采用了 JSON 格式。

以下代码将使用上述两种方法获取 ID 和余额(如果可用)。它使用了 switch 结构,在路径或正文中查找相关信息。

	switch (req.method) {
		case "GET": 
			id = req.path.replace(/\/.+\//, "");
			break;
		case "POST": 	
			var reqBody = JSON.parse(req.body);
			id = reqBody._id;
			balance = reqBody.balance;
			break;
	}

上下文

您想要阻止用户增加自己的银行余额。但是,您无法从 GETPOST 请求中获取相关信息。在 POST 更改现有余额前,您需要知道该余额。可通过两种方式确定余额:

  • 在获得修改账户余额的 POST 请求时,通过代理提交一个 GET 请求。
  • 跟踪记录您作为对 GET 请求的响应而返回给应用程序的余额。无需另外查看 POST 请求,因为在执行任何更新余额的请求之前都会执行一个 GET 请求。(参阅“文档版本和 MVCC”。)

如果一次只有一个代理实例在运行,那么第二种方法要高效得多。为此,可以创建一个空散列表作为全局变量:

var knownBalance = {};

在向应用程序返回响应的代码中,检查是否报告了余额,如果是,就更新散列表。添加的行是第 4-9 行。

	var proxiedReq = http.request(proxiedReqOpts, function(proxiedRes) {		
		proxiedRes.on("data", function(chunk) {retVal += chunk;});
		proxiedRes.on("end", function() {
			var acctInfo = JSON.parse(retVal);
			
			// If we know about a user 
			if (acctInfo._id !== undefined) {
				knownBalance[acctInfo._id] = acctInfo.balance;
			}
			
			res.send(retVal);
		});
		proxiedRes.on("error", function(err) {res.send(JSON.stringify(err) + "<hr />" + retVal);});
	});

第 3 步.编写授权代码

有了所有这些信息后,现在就可以实际编写授权代码了。在本例中,您需要检查用户是否与被更改的账户相同。如果相同,那么检查余额是否增加,如果余额增加,就拒绝该事务:

	// The only case where we deny authorization
	if ((id === user) && (balance > knownBalance[id])) {
		res.status(401).send('Unauthorized');
		return ; // No need to continue this function
	}

请注意,这种策略显式禁止一些操作,并隐式允许其他所有操作,它仅适用于此处,因为该应用程序非常简单。在生产系统中,最佳实践是拥有一个策略,它允许显式执行被许可的操作,并隐式拒绝其他所有操作。

潜在缺陷:事务回滚

使用这样一个代理时应考虑的一个重要因素是,应用程序必须能回滚被禁止的操作。例如,在样本应用程序中,从 Bill 向 Alice 的转账会将代理(及其之外的数据库)视为两个更改操作:

  1. 从 Bill 的余额中扣除这笔钱
  2. 将这笔钱添加到 Alice 的余额中

如果允许第一个操作,但拒绝第二个操作(例如,由于 Alice 也是用户),Bill 账户的这笔钱就会消失。这是不合理的行为,该行为在代理中很难预防。

正确编写的应用程序在编写时会假设数据库操作可能失败,而且具有处理失败的代码。在这里,modifyAccount 函数有两个回调:一个在成功时调用,另一个则在失败时调用。

// Modify a bank account
var modifyAccount = (user, amount, cloudantUrl, callback, errCallback) => {
    var db = require("cloudant")(cloudantUrl).db.use("accounts");
    
    db.get(user, (err, res) => {
        res.balance += amount;
        db.insert(res, (err, body) => {
            errMsg = JSON.stringify(err);
            if (err === null)
                callback();
            else
                errCallback();
        }); // end of db.insert
    });  // end of db.get
};

调用 modifyAccount 的函数使用内部调用(后一个调用)的失败回调来撤销外部调用(首先发生的调用)。

    modifyAccount(params.fromUser, -params.amount, cloudantUrl, 
        () => {
            modifyAccount(params.toUser, +params.amount, cloudantUrl, 
                () => { returnHtml(success); }, 
                () => { 
                    // Before reporting the error, undo the outer modifyAccount, which did succeed.
                    modifyAccount(params.fromUser, +params.amount, cloudantUrl, 
                        () => {errorMessage(success);},
                        () => {errorMessage(success);})   // End of undo modifyAccount
                }  // end of failure function for inner modifyAccount
            ); // end of inner modifyAccount call
        },
        () => { errorMessage(success); }
    ); // end of outer modifyAccount call

如果编写的应用程序没有这样一个回滚函数,那么在代理中提供授权时,保证状态一致将会更难。或许可以通过缓存与特定事务相关的所有更改来实现此目的,但如果要这么做,就需要找到一种识别各个事务的方法。这或许可行,但具体取决于应用程序本身。没有实现此目标的通用方法。

结论

Cloudant 代理无法取代应用程序中的安全机制,因为它拥有的信息更少,而且与用户通信的能力更加有限。但是,作为“深度防御”的一个组成部分,可以将它作为攻击者需要突破的另一道防线。此代理也可以提供一个与应用程序无关的操作日志,以防攻击者攻破应用服务器。


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Security
ArticleID=1056781
ArticleTitle=通过 Cloudant 代理执行授权
publish-date=01162017