内容


在 Node.js Bluemix 应用程序中管理帐户批准

Comments

在许多情况下,比如 B2C 应用程序,最好让用户自行注册,然后让管理员批准或拒绝帐户或特定的帐户权限。本文将介绍如何实现这样一个系统。

首先,我将介绍该应用程序的一个非常简单的版本来演示该系统,但其中包含大量安全缺陷。然后,我将分析其中的每个安全缺陷,展示如何修复它们。我将逐行分析重要的代码段,确保您理解它们,并可将该知识应用到您自己的应用程序中。

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

该应用程序的演示和安全版本

我提供了该应用程序的两个不同版本:一个演示版本和一个安全版本。在本教程的第一部分中,我们将查看演示应用程序。随后,在讨论安全性时,我们将分析它的安全版本。这些版本可通过以下方式进行访问:

运行不安全的应用程序获取代码

运行安全的应用程序获取代码

在本教程中,我将介绍如何编写一个应用程序,它允许用户自助注册,然后由管理员来批准或拒绝他们的帐户。我还将分析一些典型的安全漏洞,以及如何防御它们。

演示应用程序

打开 演示应用程序 时,会显示 3 个按钮:Login、Request an account 和 Approve accounts。

主屏幕的屏幕截图

用户界面

Login 按钮将用户跳转到登录屏幕,用户需要在该屏幕中输入电子邮件地址和密码。

用户界面的屏幕截图
用户界面的屏幕截图

如果用户尚未注册,可以单击 Request an account 按钮进行注册。此按钮会将用户跳转到 Account Request 表单,他可以在该表单中输入其姓名、密码、电子邮件地址、电话和注册理由。然后用户单击 Submit

account request 表单的屏幕截图
account request 表单的屏幕截图

主屏幕上的第 3 个按钮是 Approve Accounts 按钮。此按钮实际应该只由管理员使用,但由于此应用程序只用于演示目的,所以我也提供了它。通过单击此按钮,管理员可以查看所有当前的帐户请求,批准或拒绝它们:

等待批准的请求的屏幕截图
等待批准的请求的屏幕截图

批准一个帐户请求后,用户可以尝试再次打开该应用程序并登录。

帐户请求

在用户单击主屏幕上的 Request an Account 时,他会被重定向到 acct_request.html。这是一个简单的 HTML POST 表单;关于它的一个唯一要点是,它使用了 Bootstrap 主题(就像该应用程序中的其他 HTML 文件一样)。

该表单被提交到 /acct_req,后者由这个 app.js 函数处理:

// Deal with new account requests
app.post("/acct_req", function(req, res) {
  acctReqs[req.body.email] = req.body;
  res.send("Request received, thank you " + req.body.name + ".");
});

此函数非常简单,因为它的主要工作就是解析 POST 请求的内容。要让环境为您完成此工作,必须导入和使用 body-parser 包。这需要执行两个操作:

  1. 通过将 body-parser 包添加到依赖项列表中(第 4 行),在 package.json 中声明需要它:
    	"dependencies": {
    		"express": "4.12.x",
    		"cfenv": "1.0.x",
    		"body-parser": "*"
    	},
  2. 为 body-parser 包创建一个对象并告诉应用程序使用它。应用层中的处理函数可以是终端处理函数或中间处理函数。终端处理函数响应一个请求,而中间处理函数修改请求(例如,解析 POST 请求的内容):
	// Use body-parser to receive POST form requests
	var bodyParser = require("body-parser");
	app.use(bodyParser.urlencoded({extended:true}));

所有表单字段都在 req.body 中提供,作为一个关联数组(也称为哈希表)。要存储此请求,只需将它放在请求数组中即可:

  acctReqs[req.body.email] = req.body;

此应用程序使用电子邮件地址作为唯一标识符。这里使用电子邮件地址作为帐户请求数组 acctReqs 的索引。

acctReqs 的初始值。

这是初始化 acctReqs 的代码:

	var acctReqs = {"hacker@evil.com": {
                  email: "hacker@evil.com",
                  name: "Bad Guy",
                  justification: "I want to break your stuff.",
                  password: "Object00"},
             "niceguy@good.com": {
                  email: "niceguy@good.com",
                  name: "Good Guy",
                  justification: "You can trust me",
                  password: "Object00"}
                };

可以看到,该代码已包含两个条目:hacker@evil.comniceguy@good.com。这些条目提供了可在应用程序启动时批准或拒绝的请求。

管理员批准

当经过授权的管理员单击 Approve accounts 按钮时,浏览器将会打开 acct_approval.html。此文件更复杂,因为它使用了 Angular(MEAN 堆栈中的 “A”)作为数据模型与用户界面视图之间的控制器。

从服务器获取请求

要为管理员显示这些请求,网页需要知道有哪些请求。但是,acctReqs 存储在服务器上。可以直接在 app.js 中生成整个 acct_approval.html 文件,包括 acctReqs 变量,但这会使代码变得不容易阅读。将 HTML 文件放在公共目录中要容易得多。

解决方案是将该变量放在一个单独的脚本中,然后将该脚本包含在 acct_approval.html 中:

<script src="acctReqs.js"></script>

此脚本由 app.js 动态生成,这利用了 JSON(JavaScript 对象表示法),JSON 是对象的 JavaScript 表示。

// Request for the list of accounts
app.get("/acctReqs.js", function(req, res) {
  res.send("var acctReqs = " + JSON.stringify(acctReqs) + ";");
});

可以查看 http://approval-req.mybluemix.net/acctReqs.js 来了解当前的请求。

设置 Angular

Angular 使用了两个条目:一个应用程序和一个控制器(或多个控制器)。二者都需要关联到一个表示其范围的 HTML 标记。在本例中,二者关联到顶级标记 html

<html ng-app="myApp" ng-controller="myCtrl">
<!-- All the ng.. attributes are directives to the Angular library, which is
     used as the data model →

也必须在一个 script 标记中设置 Angular。此代码首先创建一个应用程序 myApp,然后创建控制器 myCtrl。创建 myCtrl 的调用的一个参数是初始化 $scope 的函数,该函数的结构包含位于控制器(即管理它们的控制器) “范围” 内的变量和函数。

  • acctReqs:此变量获得服务器上的相同值。
  • approve(email)decline(email):这些函数将浏览器重定向到一个 URL,该 URL 告诉服务器一个请求已被批准或拒绝。
<script>
// The data model

var myApp = angular.module("myApp", []);

myApp.controller("myCtrl", function($scope) {
       // The account requests come in a separate script we get from the server
      $scope.acctReqs = acctReqs;


      $scope.approve = function(email) {
        window.location = "approve/" + escape(email);
      }

      $scope.decline = function(email) {
        window.location = "decline/" + escape(email);
      }
});

</script>

使用 Angular

在 HTML 中,以两种方式使用 Angular。首先,当 HTML 属性具有以 ng- 开头的标记时,该标记就是一个 Angular 调用。其次,当 HTML 中的表达式放在双大括号内时 {{expression}},该表达式在控制器范围内被分析为 JavaScript。

这里第一次使用 Angular 的地方是 ng-repeat。此属性导致一个标记(在本例中为 <tr>)对一个数组中的每个元素重复一次。(还可以添加一个过滤器,以便仅使用部分元素。)此语法为 acctReqs 关联数组中的每个元素创建了一个表行。当前行的值存储在一个临时范围变量 req 中。

下一个要使用的是 ng-click 属性。类似于正常的 onClick,此属性确定在单击该按钮时运行哪段 JavaScript 代码。但是,在本例中,JavaScript 代码在 Angular 范围的上下文中运行,这意味着只有此范围内声明的函数和变量是可用的。这正是我们需要在该范围内声明 approve(email)decline(email),而不是在 ng-click 内的每个条目中放入一行代码的原因。

最后,{{req.name}}{{req.email}}{{req.justification}} 显示请求参数,以便管理员可做出决定。

    <table class="table table-condensed table-striped">
      <tr><th></th><th>Name</th><th>E-mail</th><th>Justification</th></tr>
      <tr ng-repeat="req in acctReqs">
        <td>
          <button class="btn btn-sm btn-success"
            ng-click="approve(req.email);">
            <span class="glyphicon glyphicon-ok" />
          </button>
          <button class="btn btn-sm btn-danger"
            ng-click="decline(req.email);">
            <span class="glyphicon glyphicon-remove" />
          </button>
        </td>
        <td>{{req.name}}</td>
        <td>{{req.email}}</td>
        <td>{{req.justification}}</td>
      </tr>
    </table>

服务器对响应的处理

服务器对响应的处理有两个功能。首先,它需要修改 acctReqs 来删除请求(并在请求获得批准的情况下,修改 accts 来添加新帐户)。第二,它需要将浏览器重定向回帐户批准页面。

当通过 app.<HTML verb> 函数处理在路径中使用了 :<urlVar> 的 URL 时,该变量会存储在 req.params.<urlVar> 中。在这里,该变量是被用作哈希表的键的电子邮件地址。它被用于适当地修改哈希表。

最后,res.redirect(<url>) 通过重定向到帐户批准页面来响应浏览器。因为从浏览器的角度讲,响应 “页面” 似乎位于一个子目录中,所以它被重定向回应用程序的主目录。

// Deal with approved accounts
app.get("/approve/:email", function(req, res) {
    // Move the account from request to account list
    accts[req.params.email] = acctReqs[req.params.email];
    delete acctReqs[req.params.email];

    // Redirect to the account approval page
    res.redirect("../acct_approval.html");
});

// Deal with accounts that are not approved
app.get("/decline/:email", function(req, res) {
    // Delete the request
    delete acctReqs[req.params.email];

    // Redirect to the account approval page
    res.redirect("../acct_approval.html");
});

登录

用户单击 Login 按钮时,他们将被重定向到 login.html。这是另一个简单的 HTML POST 表单,它使用了 Bootstrap。

app.js 中处理登录的函数非常简单。首先,它查看是否有一个包含该电子邮件地址的实际帐户。如果有,那么它会检查密码是否匹配。然后发送适当的响应。如果该帐户不存在,它会检查是否存在至少一个对该帐户的请求。

// Deal with logins
app.post("/login_attempt", function(req, res) {
  if (accts[req.body.email] != undefined) { // The account exists
    if (accts[req.body.email].password == req.body.password) {
      res.send("Login successful, welcome " + accts[req.body.email].name);
    } else {
      res.send("Wrong password, " + accts[req.body.email].name);
    }
  } else {  // Account does not exists
    if (acctReqs[req.body.email] != undefined) { // There is a request
      res.send("Request not processed yet, " + acctReqs[req.body.email].name);
    } else {
      res.send("I don't know you.");
    }
  }
});

应用程序安全性

我们编写的这个应用程序非常不安全。例如,目前潜在的黑客可以通过访问 https://approval-req.mybluemix.net/approve/hacker@evil.com 来批准他们自己的帐户。他们还可以访问 https://approval-req.mybluemix.net/acctReqs.js 来获取请求列表,包括密码。此外,他们可以通过查看对拥有错误密码和电子邮件地址未知的不同响应,识别注册的用户的电子邮件地址。

因此,下一步是修复这些安全漏洞,生成一个更安全的应用程序。您可以在 approval-req-sec.mybluemix.net 上访问这个安全的应用程序。

密码泄露

可以通过两种方式确保对 acctReqs.js 的访问不会揭示密码。修复对提供密码的 app.get() 的调用,或者不将密码放在一个可用的表单中的 acctReqs 中(单独存储它或者对它执行加密或哈希运算)。在本文中,我将展示如何实现这两种方式。

要对密码执行哈希运算,可以使用 password-hash 包。

  1. 首先,将下面这行添加到 package.json 中的依赖项中(第 4 行):
    "password-hash":"*",
    	"dependencies": {
    		"express": "4.12.x",
    		"cfenv": "1.0.x",
    		"password-hash": "*",
    		"body-parser": "*"
    	},
  2. 然后,添加 require 调用来使用该包(第 1 行),修改初始 acctReqs 来使用它(第 6 和第 11 行):
    pwdHash = require("password-hash");	
    	var acctReqs = {"hacker@evil.com": {
                      email: "hacker@evil.com",
                      name: "Bad Guy",
                      justification: "I want to break your stuff.",
                      password: pwdHash.generate("Object00")},
                 "niceguy@good.com": {
                      email: "niceguy@good.com",
                      name: "Good Guy",
                      justification: "You can trust me",
                      password: pwdHash.generate("Object00")}
                    };
  3. 另外,您需要修改回答 /login_attempt 请求的函数,以便使用经过哈希运算的密码(第 4 行)。
    // Deal with logins
    app.post("/login_attempt", function(req, res) {
      if (accts[req.body.email] != undefined) { // The account exists
        if (pwdHash.verify(req.body.password, accts[req.body.email].password)) {
          res.send("Login successful, welcome " + accts[req.body.email].name);
        } else {
          res.send("Wrong password, " + accts[req.body.email].name);
        }
      } else {  // Account does not exists
        if (acctReqs[req.body.email] != undefined) { // There is a request
          res.send("Request not processed yet, " + acctReqs[req.body.email].name);
        } else {
          res.send("I don't know you.");
        }
      }
    });

如果可以避免,甚至经过哈希运算的密码也不应泄漏。要保护它们免遭泄露,可以修改用于发出该数据的 app.get() 调用。如果 JSON.stringify() 获得的另一个参数是函数,它会在每个键/值对上调用该函数,让您修改该值。这被形象地称为替换器 函数。

// Request for the list of accounts
app.get("/acctReqs.js", function(req, res) {
  res.send("var acctReqs = " + JSON.stringify(acctReqs,
	function(key, val) {
		if (key == "password")
			return undefined;
		else
			return val;
	})
	+ ";");
});

管理界面的安全性

要解决的下一个问题是,黑客可以批准自己的请求。为此,他们只需将 “approve/<电子邮件地址>” 附加到应用程序的 URL。预防此攻击的一种方式是使用 cookie。当作为管理员的您读取 acct_approval.html 时,将一个浏览器 cookie 设置为一个随机值。然后,在收到管理员批准或拒绝请求的决定时,应用程序可检查该浏览器 cookie 是否拥有正确的值。这样,只有合法的管理员能够批准或拒绝请求(除非 acct_approval.html 可公开获得,出于培训目的,此应用程序中就是这样编写它的)。

为了生成该 cookie 的值,我们使用了 node-uuid 包。将它添加到 package.json 中,像其他包一样,将这些变量初始化代码添加到靠近 app.js 顶部的地方:

var cookieName = "approval-req";
var cookieVal = require("node-uuid").v4();

备注:为保持简单,此代码生成一个静态值。在生产环境中,您应该定期更改该 cookie 的值,但在管理员正在批准或拒绝请求时,允许在几分钟内使用旧值。

我们无法将对 cookie 的更改添加到将 acct_approval.html 返回到浏览器的函数,因为我们无法控制它。public/ 目录中的文件由 express 包中包含的 express.static 对象处理。我们在该行之上添加了另一个 中间件,以便在到达该行之前设置该 cookie。此中间件仅应用于 /acct_approval.html,最后会调用 next() 来表明它不是处理请求的最后一步。

app.use("/acct_approval.html", function(req, res, next) {
	res.cookie(cookieName, cookieVal);
	next();
});

// serve the files out of ./public as our main files
app.use(express.static(__dirname + '/public'));

要获取返回的 cookie 的值,可以将包 cookie-parser 添加到 package.json 中并添加下面这行来使用它:

app.use(require("cookie-parser")());

最后,在两个接受响应的 use.get() 调用中,检查该 cookie:

// Deal with approved accounts
app.get("/approve/:email", function(req, res) {
  if(req.cookies[cookieName] == cookieVal) {
    // Move the account from request to account list
    accts[req.params.email] = acctReqs[req.params.email];
    delete acctReqs[req.params.email];

    // Redirect to the account approval page
    res.redirect("../acct_approval.html");
  } else
    res.send("Nice try");
});

// Deal with accounts that are not approved
app.get("/decline/:email", function(req, res) {
  if(req.cookies[cookieName] == cookieVal) {
    // Delete the request
    delete acctReqs[req.params.email];

    // Redirect to the account approval page
    res.redirect("../acct_approval.html");
  } else
    res.send("Nice try");
});

帐户存在性的泄露

为了避免泄露帐户是否存在,可在处理 /login_attemptapp.post()(第 6-9 行)中对所有故障使用相同的错误消息。这意味着我们不再关心请求是否存在。

// Deal with logins
app.post("/login_attempt", function(req, res) {
  if (accts[req.body.email] != undefined) { // The account exists
    if (pwdHash.verify(req.body.password, accts[req.body.email].password))
      res.send("Login successful, welcome " + accts[req.body.email].name);
    else
      res.send("Bad user or password");
  } else
      res.send("Bad user or password");
});

为了将重点放在帐户批准上,我忽略了生产系统中可能存在的其他特性。

  • 持久性。在重新启动应用程序时,它会丢失其所有状态,所以所有帐户和帐户请求都会删除。生产系统会将 acctsacctReqs 存储在一个 MongoDB 数据库中,可以参阅 “使用 Bluemix 和 MEAN 堆栈构建自助发表 Facebook 信息的应用程序,第 2 部分:将用户信息存储在服务器上” 中的介绍。
  • 密码验证。正常情况下,在创建帐户时,用户会被要求输入密码两次,所以录入错误不会导致不可用的帐户。实现此目的的方式是使用 Angular,在密码不匹配时使用 ng-if 显示一个错误。有关的更多信息,请参阅 Angular 网站上的 ngIf entry
  • 电子邮件验证、密码重设和帐户通知。所有这些特性都需要发送一封电子邮件,电子邮件中的一个链接或一些文本中包含一个随机令牌。可以使用 Bluemix SendGrid 服务 发送您的电子邮件。

结束语

您现在应改能够编写非常安全的简单帐户批准系统,并针对更复杂的场景对其进行修改。对于非常复杂的场景,可以使用 IBM Security Identity Manager (ISIM)。ISIM 拥有控制几乎每个常用系统上的帐户的适配器,如果需要的话,它还支持使用复杂的批准工作流来实现复杂的业务流程。


相关主题


评论

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

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Open source
ArticleID=1020374
ArticleTitle=在 Node.js Bluemix 应用程序中管理帐户批准
publish-date=11102015