NodeJS-秘籍-三-

124 阅读18分钟

NodeJS 秘籍(三)

原文:Node.js Recipes

协议:CC BY-NC-SA 4.0

六、实现安全性和加密

在这一章中,你将研究任何应用的一个关键部分,不管你选择使用什么样的框架来开发它:安全性。然而,您不仅将研究如何将安全特性融入到您的应用中,还将获得关于 Node.js 如何在其核心中以及通过第三方实现来处理安全性和加密的独特视角。

您将研究 Node.js 中可用的各种类型的散列算法,分析它们是如何工作的,以及它们可能比其他算法更适合哪些应用。您将看到使用哈希算法验证文件完整性的解决方案。哈希算法在构建应用时很常见,因为它们被设计为接受任意数量的数据,并将其转换为该数据的可管理的固定长度表示形式,就像原始数据的签名一样。这在应用中很常见,用于密码存储等场景,在这些场景中,您应该存储可用于验证文本的引用,而不是密码的实际文本。哈希算法对于构建也是不可或缺的。您将利用基于哈希的消息验证码(HMAC)来验证和验证应用的消息。除了哈希算法之外,还有加密算法,用于在加密后实际检索原始数据。与哈希算法的区别在于哈希算法仅限于单向哈希,这意味着您不打算对哈希后的消息进行解哈希。

接下来,您还将研究可用于加密数据并保证其安全的 OpenSSL 密码。然后,您将在此基础上研究如何使用 Node.js 传输层安全性(TLS)模块来保护您的服务器。最后,通过 crypto 模块和第三方身份验证模块对凭证进行加密,您将完成 Node.js 安全领域的旅程。

6-1.分析哈希算法的类型

问题

您有一个命令行界面,通过它可以访问 Node.js 用户可以使用的所有散列算法,以便为他们的数据创建散列。在发布这个应用之前,您需要更好地理解每个算法。

解决办法

这个问题的解决方案其实很简单。您将构建 Node.js 中当前可用散列算法的列表。然后,您将在“如何工作”一节中看到这些散列的设计数量以及它们的常见用途。首先,您将构建一个散列列表,如清单 6-1 中的所示。

清单 6-1 。构建 Node.js 中可用的散列列表

/**
 * Hashes
 */

var crypto = require('crypto'),
    hashes = crypto.getHashes();

console.log(hashes.join(', '));

现在运行这段代码来获取 Node.js 的可用散列的完整列表。

清单 6-2 。Node.js 中可用的哈希

$ node 6-1-1.js
DSA-SHA1-old, dsa, dsa-sha, dsa-sha1, dsaEncryption, dsaWithSHA, dsaWithSHA1, dss1, ecdsa-with-SHA1, md4, md4WithRSAEncryption, md5, md5WithRSAEncryption, mdc2, mdc2WithRSA, ripemd, ripemd160, ripemd160WithRSA, rmd160, rsa-md4, rsa-md5, rsa-mdc2, rsa-ripemd160, rsa-sha, rsa-sha1, rsa-sha1-2, rsa-sha224, rsa-sha256, rsa-sha384, rsa-sha512, sha, sha1, sha1WithRSAEncryption, sha224, sha224WithRSAEncryption, sha256, sha256WithRSAEncryption, sha384, sha384WithRSAEncryption, sha512, sha512WithRSAEncryption, shaWithRSAEncryption, ssl2-md5, ssl3-md5, ssl3-sha1, whirlpool

其中一些已被弃用(例如“DSA-SHA1-old”),或者不是真正的加密哈希函数,而是其他加密有用的实现。也就是说,RSA 加密并不是真正的散列函数,但它可以以利用散列的方式被利用。在本节中,您将重点关注数字签名算法(DSA)、消息摘要(MD4、MD5 等。)、安全哈希算法(SHA)和 WHIRLPOOL 哈希函数、它们的用途以及潜在的漏洞。

它是如何工作的

这通过利用 Node.js 加密模块来实现。构建此模块的目的是为您在构建 Node.js 应用时可能遇到的许多加密需求提供一个健壮的实现。getHashes方法是列出所有可用散列的快捷方式,它是运行 Node.js 的平台上可用的 OpenSSL 散列的列表。

在您开始在您的应用中使用这些散列之前,注意它们是如何工作的以及它们有什么好处是很重要的;以下小节将分解最常见的算法及其特性。一般来说,加密哈希是一种将数据或消息加密为固定长度摘要(称为哈希)的方法。这种固定长度的摘要将作为代表散列的原始数据的签名或指纹,而不会泄露原始数据的内容。下面是常用算法及其功能的列表。

目录系统代理(Directory System Agent)

这种类型的加密可以对最初由国家标准与技术研究所为 DSS (数字签名标准)提出的数据进行编码。因此,这两个缩写有时可以互换使用。应当注意,DSA 不是直接的散列,而是利用散列函数来生成加密值。这个散列函数最初被设计成利用 SHA-1,但是 SHA-2 也被使用;稍后您将会读到更多关于这些散列函数的内容。

MD4

MD4 散列仍在使用,但在许多情况下,它已被 MD5 和其他更高级的散列算法所取代。它已被指定为过时。MD4 旨在快速执行。它所做的是接受一个消息并将其加密成一个 128 位的摘要。

MD4 安全性不强。在创建后不久,发现很有可能会出现哈希冲突。这意味着,即使原始消息中的微小差异通常会创建一个唯一的散列,但有几种证据和方法可以从多个消息中创建相同的散列。因此,在 MD5 规范中改进了算法。

讯息摘要 5

MD5 是 MD4 的发展,目的是提高 hash 的安全性。它再次产生一个 128 位的散列,但是它牺牲了算法的速度,尽管是轻微的。您将看到的与 MD4 的主要区别是 MD5 引入了第四个辅助函数,用于处理散列的中间步骤。这些函数还包含一些额外的常数和与 MD4 的细微差异,以使散列更加安全。

尽管如此,MD5 散列仍然是不安全的,因为它仍然容易发生冲突,因此容易受到冲突攻击。然而,它仍然是非常流行的验证文件完整性,或检查文件中的变化。MD5 还有各种其他用途,包括通用唯一标识符(UUID 版本 3)和 CRAM-MD5(一种挑战-响应认证)等等。如上所述,它仍然是一种可靠的哈希算法,但由于其安全漏洞,对于强化的安全应用或保护 SSL 连接等操作,应该避免使用它。

重复一遍

RIPEMD 消息摘要有几种变体,其最初设计基于 MD4 算法。最常见的 RIPEMD 实现是 RIPEMD-160。它是原始 128 位散列的下一代变体,创建了一个 160 位散列,就像 SHA-1 一样。RIPEMD-160 目前没有任何碰撞漏洞,因为它也有望在未来十年内保持安全。RIPEMD-160 比 SHA-1 稍慢,这可能是它没有被广泛使用的一个原因。然而,它被用于相当好的隐私(PGP)加密。RIPEMD 未被广泛使用的另一个原因是,它不像 SHA-1 那样作为事实上的标准由国家标准与技术研究所进行推广。

恒星时角

SHA 有几种变体,其中大部分都可以在 Node.js 中找到。

最初的 SHA 算法现在被称为 SHA-0,在 Node.js getHashes函数中作为 SHA 提供,它是一个 160 位的哈希算法,已知它可能会发生冲突。由于这个原因,它已经不再流行,被该算法的较新版本所取代。

在 SHA-0 之后出现了 SHA-1 ,它仍然是当今计算中使用最广泛的加密哈希函数之一。像之前的 SHA-0 一样,SHA-1 也创建了一个 160 位的摘要。如今,几乎所有最流行的安全软件协议中都使用了 SHA-1。它用于安全套接字层(SSL)、安全外壳(SSH)、TLS 和 IP 安全(IPsec)协议,以及成千上万个在 Git 版本控制系统中包含散列文件的其他实现。然而,理论上已经表明 SHA-1 具有冲突漏洞,因此已经努力创建基于 SHA-1 的甚至更安全的散列算法。

SHA-2 是 SHA-256 (256 位摘要)、SHA-224 (224 位摘要)、SHA-384 (384 位摘要)和 SHA-512 (512 位摘要)的信封名称,所有这些都可以在 Node.js 中使用。这些代表了 SHA-1 算法的发展。224 位的变体是 256 位的截断;同样,384 是 512 的一个截断。

SHA-2 散列已经在许多与 SHA-1 相同的地方实现了,包括 SSL、TLS、PGP 和 SSH。它也是比特币哈希方法以及许多平台上的下一代密码哈希方法的一部分。

漩涡

WHIRLPOOL 算法 是 Node.js 中提供的另一种哈希算法。该算法产生 512 位哈希,目前尚未发现有冲突漏洞。它已被国际标准组织作为一种标准采用,但它没有 MD5 和 SHA 系列得到那么多的支持。

在编写 Node.js 应用时,您可以使用各种散列算法,每种算法都有自己的加密算法和潜在的漏洞。在保护您的应用时,您应该仔细考虑每一个散列,并确保在评估加密散列时仔细检查每一件事情。

6-2.使用 createHash 散列数据

问题

您已经检查了 Node.js 中可用的不同类型的散列,现在您想要实现一个散列来保护消息。

解决办法

在这个解决方案中,您将通过查看 Node.js 如何散列两种不同形式的输入来检查可用的加密散列函数:空字符串和非空字符串(参见清单 6-3 )。清单 6-4 展示了一个 SHA-1 和 MD5 散列算法的例子,它们有不同的编码。

清单 6-3 。使用各种散列算法

/**
 * Analyzing types of data
 */

var crypto = require('crypto'),
    hashes = crypto.getHashes();

hashes.forEach(function(hash) {

    ['', 'The quick brown fox jumped over the lazy dog.'].forEach(function(txt) {
        var hashed;
        try {
            hashed =crypto.createHash(hash).update(txt).digest('hex');
        } catch (ex) {
            if (ex.message === 'Digest method not supported') {
                // not supported for this algo
            } else {
                console.log(ex, hash);
            }
        }

        console.log(hash, hashed);
    });
});

清单 6-4 。使用不同摘要编码的散列法

/**
 * Different Encodings
 */

var crypto = require('crypto'),
        message = 'this is a message';

console.log('sha1');
console.log(crypto.createHash('sha1').update(message).digest('hex'));
console.log(crypto.createHash('sha1').update(message).digest('base64'));
console.log(crypto.createHash('sha1').update(message).digest('binary'));

console.log('md5');
console.log(crypto.createHash('md5').update(message).digest('hex'));
console.log(crypto.createHash('md5').update(message).digest('base64'));
console.log(crypto.createHash('md5').update(message).digest('binary'));

算法之间的哈希差别很大。例如,使用 MD5 的空字符串的散列看起来像d41d8cd98f00b204e9800998ecf8427e,而使用 SHA-1 散列的相同字符串是da39a3ee5e6b4b0d3255bfef95601890afd80709。当然,当使用不同的编码时,它们有不同的外观,但是散列是相同的。

它是如何工作的

为了理解 Node.js 中这些函数的散列,您必须首先检查生成散列摘要的过程,然后您可以查看 crypto 模块的createHash方法的源代码。

在上面的解决方案中,您首先看到了如何跨 Node.js 中可用的各种哈希算法实现createHash方法。createHash函数接受一个算法,该算法必须是创建哈希可接受的算法。例如,如果您要使用不存在的“cheese”算法,您将得到错误“不支持摘要方法”。

在将算法传递给createHash方法之后,现在就有了 Node.js 散列类的一个实例。这个类是一个可读写的流。现在您需要向 Hash 类的update()方法发送一条您想要散列的消息。更新功能接收消息和(可选)该消息的编码。如果不是,则定义输入编码,可以是'utf8'、' ?? '、' ?? ';则假设输入是缓冲器。这意味着当新数据被读入流中时,这个方法可能被多次调用。

在散列流上调用 update 方法之后,现在就可以创建摘要,或者散列的实际输出。这个函数将接受一个输出编码,如果没有提供,则再次默认为一个缓冲区。可用的编码类型有'hex'、' ?? '、' ?? '。通常在哈希算法中,比如检查shasum (SHA-1 哈希),正如你将在下一节中看到的,更典型的是利用“hex”,但是在清单 6-4 中,你看到了一个使用多种输出编码的例子。

现在您可以检查 Node.js 源代码中的createHash方法和hash对象的源代码,如清单 6-5 所示。

清单 6-5 。创建哈希源

exports.createHash = exports.Hash = Hash;
function Hash(algorithm, options) {
  if (!(this instanceof Hash))
    return new Hash(algorithm, options);
  this._binding = new binding.Hash(algorithm);
  LazyTransform.call(this, options);
}

util.inherits(Hash, LazyTransform);

Hash.prototype._transform = function(chunk, encoding, callback) {
  this._binding.update(chunk, encoding);
  callback();
};

Hash.prototype._flush = function(callback) {
  var encoding = this._readableState.encoding || 'buffer';
  this.push(this._binding.digest(encoding), encoding);
  callback();
};

Hash.prototype.update = function(data, encoding) {
  encoding = encoding || exports.DEFAULT_ENCODING;
  if (encoding === 'buffer' && typeof data === 'string')
    encoding = 'binary';
  this._binding.update(data, encoding);
  return this;
};

Hash.prototype.digest = function(outputEncoding) {
  outputEncoding = outputEncoding || exports.DEFAULT_ENCODING;
  return this._binding.digest(outputEncoding);
};

您可以注意上面讨论的createHash、update 和 digest 函数,但是要特别注意binding.Hash(algorithm)方法。这是 Node.js 绑定到驱动 Node.js 核心的 C++的地方。这实际上是处理 OpenSSL 散列的地方,也是发生诸如算法不存在时抛出错误之类的事情的地方。除此之外,您会看到 hash 流检查可选的编码值,适当地设置它们,然后将关于 C++绑定方法的请求发送给调用者。

现在,您已经看到了如何在 Node.js 中对消息进行散列,方法是选择一种算法,用消息更新散列,然后用您选择的编码创建摘要。在下一节中,您将研究如何使用常见的哈希函数来验证文件的完整性。

6-3.使用哈希验证文件完整性

问题

您需要利用加密哈希算法来验证 Node.js 应用中使用的文件的完整性。

解决办法

您可能经常会遇到这样的情况,即您需要检查在 Node.js 应用中访问的文件的内容是否与以前的版本有所不同。您可以使用 Node.js 加密模块通过生成文件内容的散列来实现这一点。

在本解决方案中,您将创建一个应用,演示四种哈希算法处理读取文件内容任务的能力。在清单 6-6 中,你将传入你想要散列的文件,然后你的应用将读取文件流,当流被读取时更新消息,然后记录每个算法的结果散列。

清单 6-6 。检查文件完整性

/**
 * Checking File Integrity
 */

var fs = require('fs'),
      args = process.argv.splice('2'),
      crypto = require('crypto');
var algorithm = ['md5', 'sha1', 'sha256', 'sha512'];

algorithm.forEach(function(algo) {
        var hash = crypto.createHash(algo);

        var fileStream = fs.ReadStream(args[0]);

        fileStream.on('data', function(data) {
                hash.update(data);
        });

        fileStream.on('end', function() {
                console.log(algo);
                console.log(hash.digest('hex'));
        });
});

您可以想象在 Node.js 中构建一个部署流程,在该流程中,您从远程服务器发送或检索文件,并且您希望确保这些文件与它们来自的内容的预期散列相匹配。

您可能还需要下载一个文件。下载文件后,您可能需要检查该文件的完整性,根据您知道的准确值进行验证。在清单 6-7 的中,您下载了 Windows 0 . 10 . 10 版本的 Node 可执行文件,并对照这个散列的已知值检查shasum

清单 6-7 。下载文件并检查 shasum

/**
 * Verifying file integrity
 */

var http = require('http'),
        fs = require('fs'),
        crypto = require('crypto');

var node_exe = fs.createWriteStream('node.exe');

var req = http.get('http://nodejs.org/dist/v0.10.10/node.exe', function(res) {
        res.pipe(node_exe);
        res.on('end', function() {
                var hash = crypto.createHash('sha1');

                readr = fs.ReadStream('node.exe');

                readr.on('data', function(data) {
                       hash.update(data);
                });

                readr.on('end', function() {
                       // Should match 419fc85e5e16139260f7b2080ffbb66550fbe93f  node.exe
                       // from http://nodejs.org/dist/v0.10.10/SHASUMS.txt
                       var dig = hash.digest('hex');
                       if (dig === '419fc85e5e16139260f7b2080ffbb66550fbe93f') {
                              console.log('match');
                       } else {
                              console.log('no match');
                       }
                       console.log(dig);
                });
        });
});

它是如何工作的

清单 6-6 中的解决方案首先需要加密和文件系统模块,两者都是实现所需要的。然后,创建一组不同的算法用于散列。正如您在前面几节中看到的,最常用的两种是 MD5 和 SHA-1,但是 SHA-2 的变体也越来越流行。

当您遍历每个算法时,您可以使用crypto.getHash创建一个新的散列,传递要使用的算法。然后使用fs.ReadStream创建一个文件流,在命令行上传递一个作为参数提供的文件名。您可以轻松地传递任何要从应用中读取的文件名。当 Node.js 读取文件时,会发出数据事件。

在对数据事件侦听器的回调中,您开始处理散列的消息部分。这是通过将数据直接传递给散列的update()函数来实现的。文件读取完毕后,将发出“end”事件。在这个回调中,您将实际生成散列摘要并将结果记录到控制台。

当您对文件进行哈希处理时,很容易看出内容的细微变化会导致不同的哈希。例如,仅包含字符串“这是文本”的文件将生成一个shasum,它完全独立于包含字符串‘This is test’的类似文件的shasum

在第二个例子中,清单 6-7 ,你创建了一个非常实用的解决方案,一旦一个文件被下载到你的服务器上,你就用一个shasum来检查它的完整性。为此,您需要导入 http、文件系统和加密模块。这个实现从在您的文件系统上创建一个可通过fs.createWriteStream写入的文件开始。接下来,向文件源发出一个 HTTP GET 请求;在这种情况下,您将检索 0.10.10 版 Windows 的 Node.js 可执行文件。您知道文件应该匹配的正确的shasum,因为它可以从这个版本的下载页面免费获得。

来自这个 GET 请求的响应随后被传送到您使用response.pipe(file)创建的文件中。一旦响应完成,用‘end’事件表示,您就可以从文件系统中读取文件了。就像在前面的例子中一样,您使用给定的算法(在本例中是 SHA-1 算法)创建一个散列,并在文件读取器流的“数据”事件上更新消息。一旦读取完成,您就可以通过对散列调用 digest('hex ')方法来生成散列。现在,您已经获得了下载文件的 shasum,您可以将它与预期值进行比较,以确保下载是完整的并且没有损坏。

检查系统上文件的完整性是至关重要的,但是如果你只是想获得远程文件的散列,你可以通过直接散列响应流来生成文件的 shasum,如清单 6-8 所示。

清单 6-8 。散列 HTTP 响应流

/**
* Verifying file integrity
 */

var http = require('http'),
        fs = require('fs'),
        crypto = require('crypto');

var req = http.get('http://nodejs.org/dist/v0.10.10/node.exe', function(res) {
        var hash = crypto.createHash('sha1');

        res.on('data', function(data) {
                hash.update(data);
        });

        res.on('end', function() {
                console.log(hash.digest('hex'));
        });
});

这消除了文件写入,并在响应流发出“数据”事件时简单地更新散列,最终在响应结束时生成摘要。

6-4.使用 HMAC 来验证和认证消息

问题

您需要使用 HMAC 对 Node.js 应用中的消息进行身份验证。

解决办法

要生成 HMAC,您需要对本章前面几节中概述的加密哈希函数有一个基本的了解。Node.js 提供了一个类似于createHash函数的方法,可以让你轻松地用 HMAC 保护你的消息。清单 6-9 展示了如何在 Node.js 中实现一个基于 SHA-1 的 HMAC,并直接与 SHA-1 散列法进行了比较。

清单 6-9 。使用 HMAC

/**
 * Using Hash-Based Message Authentication
 */

var crypto = require('crypto')
        secret = 'not gonna tell you';

var hash = crypto.createHash('sha1').update('text to keep safe').digest('hex');
var hmac = crypto.createHmac('sha1', secret).update('text to keep safe').digest('hex');

console.log(hash);
console.log(hmac);

这将生成两个 160 位编码哈希的输出,但即使它们编码的是相同的文本,它们也有很大的不同,因为 HMAC 使用密钥来验证哈希。

SHA-1·哈希对 SHA-1·HMAC

f59de5dd5f2d5c49e45e1317448031baa38ab7e9
c6b314dbebdd4ff17d0fc84e9ee0d5ab5821df5f

它是如何工作的

HMAC 创建了一种验证消息完整性的方法,就像任何加密散列一样,而且由于集成了密钥,它还允许您验证消息的真实性。这可能不是加密函数的圣杯,但它超越了简单散列消息的安全性,因为它确实提供了一种额外的验证方法。这将有助于您断言您的消息在传输过程中没有被更改,提供了比更高级别的加密技术更高的保证级别。

理论上,当您创建消息的 SHA-1 散列时,通过将秘密与消息连接起来,您可以生成附有密钥的有效消息,但是这种结构仍然存在漏洞。这是因为,如果攻击者能够确定消息,他或她就可以通过编程方式提取密钥,并访问他或她尚未遇到的消息上的受保护信息。这很危险。

基本上,SHA-1 的 HMAC 实际上是在两次传达 SHA-1 的信息。这自动意味着您的散列更好,但仍然没有被认证。然而,在 HMAC 中,密钥被添加到消息中,这样 HMAC 就成为可认证的摘要。您知道您的 HMAC 的发送者是实际的发送者,因为当您比较 HMAC 的摘要时,传递的密钥被自动验证为可信的。这是独立构建的,因此不管您遇到了什么消息,以及传递了什么密钥,您都无法确定即将到来的秘密的信息。如上所述,这提供了额外的保证,即您的消息没有被更改。

在您的解决方案中,您通过调用createHmac函数来创建 HMAC。该函数接受您将在 HMAC 中使用的哈希算法类型,并且它还将在构造函数参数中获取密钥。在实例化之后,crypto.Hmac被创建,具有相同方法的流程作为散列对象被附加到原型。区别在于 C++绑定指向HMAC绑定,而不是散列,这意味着处理实现了正确的 HMAC 算法。然后,C++解析 HMAC 的参数,然后通过利用 OpenSSL HMAC 实现来处理 HMAC,以生成最终结果。

Node.js HMAC 实现

//hmac in crypto.js
exports.createHmac = exports.Hmac = Hmac;

function Hmac(hmac, key, options) {
  if (!(this instanceof Hmac))
    return new Hmac(hmac, key, options);
  this._binding = new binding.Hmac();
  this._binding.init(hmac, toBuf(key));
  LazyTransform.call(this, options);
}

util.inherits(Hmac, LazyTransform);

Hmac.prototype.update = Hash.prototype.update;
Hmac.prototype.digest = Hash.prototype.digest;
Hmac.prototype._flush = Hash.prototype._flush;
Hmac.prototype._transform = Hash.prototype._transform;

//hmac in node_crypto.cc
void Hmac::Initialize(v8::Handle<v8::Object> target) {
  HandleScope scope(node_isolate);

  Local<FunctionTemplate> t = FunctionTemplate::New(New);

  t->InstanceTemplate()->SetInternalFieldCount(1);

  NODE_SET_PROTOTYPE_METHOD(t, "init", HmacInit);
  NODE_SET_PROTOTYPE_METHOD(t, "update", HmacUpdate);
  NODE_SET_PROTOTYPE_METHOD(t, "digest", HmacDigest);

  target->Set(FIXED_ONE_BYTE_STRING(node_isolate, "Hmac"), t->GetFunction());
}

void Hmac::New(const FunctionCallbackInfo<Value>& args) {
  HandleScope scope(node_isolate);
  Hmac* hmac = new Hmac();
  hmac->Wrap(args.This());
}

void Hmac::HmacInit(const char* hash_type, const char* key, int key_len) {
  HandleScope scope(node_isolate);

  assert(md_ == NULL);
  md_ = EVP_get_digestbyname(hash_type);
  if (md_ == NULL) {
    return ThrowError("Unknown message digest");
  }
  HMAC_CTX_init(&ctx_);
  if (key_len == 0) {
    HMAC_Init(&ctx_, "", 0, md_);
  } else {
    HMAC_Init(&ctx_, key, key_len, md_);
  }
  initialised_ = true;
}

void Hmac::HmacInit(const FunctionCallbackInfo<Value>& args) {
  HandleScope scope(node_isolate);

  Hmac* hmac = ObjectWrap::Unwrap<Hmac>(args.This());

  if (args.Length() < 2 || !args[0]->IsString()) {
    return ThrowError("Must give hashtype string, key as arguments");
  }

  ASSERT_IS_BUFFER(args[1]);

  const String::Utf8Value hash_type(args[0]);
  const char* buffer_data = Buffer::Data(args[1]);
  size_t buffer_length = Buffer::Length(args[1]);
  hmac->HmacInit(*hash_type, buffer_data, buffer_length);
}

bool Hmac::HmacUpdate(const char* data, int len) {
  if (!initialised_) return false;
  HMAC_Update(&ctx_, reinterpret_cast<const unsigned char*>(data), len);
  return true;
}

void Hmac::HmacUpdate(const FunctionCallbackInfo<Value>& args) {
  HandleScope scope(node_isolate);

  Hmac* hmac = ObjectWrap::Unwrap<Hmac>(args.This());

  ASSERT_IS_STRING_OR_BUFFER(args[0]);

  // Only copy the data if we have to, because it's a string
  bool r;
  if (args[0]->IsString()) {
    Local<String> string = args[0].As<String>();
    enum encoding encoding = ParseEncoding(args[1], BINARY);
    if (!StringBytes::IsValidString(string, encoding))
      return ThrowTypeError("Bad input string");
    size_t buflen = StringBytes::StorageSize(string, encoding);
    char* buf = new char[buflen];
    size_t written = StringBytes::Write(buf, buflen, string, encoding);
    r = hmac->HmacUpdate(buf, written);
    delete[] buf;
  } else {
    char* buf = Buffer::Data(args[0]);
    size_t buflen = Buffer::Length(args[0]);
    r = hmac->HmacUpdate(buf, buflen);
  }

  if (!r) {
    return ThrowTypeError("HmacUpdate fail");
  }
}

bool Hmac::HmacDigest(unsigned char** md_value, unsigned int* md_len) {
  if (!initialised_) return false;
  *md_value = new unsigned char[EVP_MAX_MD_SIZE];
  HMAC_Final(&ctx_, *md_value, md_len);
  HMAC_CTX_cleanup(&ctx_);
  initialised_ = false;
  return true;
}

void Hmac::HmacDigest(const FunctionCallbackInfo<Value>& args) {
  HandleScope scope(node_isolate);

  Hmac* hmac = ObjectWrap::Unwrap<Hmac>(args.This());

  enum encoding encoding = BUFFER;
  if (args.Length() >= 1) {
    encoding = ParseEncoding(args[0]->ToString(), BUFFER);
  }

  unsigned char* md_value = NULL;
  unsigned int md_len = 0;

  bool r = hmac->HmacDigest(&md_value, &md_len);
  if (!r) {
    md_value = NULL;
    md_len = 0;
  }

  Local<Value> rc = StringBytes::Encode(
        reinterpret_cast<const char*>(md_value), md_len, encoding);
  delete[] md_value;
  args.GetReturnValue().Set(rc);
}

6-5.回顾 OpenSSL 密码和安全性

问题

作为 Node.js 开发人员,您需要对 OpenSSL 密码有一个高层次的理解。

解决办法

Node.js 为 OpenSSL 密码提供了一个包装器。因此,您可以使用的密码是通过 OpenSSL 获得的。要查看这些可用的密码,您可以运行一个简单的程序(参见清单 6-10 )来输出 Node.js 中可用的各种密码。

清单 6-10 。Node.js 可用的密码

/**
 * Reviewing ciphers
 */

var crypto = require('crypto');

var ciphers = crypto.getCiphers();
console.log(ciphers.join(', '));

在清单 6-11 的中,您可以看到getCiphers()函数的输出。这些是所有可用的密码,其中几个将在它如何工作一节中讨论。

清单 6-11 。crypto.getCiphers()

[ 'CAST-cbc', 'aes-128-cbc', 'aes-128-cbc-hmac-sha1', 'aes-128-cfb', 'aes-128-cfb1', 'aes-128-cfb8', 'aes-128-ctr', 'aes-128-ecb', 'aes-128-gcm', 'aes-128-ofb', 'aes-128-xts', 'aes-192-cbc', 'aes-192-cfb', 'aes-192-cfb1', 'aes-192-cfb8', 'aes-192-ctr', 'aes-192-ecb', 'aes-192-gcm', 'aes-192-ofb', 'aes-256-cbc', 'aes-256-cbc-hmac-sha1', 'aes-256-cfb', 'aes-256-cfb1', 'aes-256-cfb8', 'aes-256-ctr', 'aes-256-ecb', 'aes-256-gcm', 'aes-256-ofb', 'aes-256-xts', 'aes128', 'aes192', 'aes256', 'bf', 'bf-cbc', 'bf-cfb', 'bf-ecb', 'bf-ofb', 'blowfish', 'camellia-128-cbc', 'camellia-128-cfb', 'camellia-128-cfb1', 'camellia-128-cfb8', 'camellia-128-ecb', 'camellia-128-ofb', 'camellia-192-cbc', 'camellia-192-cfb', 'camellia-192-cfb1', 'camellia-192-cfb8', 'camellia-192-ecb', 'camellia-192-ofb', 'camellia-256-cbc', 'camellia-256-cfb', 'camellia-256-cfb1', 'camellia-256-cfb8', 'camellia-256-ecb', 'camellia-256-ofb', 'camellia128', 'camellia192', 'camellia256', 'cast', 'cast-cbc', 'cast5-cbc', 'cast5-cfb', 'cast5-ecb', 'cast5-ofb', 'des', 'des-cbc', 'des-cfb', 'des-cfb1', 'des-cfb8', 'des-ecb', 'des-ede', 'des-ede-cbc', 'des-ede-cfb', 'des-ede-ofb', 'des-ede3', 'des-ede3-cbc', 'des-ede3-cfb', 'des-ede3-cfb1', 'des-ede3-cfb8', 'des-ede3-ofb', 'des-ofb', 'des3', 'desx', 'desx-cbc', 'id-aes128-GCM', 'id-aes192-GCM', 'id-aes256-GCM', 'idea', 'idea-cbc', 'idea-cfb', 'idea-ecb', 'idea-ofb', 'rc2', 'rc2-40-cbc', 'rc2-64-cbc', 'rc2-cbc', 'rc2-cfb', 'rc2-ecb', 'rc2-ofb', 'rc4', 'rc4-40', 'rc4-hmac-md5', 'seed', 'seed-cbc', 'seed-cfb', 'seed-ecb', 'seed-ofb' ]

它是如何工作的

密码是一种通过使用一套算法来加密和解密数据的方法。正如你在本节的解答中看到的,有许多算法可供你使用。其中许多是块密码,或作用于固定数据块的密码,而不是作用于数据流并将明文转换为加密形式或密文的密码。每种密码都有自己的实现,我将详细讨论。

image 本节讨论一些相对复杂的关于各种密码的算法和实现的材料。这里定义了一些在提到加密算法时常见的术语,供您参考。

攻击媒介:一组针对安全漏洞的恶意代码。

分组:通常在分组密码中使用的一组特定大小的比特。

分组密码:一种在单个分组内运行的密码,在不同的分组上进行排列,直到得到最终的密文。

密文:使用密码加密明文的最终结果。

置换:数据的一轮处理或转换。

相关密钥攻击:一种攻击手段,目标是使用多个数学上相关的密钥的密码。这些密码的结果可用于推断密码和泄露加密值。

是吗

DES 代表数据加密标准,是 20 世纪 70 年代 IBM 最初设计的一种分组密码。DES 使用 64 位的密码块大小,密钥大小也是 64 位。该算法将采用 64 位明文块,对该块进行初始排列,将该块分成两个 32 位的半块,然后通过对密钥的一部分进行异或运算,以交替的方式处理它们。这个过程重复 16 轮,直到出现最终的排列。结果是 DES 密文。

像其他密码一样,DES 容易受到强力攻击,攻击者可以对所有可能的密钥进行检查。因为 DES 中的密钥长度是 56 位(64 减去奇偶校验的最后 8 位),所以密钥相对较短,从而使得暴力攻击可行。然而,尽管 DES 易受攻击,但直到它在市场上出现了 20 多年后,攻击才得以成功演示。

由于其脆弱性,DES 不被许多应用所青睐;然而,有一种替代实现仍然被广泛使用:三重 DES。

三重 DES 是一种增加 DES 算法密钥大小的方法,它实际上是将该过程运行三次。整体设计相同,但选择了三个键。第一个密钥用于加密明文。然后使用第二密钥来解密第一加密。最后,第三个密钥再次运行 DES 以生成密文。这些密钥可以全部相同,一个不同,或者全部三个不同,它们的强度根据密钥的不同而不同,因为本质上是由您决定密码的密钥长度。虽然仍有已知的对三重 DES 的攻击,但它比 DES 本身更安全。

RC2〔??〕

RC2(或 Rivest Cipher 2),也是一种分组密码,是由 RSA 著名的 Ron Rivest 在 20 世纪 80 年代末创造的。像 DES 一样,RC2 密码由 64 位块组成,在算法中包含 18 轮。有 16 轮“混合”和 2 轮“捣碎”RC2 算法的密钥大小在 8 到 128 位之间变化,默认为 64 位。这种密码存在一个已知的相关密钥攻击漏洞。

RC4

RC4 是一个流密码,也是由罗恩·里维斯特在 20 世纪 80 年代末设计的。众所周知,它的速度和简单。这种密码的工作原理是生成一个用于加密的近似随机位流。这分两步进行:首先,有一个数组生成步骤,然后是伪随机生成步骤。通过一次两个地循环遍历数组的半随机字节,交换数组中每个字节的值,然后以 256 为模对这些值进行处理,从而生成输出。结果用于在字节数组中查找该操作的总和。

RC4 已被广泛应用于许多应用中,如 TLS、有线等效保密(WEP)和保护无线电脑网络安全系统(WPA)。然而,部分由于伪随机值,攻击向量并不是不可穿透的。正因为如此,在 2001 年,无线网络的 WEP 加密受到攻击,这促使了无线加密的后续实现。

CAST 是分组密码。它广泛用于 PGP 和 GNU 隐私保护(GPG)加密版本。该算法本身利用 40 到 128 位的密钥大小,并且将运行 12 轮或 16 轮,尽管 12 轮仅在密钥大小小于 80 位时出现。底层函数由八个 32 位替换框组成,这些替换框基于其他各种算法,如 XOR、模加法、bent 函数和旋转。CAST 密码中使用了三种不同的循环函数。round 函数的第一个版本用于第 1、4、7、10、13、16 轮;第二个在第 2、5、8、11、14 轮;第三次是第 3、6、9、12 和 15 轮。

山茶

山茶花密码 是另一种 128 位分组密码,其分组大小为 16 字节。密钥大小在 128、192 和 256 位之间变化。Camellia 是另一种 Feistel 密码,如果使用 128 位密钥,它将使用 18 轮,如果使用更大的密钥,它将使用 24 轮。像 CAST 一样,Camellia 使用替换盒。对于 Camellia,这些盒是 8 位乘 8 位的盒,并且使用了其中的四个。每六个回合有一个特殊的变换应用于这个密码。

河豚

河豚密码 是布鲁斯·席耐尔设计的分组密码。尽管它容易受到包括差分攻击在内的传播媒介的攻击,但它仍然受到高度重视。块大小为 64 位,密钥可以是 32 到 448 位之间的任何值。它使用 16 发子弹和大型 S 盒。在 150 Hz 的 Pentium 上,该算法的速度为 8.3 MB/s。

有几个著名的密码管理产品利用河豚。其中包括 1 密码、密码保险箱和密码钱包等。它也用于 GPG 和许多文件和磁盘加密软件。

俄歇电子能谱

AES (又名 Rijndael),或高级加密标准,是一种旨在取代 DES 的加密算法。AES 的块大小为 128 位,密钥大小可以是 128、192 或 256 位。AES 对 128 位密钥进行 10 轮运算,对 192 位密钥进行 12 轮运算,对 256 位密钥进行 14 轮运算。对 4 字节乘 4 字节的矩阵进行 AES 密码运算的过程称为“状态”该过程首先通过使用 Rijndael 密钥表来扩展密钥,然后可以开始回合。

第一轮被称为“AddRoundKey”,它提取一个子密钥,并通过使用 XOR 来组合状态中的一个字节。

这将开始剩余的回合,但不包括最后一轮。这些循环从执行“子字节”步骤开始,该步骤通过 8 位替换框替换“状态”中的每个字节。接下来是“ShiftRows”步骤,该步骤将按照设定的量移动所有行的值。每行的数量不同。下一步是“混合列”步骤。在该步骤中,通过使用可逆线性变换来组合“状态”中的列。通过这一步,每一列通过与一个已知的多项式或矩阵相乘来进行转置,从而得到最终的混合列。然后还有另一个“AddRoundKey”步骤。

轮次完成后,除了省略“混合列”步骤之外,最后一轮次的操作方式与前面的轮次相同。结果是 AES 密文。

AES 容易受到相关密钥攻击、区分攻击和密钥恢复攻击。然而,这些攻击的复杂性是不容忽视的,AES 仍然是基本安全的。事实上,它可以说是当今实践中最广泛使用的加密密码。

它用于加密 7Zip、RAR 和 WinZip 实例中的文件归档。使用 AES 的其他地方是像 BitLocker 这样的磁盘加密技术。使用 AES 的还有 GPG、IPsec、IronKey、OpenSSL(node . js 的 crypto 派生的包装器)、Pidgin 和 Linux 内核 Crypto API。当然,今天有更多的地方使用 AES,但这只是少数。

当您构建 Node.js 应用时,有多种密码可供您使用。您应该选择适合您的解决方案的特定需求的实现,并跟上不断变化的标准和新的实现。

6-6.使用 OpenSSL 密码加密数据

问题

您已经对 Node.js 中可用的 OpenSSL 密码有了一些了解,现在您需要利用这些密码来加密数据。

解决办法

了解如何在您的代码中实现密码非常重要。为此,您将构建一个解决方案,它将获取一个密钥和一个文本字符串,然后利用 AES-256 算法从您的明文创建一个密文,并再次解密它,如清单 6-12 中的所示。

清单 6-12 。从明文创建密文

/**
 * encrypting data
 */

var crypto = require('crypto'),
        algo = 'aes256',
        key = 'cheese',
        text = 'the itsy bitsy spider went up the water spout';

var cipher = crypto.createCipher(algo, key);
var encrypted = cipher.update(text, 'utf8', 'hex') + cipher.final('hex');

console.log(encrypted);

var decipher = crypto.createDecipher(algo, key);
var decrypted = decipher.update(encrypted, 'hex', 'utf8') + decipher.final('utf8');

if (decrypted === text) {
        console.log('success!');
}

在应用中加密明文是很重要的,但是在某些情况下,您可能希望加密整个文件的内容,然后再解密该文本。这个解决方案需要使用文件系统模块,以便您能够读取文件的内容。

清单 6-13 。加密文件的内容

/**
 * using ciphers on files
*/

var crypto = require('crypto'),
        fs = require('fs'),
        algo = 'aes256',
        key = 'cheese';

var text = fs.readFileSync('6-6-1.txt', { encoding: 'utf8' });

var cipher = crypto.createCipher(algo, key);
var encrypted = cipher.update(text, 'utf8', 'hex') + cipher.final('hex');

console.log(encrypted);

var decipher = crypto.createDecipher(algo, key);
var decrypted = decipher.update(encrypted, 'hex', 'utf8') + decipher.final('utf8');

if (decrypted === text) {
        console.log('success!');
        console.log(text);
}

它是如何工作的

在 Node.js 中使用 crypto 模块时,createCipher函数是必不可少的。createCipher方法将接受一个算法和一个密码或密钥。此方法的结果是创建一个密码对象。密码对象是一个流,有三种方法。

Node.jscrypto.createCipher 对象

exports.createCipher = exports.Cipher = Cipher;
function Cipher(cipher, password, options) {
  if (!(this instanceof Cipher))
    return new Cipher(cipher, password, options);
  this._binding = new binding.CipherBase(true);

  this._binding.init(cipher, toBuf(password));
  this._decoder = null;

  LazyTransform.call(this, options);
}

util.inherits(Cipher, LazyTransform);

Cipher.prototype._transform = function(chunk, encoding, callback) {
  this.push(this._binding.update(chunk, encoding));
  callback();
};

Cipher.prototype._flush = function(callback) {

  this.push(this._binding.final());  callback();
};

如本节中的解决方案一样,要加密数据或数据串,可以使用 update 函数。update 方法接受您希望加密的数据、输入编码和输出编码。这种方法的结果是加密的数据,并且像加密散列一样,它可以在一个密码上被多次调用。一旦你完成了加密,你将调用接受输出编码的cipher.final函数,给你留下加密的结果。

密码更新和最终输出

Cipher.prototype.update = function(data, inputEncoding, outputEncoding) {
  inputEncoding = inputEncoding || exports.DEFAULT_ENCODING;
  outputEncoding = outputEncoding || exports.DEFAULT_ENCODING;

  var ret = this._binding.update(data, inputEncoding);

  if (outputEncoding && outputEncoding !== 'buffer') {
    this._decoder = getDecoder(this._decoder, outputEncoding);
    ret = this._decoder.write(ret);
  }

  return ret;
};

Cipher.prototype.final = function(outputEncoding) {
  outputEncoding = outputEncoding || exports.DEFAULT_ENCODING;
  var ret = this._binding.final();

  if (outputEncoding && outputEncoding !== 'buffer') {
    this._decoder = getDecoder(this._decoder, outputEncoding);
    ret = this._decoder.end(ret);
  }

  return ret;
};

最后,您可以通过使用 setAutoPadding(false)函数将密码输入数据的填充覆盖到块大小中。这个需要之前调用。最终()。

Cipher.prototype.setAutoPadding = function(ap) {
  this._binding.setAutoPadding(ap);
  return this;
};

正如您可能已经猜到的,用于逆转加密过程的crypto.createDecipher函数的工作方式与createCipher函数类似。确实如此,因为它在实例化过程中创建了一个解密对象。一旦创建了它,您就可以访问与 cipher 对象相同的 API。

node . js crypto . create deciper

exports.createDecipher = exports.Decipher = Decipher;
function Decipher(cipher, password, options) {
  if (!(this instanceof Decipher))
    return new Decipher(cipher, password, options);

  this._binding = new binding.CipherBase(false);
  this._binding.init(cipher, toBuf(password));
  this._decoder = null;

  LazyTransform.call(this, options);
}

util.inherits(Decipher, LazyTransform);

Decipher.prototype._transform = Cipher.prototype._transform;
Decipher.prototype._flush = Cipher.prototype._flush;
Decipher.prototype.update = Cipher.prototype.update;
Decipher.prototype.final = Cipher.prototype.final;
Decipher.prototype.finaltol = Cipher.prototype.final;
Decipher.prototype.setAutoPadding = Cipher.prototype.setAutoPadding;

在您的解决方案中,您创建了两个密文示例,并在同一个应用中破译了该文本。对于文件中的明文字符串的情况,这通过使用密码密钥‘cheese’并在createCipher方法中使用它来实现。这实际上与第二个示例没有什么不同,在第二个示例中,您使用文件系统读取文件的内容,按照 UTF-8 编码,并解密得到的密文以找到预期的结果。在这个例子中,您使用了 AES-256 密码算法,但是任何可接受的 OpenSSL 算法都可以。例如,您可以很容易地用“cast”或“camellia256”替换 aes256,前提是您在加密和解密数据之间保持一致。

6-7.使用 Node.js 的 TLS 模块保护您的服务器

问题

您有一个正在传输信息的 Node.js 服务器,您希望通过利用 Node.js 的 TLS 模块来确保传输的安全性。

解决办法

在 Node.js 中构建 TLS 服务器看起来很熟悉。它类似于你在第四章中创建的 HTTPS 服务器。这是因为 HTTP 服务器对象的基础架构是从 TLS 模块继承的。

要创建 TLS 服务器,您需要从 TLS 模块本身开始。然后,您将构建一个对您的服务器密钥和证书文件的引用,并将它们作为选项传递,如清单 6-14 所示。

清单 6-14 。创建 TLS 服务器

/**
* using TLS
*/

var tls = require('tls'),
        fs = require('fs');

var options = {
        key: fs.readFileSync('srv-key.pem'),
        cert: fs.readFileSync('srv-cert.pem')
};

tls.createServer(options, function(s) {
        s.write('yo');
        s.pipe(s);
}).listen(8888);

使用有效的密钥和证书创建了安全的 TLS 服务器后,您需要创建一个能够连接到它的客户端。这也是 Node.js 的一项功能。事实上,如清单 6-15 所示,它几乎等同于网络模块创建连接的能力;但是,您需要注意指向证书颁发机构和凭证,以便对安全传输进行身份验证。

清单 6-15 。TLS 连接

/**
* tls connection
*/

var tls = require('tls'),
        fs = require('fs');

var options = {
        key: fs.readFileSync('privatekey.pem'),
        cert: fs.readFileSync('certificate.pem'),
        ca: fs.readFileSync('srv-cert.pem')
};

var connection = tls.connect(8888, options, function() {
        if (connection.authorized) {
                console.log('authorized');
        } else {
                console.log(':( not authorized');
        }
});

connection.on('data', function(data) {
        console.log(data);
});

它是如何工作的

TLS 是一种加密发送到服务器和从服务器接收的数据的方法。在这个解决方案中,您创建了一个服务器,它利用了一个密钥和证书,该密钥和证书是通过终端中的 OpenSSL 命令生成的,正如您在第四章中看到的那样。

清单 6-16 。生成 OpenSSL 密钥

$ openssl genrsa -out srv-key.pem 1024
$ openssl req -new –key srv-key.pem -out src-crt-request.csr
$ openssl x509 -req -in srv-crt-request.csr -signkey srv-key.pem -out srv-cert.pem

一旦有了密钥和证书,就可以在调用 tls.createServer 时将它们传递给 options 对象,并告诉它监听端口 8888。createServer 函数不仅接受 options 参数,还接受回调。这个回调在到服务器的连接上发出,并通过函数沿着安全流传递。在您的解决方案中,您向流中写入一个字符串,然后通过管道将其输出。

当您创建服务器时,有更多的选项可用,例如为握手设置超时或拒绝未授权的连接。所有这些都被考虑在内,以保护您的服务器。

  • ca:可信证书的字符串或缓冲区的数组。
  • cert:包含保密增强邮件(PEM)格式的服务器证书密钥的字符串或缓冲区。(必需)
  • ciphers:描述要使用或排除的密码的字符串。
  • crl:PEM 编码的证书撤销列表(CRL)的字符串或字符串列表
  • handshakeTimeout:如果 SSL/TLS 握手在一定的毫秒数内没有完成,则中止连接。默认值为 120 秒。
  • honorCipherOrder:选择密码时,使用服务器的偏好设置,而不是客户端的偏好设置。
  • key:包含 PEM 格式的服务器私钥的字符串或缓冲区。(必需)
  • NPNProtocols:可能的下一个协议协商(NPN)协议的数组或缓冲区。(协议应根据其优先级排序。)
  • passphrase:私钥(或 pfx)的字符串或密码短语。
  • pfx:包含 PFX(或 PKCS #12)格式的服务器的私钥、证书和认证机构(CA)证书的字符串或缓冲区。(这与keycert,ca选项是互斥的。)
  • rejectUnauthorized:如果为真,服务器将拒绝任何未经所提供的 ca 列表授权的连接。(该选项仅在requestCert为真时有效;默认值为 false。)
  • requestCert:如果为真,服务器将向连接的客户端请求证书,并尝试验证该证书。(默认为false。)
  • sessionIdContext:包含会话恢复不透明标识符的字符串。
  • SNICallback:如果客户端支持服务器名称标识(SNI) TLS 扩展,将调用的函数。

有趣的是,这些值成为用来标识服务器的凭证的一部分。这是通过将相关参数传递给crypto.createCredentials函数来实现的。

清单 6-17 。创建服务器方法凭据

 var sharedCreds = crypto.createCredentials({
    pfx: self.pfx,
    key: self.key,
    passphrase: self.passphrase,
    cert: self.cert,
    ca: self.ca,
    ciphers: self.ciphers || DEFAULT_CIPHERS,
    secureProtocol: self.secureProtocol,
    secureOptions: self.secureOptions,
    crl: self.crl,
    sessionIdContext: self.sessionIdContext
  });

现在您有了一个安全的 TLS 服务器。你需要连接到它。要测试连接,只需打开一个终端窗口并连接。

image 注意在 Windows 上,默认不包含 OpenSSL。通过在http://openssl.org/related/binaries.html下载一个二进制文件,你可以很容易地把它添加到你的机器上。这将安装到您计算机上的 C:\OpenSSL-Win32。然后,您可以从 PowerShell 的 C:\OpenSSL-Win32\bin 目录中运行 OpenSSL。

清单 6-18 。连接到安全服务器

$ openssl s_client –connect localhost:8888

然而,更健壮的客户机,比如用 Node.js 构建的客户机,是可能的。为了在您的解决方案中构建您的客户端连接,您首先使用tls.connect创建一个连接。向其传递端口(和可选的 URL)。然后是 options 对象,您会注意到除了“ca”选项之外,它看起来与服务器的选项非常相似。这是服务器 CA 的值。因为服务器的凭证是自签名的,所以识别它的唯一方法是通过它本身。一旦连接,您就可以访问连接流。这个流有一个属性,它会告诉你是否真的通过了服务器的认证。从那里,一旦您被授权,您就可以执行通常在网络应用中可能执行的客户机-服务器交互,但是您现在有了 TLS 的附加安全性。

6-8.使用加密模块加密用户凭证

问题

您有一个 Node.js 应用,它需要对服务器进行身份验证,您需要确保它是加密的。

解决办法

如果您打算用 Node.js 构建任何类型的安全应用,您可能需要一种向数据库验证用户身份的方法。例如,假设您有一个在线购物车,您想让您的用户注册一个帐户,以便更快地结账,并向他们发送促销信息。您可以轻松地在数据存储中实现一个用户表或文档,它将保存用户名和密码,但是存储明文密码不是一个好主意。这也是完全不必要的,因为正如您将看到的,在使用 Node.js 加密模块时很容易增加安全性。

为了构建这个解决方案,假设您的用户刚刚向您的站点提交了一个密码,现在您希望将其存储为一个散列。这很棒,因为您根本不需要存储明文密码,而且通过使用 salt,您能够轻松地验证后续登录。清单 6-19 展示了如何创建这个实现的一个版本来存储凭证。

清单 6-19 。创建安全凭据

/**
* user credentials
*/

var crypto = require('crypto'),
        password = 'MySuperSecretPassword';

function getHmac(password, salt) {
        var out = crypto.createHmac('sha256', salt).update(password).digest('hex');
        return out;
}
function getHash(password, salt) {
        var out = crypto.createHash('sha256').update(salt + password).digest('hex');
        return out;
}

function getSalt() {
        return crypto.randomBytes(32).toString('hex');
}
var salt = getSalt();
var hmac = getHmac(password, salt);
var hash = getHash(password, salt);
console.log('my pwd: ', password, ' salted: ', salt, ' and hashed: ', hash);
console.log('hmac: ' , hmac);

它是如何工作的

当你看一看这是如何工作的,你会注意到这个解决方案基本上只有两个部分。首先,你生成一个随机的 salt,当你用createHashcreateHmac散列你的密码时,它将成为密钥;第二,你散列你的密码。

关于保护你的密码的正确方法有不同的观点。一些人认为像SHA-256 (SHA256[password])这样的东西足够安全。然而,大多数其他人会认为您需要对您的散列加盐,如本解决方案所示。接下来的争论是,你的散列应该有多大,它是否有必要是一个密码安全的伪随机数,或者是否任何随机字节的集合都可以工作。对于这个解决方案,您的代码获取一组随机的 32 字节。

您可以在getSalt函数中看到这一点。在这里,您可以访问加密模块和randomBytes函数,它实际上是一个加密安全的伪随机字节集合。Node.js 源代码中的randomBytes调用是从 JavaScript 到 C++实现的直接绑定。

现在您有了盐,您可以使用它来安全地散列您的密码。这在本解决方案中以两种方式进行了演示。一个是通过getHmac功能。该函数将使用您生成的盐来创建您的基于 SHA-256 的 HMAC。然后用 HMAC 更新密码,并生成十六进制编码的摘要。

使用Hmac方法的替代方法是在密码后面添加 salt,然后 SHA-256 得到结果,如getHash函数所示。

对于这两种方法来说,重要的是不要对每个用户使用相同的盐。如果您使用相同的 salt,您将容易受到字典反向查找攻击。这意味着,如果你和你的朋友使用相同的密码,他们的散列将是相同的,因为盐保持不变。狡猾的恶意来源将能够确定这些模式,并最终提取安全数据。然而,使用不同的盐,你不会碰到这种情况;因为生成的 salt 在加密上是可靠的,所以它们在相同的 salt 和密码散列上发生冲突的可能性被最小化到几乎不重要的程度。

6-9.使用第三方认证模块

问题

您需要在 Node.js 中对您的用户进行身份验证,为此,您需要利用适当的第三方模块进行身份验证。

解决办法

您可以想象您有一个在上一节中讨论过的基于 Node.js 的购物车。您知道需要一种安全的方式来存储用户登录和身份验证数据,但是您可能不愿意使用自己的身份验证模块。为了减轻这一点,你做了大量的研究,并找到了一些解决方案。其中一个在方法上类似于 rolling your own,但是它在 npm 中打包为一个名为“bcrypt”的模块。这个模块将允许随机盐生成,散列您的密码,并访问这些值,以便您能够将它们安全地存储在您的数据存储。这个实现如清单 6-20 所示。这个实现只是一个片段,你可以想象它是一个更大规模的应用。这个代码片段是一个应用中注册路由的示例,它将获取用户数据,如果没有找到用户,它将利用 bcrypt 为用户生成一个 salt 和 hash,并将其保存到数据存储中。

清单 6-20 。使用 bcrypt 散列密码

app.post("/register", function(req, res) {
  var usrnm = req.body.name;
  User.findOne({username: usrnm}, function(err, usrData) {
        if (usrData === null) {
          //create
          bcrypt.genSalt(10, function(err, salt) {

                bcrypt.hash(req.body.pwd, salt, function(err, hash) {

                  var newUser = new User({ username: usrnm, email: req.body.email, pwHash: hash });
                       newUser.save(function(err) {
                         if (err) {
                              res.send({name: usrnm, message: "failure", error: err});
                              return;
                         }
                         res.send({name: usrnm, message: "success"});
                       });
                  });
                });

        } else {
          //emit to client
          res.send({name: usrnm, message: "failure", error: "User already exists"});
        }
  });
});

您随后能够发现的另一种方法是构建在 express.js 框架上的方法。但这是 Mozilla Persona 身份提供者的扩展。

image Express.js 是一个非常流行的构建 web 应用的框架,你将在本书的后面读到更详细的内容。Mozilla Persona 是利用您的电子邮件地址作为您的身份提供者的一种方式,消除了您的用户对您的站点拥有特定密码的需要。

你可以通过安装一个与 Express.js 协同工作的模块“express-persona”来实现它,如清单 6-21 所示。

清单 6-21 。使用角色进行身份验证

require('express-persona')(app, {
        audience: 'http://localhost:3000', // Must match your browser's address bar
        verifyResponse: function(error, req, res, email) {
                var out;
                if (error) {
                       out = { status: 'failure', reason: error };
                       res.json(out);
                } else {
                       models.user.findOrCreate(email, function(result) {
                              if (result.status === 'okay' ) {
                                      out = { status: 'okay', user: result.user };
                              } else {
                                      out = { status: 'failure', reason: 'mongodb failed to find or create user' };
                              }
                              res.json(out);
                       });
                }
        }
});

You have seen two possible implementations of third-party authentication modules, but there is a seemingly limitless supply if you examine the npm registry. It is important to scrutinize all security implementations that you will utilize on your server or in your Node.js application.

它是如何工作的

您首先实现了一个利用了bcrypt模块的解决方案。这是通过使用命令npm install bcrypt安装的,然后需要代码中的模块。bcrypt着手解决哈希密码中易受字典攻击的任何潜在漏洞。

bcrypt溶液来源于河豚。它根据特定的密码实现密钥调度。在此之后,它通过创建一个缓慢的自适应散列算法,超越了正常的加盐和散列。速度慢是好事,因为这会导致攻击者无法执行同样多的操作,从而大幅增加随后破解密码所需的时间。

这个 Node.js 模块中的基本实现只需要创建一个 salt,然后将它传递给一个散列函数,得到一个散列密码。

清单 6-22 。用 bcrypt 生成 Salt 和 Hash

bcrypt.genSalt(10, function(err, salt) {
    bcrypt.hash(req.body.pwd, salt, function(err, hash) {
        //store password
    });
});

genSalt函数接受轮数(默认为 10)、种子长度(默认为 20)和回调,这是必需的。如果发生错误,回调将提供错误以及 salt 值。从这个 salt 中,您应该使用bcrypt.hash函数创建散列。这需要您希望加密的明文密码、salt 和一个回调。回调将产生密码的散列,然后您可以将它存储在您的数据存储中。

要在登录时解密密码并将其与数据存储中的值进行比较,可以调用bcrypt.compare方法。它接受要验证的密码作为第一个参数,然后接受来自数据库的哈希值。这将返回一个布尔值—如果匹配则为真,否则为假。

在解决方案的第二部分中,您看到了如何实现 Mozilla Persona 进行用户身份验证。这是通过使用适合 Express.js 应用的“express-persona”实现的。Mozilla Persona 还有其他实现,包括 persona-id,它不依赖于特定的框架,或者您可以推出自己的实现。

您会看到,在您的解决方案中,您需要该模块,然后向它传递您的 express.js 应用和一个对象。该对象包含目标受众,即您的应用的 URL。它还包含一个 verifyResponse 函数,该函数将在验证成功或失败时生成一个路径,允许您将用户信息存储在数据库中。这个 Node.js 实现的补充是客户端。

为了让客户端与服务器通信,您需要在源代码中包含 login.persona.org/include.js 脚本。然后,您需要为navigator.id.login()logout()事件注册事件。

清单 6-23 。绑定到角色登录和注销

document.querySelector("#login").addEventListener("click", function() {
  navigator.id.request();
}, false);

document.querySelector("#logout").addEventListener("click", function() {
  navigator.id.logout();
}, false);

Persona 还需要您实现一个监视功能来监听这些事件。当检测到一个时,它将向 Express.js 应用中的/persona/verify 或/persona/logout 路由发送一个 XMLHttpRequest。

清单 6-24 。角色导航器观察方法

navigator.id.watch({
  onlogin: function(assertion) {
    var xhr = new XMLHttpRequest();
    xhr.open("POST", "/persona/verify", true);
    xhr.setRequestHeader("Content-Type", "application/json");
    xhr.addEventListener("loadend", function(e) {
      var data = JSON.parse(this.responseText);
      if (data && data.status === "okay") {
        console.log("You have been logged in as: " + data.email);
      }
    }, false);

    xhr.send(JSON.stringify({
      assertion: assertion
    }));
  },
  onlogout: function() {
    var xhr = new XMLHttpRequest();
    xhr.open("POST", "/persona/logout", true);
    xhr.addEventListener("loadend", function(e) {
      console.log("You have been logged out");
    });
    xhr.send();
  }
});

如上所述,用户识别有很多第三方模块。这些都应该仔细实施,以确保正确保护用户的登录凭证。当然,您可以自己实现这些模块中涉及的安全原则。

摘要

Node.js 完全有能力创建和其他框架一样安全的应用。加密模块是 OpenSSL 哈希、密码和加密功能的包装,为生成安全哈希和密文以及保护数据安全提供了顶层支持。

Node.js 还提供了一个框架来保证与 TLS 模块的服务器通信安全。这允许您在客户端和服务器之间创建安全的连接,并对 HTTPS 上的 HTTP 流量进行加密。

您还看到了如何构建一个用户身份验证模块,以更安全的方式存储用户凭证。最后,您看到了如何在现有框架的基础上创建一种通过第三方模块进行身份验证的安全方法。

保护应用不是一项简单的任务,需要时间和研究来获得正确的解决方案和实施。您还需要保持最新并留意最佳实践的变化,它们如何影响您的应用,以及如何通过 Node.js 加密模块增强您的安全机制。

七、探索其他 Node.js 模块

您已经看到了许多不同的 Node.js 模块,您可以在希望构建的 Node.js 应用中利用这些模块。但是,在构建 Node.js 应用时,可以使用 Node.js 核心的许多其他模块和部分。这一章将涉及到一些本地 Node.js 模块,并扩展它们的实现,让你对这些模块有更好的理解。理解这些模块在构建 Node.js 应用中起着至关重要的作用是很重要的。

在本章中,您将使用域名系统(DNS)模块来解析远程服务器的主机名和 IP 地址。通过使用缓冲区,您将获得更好的流处理,并且您将看到应用的集群化。您将使用全局流程对象,利用计时器,并处理服务器请求上的查询字符串。您还将看到 Node.js 控制台中公开的内容,以及 Node.js 中可用的调试器 URL。

7-1.使用 DNS 创建简单的 DNS 服务器

问题

您希望能够在 Node.js 应用中从远程服务器获取信息。这些信息可以是 IP 地址或域名。

解决办法

Node.js 为您提供了一种访问远程服务器的域名、IP 地址和域名的方法。这可以通过创建一个接受域名的简单 Node.js 命令行应用(如清单 7-1 所示)来实现。结果是与该域名相关联的所有 IP 地址的列表。

清单 7-1 。DNS 查找命令行工具

/**
* DNS
*/
var dns = require('dns'),
        args = process.argv.splice(2),
        domain = args[0];

dns.resolve(domain, function (err, addresses) {
  if (err) throw err;

  addresses.forEach(function (address) {
        getDomainsReverse('resolve', address);
  });
});

dns.lookup(domain, function(err, address, family) {
        if (err) console.log(err);
        getDomainsReverse('lookup', address);
});

function getDomainsReverse(type, ipaddress) {
        dns.reverse(ipaddress, function(err, domains) {
                if (err)  {
                    console.log(err);
                } else if (domains.length > 1) {
                        console.log(type + ' domain names for '  + ipaddress + ' ' + domain);
                } else {
                        console.log(type + ' domain name for '   + ipaddress + ' ' + domain);
                }
        });
}

利用这个命令行工具将产生类似于清单 7-2 中的输出,显示查询的结果以及使用了哪种 Node.js DNS 工具来收集结果。

清单 7-2 。使用 Node.js DNS 查找命令行工具

$ node 7-1-1.js g.co
resolve domain name for 173.194.46.37 g.co
resolve domain name for 173.194.46.38 g.co
resolve domain name for 173.194.46.39 g.co
resolve domain name for 173.194.46.40 g.co
resolve domain name for 173.194.46.46 g.co
resolve domain name for 173.194.46.32 g.co
resolve domain name for 173.194.46.33 g.co
resolve domain name for 173.194.46.34 g.co
resolve domain name for 173.194.46.36 g.co
lookup domain name for 74.125.225.78 g.co
resolve domain name for 173.194.46.41 g.co
resolve domain name for 173.194.46.35 g.co

它是如何工作的

Node.js 实现了一个 DNS 版本,它是“C-ares”的包装版本。这是一个为异步 DNS 请求而构建的 C 库。该模块是 Node.js ' dns'模块,是上述解决方案所必需的。

上面的解决方案允许您运行 Node.js 命令,并传递您希望解析或查找的域名。这将从 Node.js 进程的参数中解析出来,传递给查询该域名的两个方法。DNS 对象上的这两个方法是dns.resolve()dns.lookup()。其中每一个都执行类似的任务,因为它们都将从 DNS 服务器获取与传递给该函数的域名相关联的 IP 地址。然而,实现却大不相同。

dns.lookup()函数将接受一个域名和一个回调。有一个可选的第二个参数,可以向其中传递族参数。family 参数将是 4 或 6,表示您希望查询哪个 IP 系列的地址。对dns.lookup()函数的回调将提供错误、地址和族参数,如果它们可用的话。从下面查找函数的源代码中可以看出,一旦配置了初始参数,Node.js 就会调用 C-ares DNS 模块 cares.getaddrinfo的本机包装器,并返回包装后的结果。

清单 7-3 。来自 Node.js dns.js 源的 Dns.lookup 方法

exports.lookup = function(domain, family, callback) {
  // parse arguments
  if (arguments.length === 2) {
    callback = family;
    family = 0;
  } else if (!family) {
    family = 0;
  } else {
    family = +family;
    if (family !== 4 && family !== 6) {
      throw new Error('invalid argument: `family` must be 4 or 6');
    }
  }
  callback = makeAsync(callback);

  if (!domain) {
    callback(null, null, family === 6 ? 6 : 4);
    return {};
  }

  if (process.platform == 'win32' && domain == 'localhost') {
    callback(null, '127.0.0.1', 4);
    return {};
  }

  var matchedFamily = net.isIP(domain);
  if (matchedFamily) {
    callback(null, domain, matchedFamily);
    return {};
  }

  function onanswer(addresses) {
    if (addresses) {
      if (family) {
        callback(null, addresses[0], family);
      } else {
        callback(null, addresses[0], addresses[0].indexOf(':') >= 0 ? 6 : 4);
      }
    } else {
      callback(errnoException(process._errno, 'getaddrinfo'));
    }
  }

  var wrap = cares.getaddrinfo(domain, family);

  if (!wrap) {
    throw errnoException(process._errno, 'getaddrinfo');
  }

  wrap.oncomplete = onanswer;

  callback.immediately = true;
  return wrap;
};

本解决方案中使用的第二个函数是 dns.resolve()函数,用于解析域名。这个函数也接受一个域和一个回调,并带有可选的第二个参数。回调函数提供了一个错误和一个已经解决的地址数组。如果该方法导致错误,它将是表 7-1 中显示的代码之一。

表 7-1 。DNS 错误代码

dns 格式的错误。错误)描述
ADDRGETNETWORKPARAMS找不到 GetNetworkParams 函数
坏家庭不支持的地址族
BADFLAGS指定了非法标志
错误提示指定了非法的提示标志
坏名域名格式不正确
坏查询格式错误的 DNS 查询
巴德雷普格式错误的 DNS 回复
巴德斯特勒格式错误的字符串
取消取消 DNS 查询
经济复兴无法联系 DNS 服务器
毁灭频道被破坏了
文件结束文件结尾
文件读取文件时出错
前任的DNS 服务器声明查询格式不正确
LOADIPHLPAPI加载 iphlpapi.dll 时出错
无数据DNS 服务器返回了一个没有数据的答案
叫什么名字被遗忘
无名给定的主机名不是数字
找不到找不到域名
白色祛皱美眼笔DNS 服务器没有执行请求的操作
未初始化c-ares 未初始化
拒绝DNS 服务器拒绝查询
SERVFAIL(服务失败)DNS 服务器返回一般故障
超时联系 DNS 服务器时超时

dns.lookup()方法不同,该方法可选的第二个参数是一个记录类型,表示您试图解析的 DNS 记录的类型。记录类型有以下几种:“A”、“AAAA”、“MX”、“TXT”、“SRV”、“PTR”、“NS”和“CNAME”。dns.resolve()方法可以将这七种记录类型中的任何一种作为参数;但是,如果没有提供,则默认为“A”类型或 IPv4。

与这种方法相关,但没有显示在解决方案中的是七种记录类型的包装器。这些都是无需传递可选参数就能获得您想要的精确分辨率的简便方法。这些包括dns.resolve4, dns.resolve6, dns.resolveMx, dns.resolveTxt, dns.resolveSrv, dns.resolvePtr, dns.resolveNsdns.resolveCname

清单 7-4 。Dns.resolve 和来自 dns.js 的亲戚

var resolveMap = {};
exports.resolve4 = resolveMap.A = resolver('queryA');
exports.resolve6 = resolveMap.AAAA = resolver('queryAaaa');
exports.resolveCname = resolveMap.CNAME = resolver('queryCname');
exports.resolveMx = resolveMap.MX = resolver('queryMx');
exports.resolveNs = resolveMap.NS = resolver('queryNs');
exports.resolveTxt = resolveMap.TXT = resolver('queryTxt');
exports.resolveSrv = resolveMap.SRV = resolver('querySrv');
exports.resolveNaptr = resolveMap.NAPTR = resolver('queryNaptr');
exports.reverse = resolveMap.PTR = resolver('getHostByAddr');

exports.resolve = function(domain, type_, callback_) {
  var resolver, callback;
  if (typeof type_ == 'string') {
    resolver = resolveMap[type_];
    callback = callback_;
  } else {
    resolver = exports.resolve4;
    callback = type_;
  }

  if (typeof resolver === 'function') {
    return resolver(domain, callback);
  } else {
    throw new Error('Unknown type "' + type_ + '"');
  }
};

对于解决方案的最后一部分,您构建了一个getDomainsReverse函数。这是一个用于dns.reverse函数的包装器,它被设计用来接受一个 IP 地址并找到所有与提供的 IP 地址匹配的域。在您的解决方案中,您将它抽象出来,这样dns.lookup()dns.resolve()方法都可以重用该函数。然后将检索结果记录到控制台。

您可以看到,DNS 模块为您提供了许多机会来收集离您的位置很远的域和服务器的信息。在解析主机问题的解决方案中,您能够利用dns.lookupdns.resolvedns.reverse方法来完成这项任务。

7-2.用缓冲区处理流

问题

您需要使用缓冲区对象来更好地处理流。

解决办法

缓冲区是类似于数组而不仅仅是字符串的二进制数据形式的数据。Buffer 是一个 Node.js 全局对象,用于处理缓冲区。这意味着你可能永远不需要require('buffer'),尽管这是可能的,因为对象的全局性质。为了检查 Buffer 对象的能力,您将创建一个 Node.js 文件,如清单 7-5 所示,它将执行 Buffer 可用的大部分功能。这将使您更好地理解如何处理 Node.js 生态系统中的许多可用缓冲区。

清单 7-5 。Node.js 中的缓冲区

/**
* Buffer
*/

var buffer = new Buffer(16);
console.log('size init', buffer.toString());

buffer = new Buffer([42, 41, 41, 41, 41, 41, 41, 42, 42,4, 41, 41, 0, 0, 7, 77], 'utf-8');
console.log('array init', buffer.toString());

buffer = new Buffer('hello buffer', 'ascii');
console.log(buffer.toString());

buffer = new Buffer('hello buffer', 'ucs2');
console.log(buffer.toString());

buffer = new Buffer('hello buffer', 'base64');
console.log(buffer.toString());

buffer = new Buffer('hello buffer', 'binary');
console.log(buffer.toString());

console.log(JSON.stringify(buffer));
console.log(buffer[1]);
console.log(Buffer.isBuffer('not a buffer'));
console.log(Buffer.isBuffer(buffer));
// allocate size
var buffer = new Buffer(16);
// write to a buffer
console.log(buffer.write('hello again', 'utf-8'));
// append more starting with an offset
console.log(buffer.write(' wut', 11, 'utf8'));
console.log(buffer.toString());
// slice [start, end]
buf = buffer.slice(11, 15);

console.log(buf.toString());
console.log(buffer.length);

console.log(buffer.readUInt8(0));
console.log(buffer.readUInt16LE(0));
console.log(buffer.readUInt16BE(0));
console.log(buffer.readUInt32LE(0));
console.log(buffer.readUInt32BE(0));
console.log(buffer.readInt16LE(0));
console.log(buffer.readInt16BE(0));
console.log(buffer.readInt32LE(0));
console.log(buffer.readInt32BE(0));
console.log(buffer.readFloatLE(0));
console.log(buffer.readFloatBE(0));
console.log(buffer.readDoubleLE(0));
console.log(buffer.readDoubleBE(0));

buffer.fill('4');

console.log(buffer.toString());

var b1 = new Buffer(4);
var b2 = new Buffer(4);
b1.fill('1');
b2.fill('2');

console.log(b1.toString());
console.log(b2.toString());

b2.copy(b1, 2, 2, 4);

console.log(b1.toString());

清单 7-5 中的解决方案强调了当你在 Node.js 中使用缓冲区时,你可以使用的许多功能。接下来你将看到一个使用“net”模块的例子,以及当你在客户机和服务器之间通信时,你如何以缓冲区的形式发送数据。

清单 7-6 。使用缓冲区

var net = require('net');

var PORT = 8181;

var server = net.Server(connectionListener);

function connectionListener(conn) {
    conn.on('readable', function() {
        //buffer
        var buf = conn.read();
        if (Buffer.isBuffer(buf)) {
            console.log('readable buffer: ' , buf);
            conn.write('from server');
        }
    });

    conn.on('end', function() {
    });
}

server.listen(PORT);

//Connect a socket
var socket = net.createConnection(PORT);

socket.on('data', function(data) {
    console.log('data recieved: ',  data.toString());
});

socket.on('connect', function() {
    socket.end('My Precious');
});

for (var i = 0; i < 2000; i++) {
    socket.write('buffer');
}

socket.on('end', function() {
});

socket.on('close', function() {
    server.close();
});

它是如何工作的

缓冲区是在 Node.js 中处理八位字节流的最佳方式。它们表示从 Node.js 应用内部传输的原始数据,并且可以在 Node.js 中的多个位置找到它们。缓冲区非常通用。正如你在清单 7-5 中看到的,他们有很多方法可以为你提供给定工作的最佳解决方案。

当您希望创建缓冲区时,有几种方法可以使用。在这个解决方案中,您首先通过为缓冲区分配大小var buffer = new Buffer(16);来创建一个缓冲区。然后,您可以通过将一个数组直接传递给缓冲区的构造函数var buffer = new Buffer([42, 42]...);来生成一个新的缓冲区。创建新缓冲区的第三种方法是将一个字符串直接传递给构造函数var buffer = new Buffer('hello world');。构造函数也接受一种编码类型,设置为字符串。如果没有传递编码,那么编码将默认为 utf8。

一旦创建了缓冲区,现在就可以操作缓冲区了。缓冲区对象本身有一些直接可用的方法。这些方法包括Buffer.isEncoding(encoding)Buffer.isBuffer(object)Buffer.byteLength(buffer),它们分别评估编码是否按预期设置,查看给定对象是否是缓冲区,并返回缓冲区的字节长度。

除了这些类本身的缓冲方法,还有一些方法,在表 7-2 中列出,你可以在使用缓冲时使用。

表 7-2 。缓冲方法和事件

方法(缓冲。)描述
写入(字符串,[偏移量],[长度],[编码])按照给定的偏移量和编码将字符串写入缓冲区。
toString([编码]、[开始]、[结束])从开始到结束,将缓冲区转换为给定范围内具有给定编码的字符串。
toJSON()返回缓冲区的 JSON 化版本。
长度以字节为单位返回缓冲区的大小。
copy([目标缓冲区]、[目标启动]、[sourceStart]、[sourceEnd])将缓冲区数据从源复制到目标:var b1 = new Buffer('1111'); var b2 = new Buffer('2222'); b2.copy(b1, 2, 2, 4); //b2 == 1121
切片([开始],[结束])在开始和结束参数之间分割缓冲区。产生新的缓冲区。
readUInt8(偏移量,[noassert])从偏移量开始,以无符号 8 位整数形式读取缓冲区。
readUInt16LE(偏移,无修正)从偏移量开始,以无符号 16 位整数 little endian 形式读取缓冲区。
readUInt16BE(偏移量[noassert])从偏移量开始,以无符号 16 位整数 big endian 形式读取缓冲区。
readUInt32LE(偏移,无修正)从偏移量开始,以无符号 32 位整数小端格式读取缓冲区。
readUInt32BE(偏移量,[noassert])读取缓冲区,从偏移量开始,为无符号 32 位整数,大端格式。
readInt8(偏移量,[noassert])从偏移量开始,以 8 位整数形式读取缓冲区。
readInt16LE(偏移,[no adjustment])从偏移量开始,以 16 位小端方式读取缓冲区。
readInt16BE(偏移量,[noassert])以 16 位 big endian 形式从偏移量开始读取缓冲区。
readInt32LE(位移,[no water])从偏移量开始,以 32 位小端方式读取缓冲区。
readInt32BE(偏移量,[noassert])从偏移量开始,以 32 位大端顺序读取缓冲区。
readFloatLE(offset, [noassert])以 float,little endian 形式从偏移量开始读取缓冲区。
readFloatBE(偏移量,[noassert])读取缓冲区,从偏移量开始,作为一个浮点,大端。
read doublel(offset,[noassert])从偏移量开始,以双精度小端方式读取缓冲区。
readDoubleBE(偏移,[no adjustment])从偏移量开始,以双精度大端方式读取缓冲区。
writeUInt8(值,偏移量,[noassert]从偏移量开始,将一个无符号 8 位整数写入缓冲区。
writeUInt16LE(值,偏移量,[noassert])从偏移量 little endian 开始,将一个无符号 16 位整数写入缓冲区。
writeUInt16BE(值,偏移量,[noassert])从偏移量 big endian 开始,将一个无符号 16 位整数写入缓冲区。
writeUInt32LE(值,偏移量,[noassert])从偏移量 little endian 开始,将一个无符号 32 位整数写入缓冲区。
writeUInt32BE(值,偏移量,[noassert])从偏移量 big endian 开始,将一个无符号 32 位整数写入缓冲区。
writeInt8(值,偏移量,[noassert])从偏移量开始,将一个 8 位整数写入缓冲区。
writeInt16LE(值,偏移,[no improved])从偏移量 little endian 开始,将一个 16 位整数写入缓冲区。
writeInt16BE(值,偏移量,[noassert])从偏移量 big endian 开始,将一个 16 位整数写入缓冲区。
writeInt32LE(值、偏移、无改善)从偏移量 little endian 开始,将一个 32 位整数写入缓冲区。
writeInt32BE(值,偏移量,[noassert])从偏移量 big endian 开始,将一个 32 位整数写入缓冲区。
writeFloatLE(值,偏移量,[noassert])从偏移量 little endian 开始,将一个浮点值写入缓冲区。
siwriteFloatBE(值,偏移量,[no asset])从偏移量 big endian 开始,将一个浮点值写入缓冲区。
writeDoubleLE(value,offset,[no asset])从偏移量 little endian 开始,将一个 double 值写入缓冲区。
writeDoubleBE(值,偏移量,[no asset])从偏移量 big endian 开始,将一个 double 值写入缓冲区。
fill(值,[偏移量],[结束])用从偏移量到结束范围指定的值填充缓冲区。

缓冲区中有许多方法可用于非常特殊的目的。例如,如果您需要以 little endian 格式读写无符号 32 位整数,缓冲区可以做到这一点。虽然这些方法非常灵活,但在大多数情况下,你会使用如清单 7-4 所示的缓冲区。这是一个“网络”服务器和客户端互相发送数据的例子。数据变成一个可读的流,这是一个缓冲区。您将能够对任何缓冲区执行一节中描述的方法,从而允许您操作应用中使用的数据和流。

7-3.使用 Node.js 进行聚类

问题

您希望构建一个进程集群来更有效地运行您的应用。

解决办法

Node.js 为集群提供了一个解决方案。在撰写本文时,该特性仍处于试验阶段,但它能够将单线程 Node.js 应用转变为在您的机器上利用多个内核的应用。通过这种方式,您可以将 Node.js 任务委托给不同的线程,从而实现更大的可伸缩性。在这个解决方案中,您将生成一个使用集群模块的 Node.js 应用。第一个例子是一个独立的解决方案,它将分割一个简单的 HTTP 服务器,并将各种集群方法的结果记录到您的控制台。

清单 7-7 。使聚集

/**
* Clustering
*/

var cluster = require('cluster'),
    http = require('http'),
    cpuCount = require('os').cpus().length;

if (cluster.isMaster) {
    for (var i = 0; i < cpuCount; i++) {
        cluster.fork();
    }
cluster.on('fork', function(worker) {
    console.log(worker + ' worker is forked');
});
cluster.on('listening', function(worker, address) {
    console.log(worker + ' is listening on ' + address);
});
cluster.on('online', function(worker) {
    console.log(worker + ' is online');
});
cluster.on('disconnect', function(worker) {
    console.log(worker + ' disconnected');
});
cluster.on('exit', function(worker, code, signal) {
    console.log('worker ' + worker.process.pid + ' died');
  });
} else {
  // Workers can share any TCP connection
  // In this case it is an HTTP server
  http.createServer(function(req, res) {
    res.writeHead(200);
    res.end("hello world\n");
  }).listen(8000);
}

现在,您将配置集群来执行第二个 Node.js 文件,对机器上的每个内核执行一次。

清单 7-8 。集群化 Node.js 流程

/**
* Clustering
*/

var cluster = require('cluster'),
                cpuCount = require('os').cpus().length;

cluster.setupMaster({
        exec: '7-3-3.js'
});
if (cluster.isMaster) {
        for (var i = 0; i < cpuCount; i++) {
                cluster.fork();
        }
        cluster.on('fork', function(worker) {
                console.log(worker + ' worker is forked');
        });
        cluster.on('listening', function(worker, address) {
                console.log(worker + ' is listening on ' + address);
        });
        cluster.on('online', function(worker) {
                console.log(worker + ' is online');
        });
        cluster.on('disconnect', function(worker) {
                console.log(worker + ' disconnected');
        });
        cluster.on('exit', function(worker, code, signal) {
    console.log('worker ' + worker.process.pid + ' died');
  });
}

清单 7-9 ,工作进程,如下所示。

清单 7-9 。工作进程

var http = require('http');

http.createServer(function(req, res) {
        console.log(req.url);
  res.writeHead(200);
  res.end("hello world\n");
}).listen(8000);

它是如何工作的

Node.js 中的集群本质上是利用一个 Node.js 模块和利用child_process.fork()函数分割工作进程的解决方案,同时维护主进程和工作进程之间的引用和通信。工作进程可以是 TCP 或 HTTP 服务器,请求由主进程处理。然后,这个主进程利用循环负载平衡在服务器中分配负载。它通过监听连接,然后调用 distribute 方法并将处理交给工作进程来完成。

清单 7-10 。主监听,然后分配负载

this.server.once('listening', function() {
    self.handle = self.server._handle;
    self.handle.onconnection = self.distribute.bind(self);
    self.server._handle = null;
    self.server = null;
  });
RoundRobinHandle.prototype.distribute = function(handle) {
  this.handles.push(handle);
  var worker = this.free.shift();
  if (worker) this.handoff(worker);
};

RoundRobinHandle.prototype.handoff = function(worker) {
  if (worker.id in this.all === false) {
    return;                     // Worker is closing (or has closed) the server.
  }
  var handle = this.handles.shift();
  if (typeof handle === 'undefined') {
    this.free.push(worker);     // Add to ready queue again.
    return;
  }
  var message = { act: 'newconn', key: this.key };
  var self = this;
  sendHelper(worker.process, message, handle, function(reply) {
    if (reply.accepted)
      handle.close();
    else
      self.distribute(handle);  // Worker is shutting down. Send to another.
    self.handoff(worker);
  });
};

在您的解决方案中,您主要使用主流程。在这两种情况下,您都有一个简单的 HTTP 服务器,当向服务器地址发出请求时,它会以“hello world”作为响应。导入集群模块后,使用cluster.isMaster 检查该进程是否是集群的主进程。现在,您可以通过使用“os”模块检查您的计算机拥有的内核数量来查看您应该在您的计算机上创建多少个集群。对于每个 CPU,您派生一个名为cluster.fork()的新工作进程。因为底层框架仍然生成一个新的子进程,它仍然是 v8 的一个新实例,所以您可以假设每个 worker 的启动时间在大多数情况下将大于 0 并且小于 100 ms。它还会在启动时为每个进程生成大约 10 MB 的内存消耗。

您现在知道这是主流程,因此您能够绑定到将被传递到主流程的事件。感兴趣的事件是“fork”、“listening”、“exit”、“online”和“setup”。“fork”事件在工作进程被成功分叉时发出,并为被分叉的进程提供工作对象。一旦创建并运行分叉流程,就会发生“在线”事件。一旦工作者开始监听,就发送“监听”事件。当工作进程终止时,将发出“exit”事件。如果发生这种情况,您可能需要调用。fork()方法来替换关闭的工作线程。

集群是一个强大的模块,它允许您在机器上的多个进程之间分配服务器的负载。随着 Node.js 应用的增长,该功能在创建可伸缩应用时会变得很重要。

7-4.使用查询字符串

问题

您用 Node.js 构建了一个 web 服务器,您希望巧妙地处理通过 HTTP 请求传递给应用的查询字符串的各种差异。

解决办法

Node.js 有一个查询字符串模块,允许您为 Node.js 应用正确解析和编码查询字符串参数。

清单 7-11 。使用查询字符串模块

/**
* querystrings
*/

var qs = require('querystring');

var incomingQS = [ 'foo=bar&foo=baz',
                  'trademark=%E2%84%A2',
                  '%7BLOTR%7D=frodo%20baggins'];

incomingQS.forEach(function(q) {
        console.log(qs.parse(q));
});

var outgoingQS = { good: 'night', v: '0.10.12', trademark: '™'};
console.log(qs.stringify(outgoingQS));

var newQS = qs.stringify(outgoingQS, '|', '∼');
console.log(newQS);

console.log(qs.parse(newQS));

您可以将任意查询字符串作为输入,然后将其解析为一个对象。您还能够获取任意对象,并将其解析为 URL 安全查询字符串。

它是如何工作的

虽然查询字符串模块不是一个巨大的模块,但它只导出四个方法。它为处理查询字符串提供了一个非常有用的解决方案。在这个解决方案中,您首先需要'querystring'模块。这个模块提供了几种方法来帮助处理应用中的查询字符串。

首先,querystring.parse函数接受一个字符串,并可选地覆盖分隔符,默认为&和等于。还有一个 options 对象,允许您覆盖要处理的 1000 个最大键(maxKeys)的缺省值。对于查询字符串中的每个键,‘querystring’模块将尝试通过首先使用 JavaScript 自带的decodeURIComponent()函数来解析值。如果这产生了一个错误,那么这个模块将会转移到它自己的被称为querystring.unescape的实现上。

其次,querystring.stringify函数将接受您希望编码到querystring中的对象。或者,该方法还将允许您覆盖默认的&号和等号分隔符。querystring.stringify方法将解析对象,将其转换成字符串,然后调用模块的QueryString.escape方法。这个方法只是 JavaScript 的encodeURIComponent()的一个包装器。

7-5.用“进程”处理事件

问题

您希望能够在 Node.js 应用中全局处理事件。

解决办法

Node.js 流程模块是一个全局对象,可以在 Node.js 应用中的任何地方访问。在这个解决方案中,您可以想象一种情况,其中您有一个模块和一个辅助模块。主模块绑定到初始化事件,并开始调用辅助模块的某些方法。主模块看起来像清单 7-12 中所示的模块,辅助模块看起来像清单 7-13 中所示的模块。

清单 7-12 。主模块,处理流程

/**
* using the process
*/

function log(msg) {
        if (typeof msg === 'object') {
                msg = JSON.stringify(msg);
        }
        process.stdout.write(msg + '\n');
}
//add listeners
process.on('power::init', function() {
        log('power initialized');
});

process.on('power::begin', function() {
        log('power calc beginning');
});

process.on('exit', function() {
        log(process.uptime());
        log('process exiting...');
});

process.on('uncaughtException', function(err) {
        log('error in process ' +  err.message + '\n');
});
log(process.cwd());
process.chdir('..');
log(process.cwd());
log(process.execPath);
log(process.env.HOME);
log(process.version);
log(process.versions);
log(process.config);
log(process.pid);
log(process.platform);
log(process.memoryUsage());
log(process.arch);

var pow = new require('./power');

var out = pow.power(42, 42);
log(out);

// throws
setTimeout(pow.error, 1e3);

清单 7-13 。辅助模块

/**
* power module
*/
process.emit('power::init');

exports.power = function(base, exponent) {
  var result = 1;
  process.emit('power::begin');
  for (var count = 0; count < exponent; count++)
    result *= base;
  return result;
};

它是如何工作的

Node.js 流程对象可以为您的应用获取有价值的信息和实用程序。流程对象是全局的,是一个EventEmitter。这就是为什么在上面的例子中,您能够从 power.js 文件中发出'power::init'。你也通过调用process.on('power::init', callback)来绑定到这个。然后绑定到其他事件。首先,绑定到另一个自定义事件,该事件是在开始执行 power.js 模块的 power 函数时发出的。

另外两个事件是 Node.js 流程的内置事件。首先,绑定到'exit'事件。这将在流程准备退出时触发,给你最后一次机会记录错误或通知用户流程即将结束。您侦听的另一个内置事件是'uncaughtException'事件。该事件由任何异常触发,否则这些异常会出现在控制台上并使您的应用崩溃。在该解决方案中,您可以通过尝试调用 power.js 模块上不存在的方法来触发该事件。

流程模块不仅仅处理事件。事实上,它可以提供大量与您当前 Node.js 流程相关的信息,其中许多信息您可以在创建解决方案时加以利用。表 7-3 详述了流程对象上的这些其他方法和属性。

表 7-3 。过程对象方法和属性

方法描述
中止()此方法将中止该过程。
拱门您系统的架构。
阿尔戈夫这是实例化 Node.js 流程的参数。在解析传递给应用的参数时,您已经看到了这一点。
总监(主任)更改您的进程当前工作的目录。
配置列出了 Node.js 应用的配置。
cwd()打印进程的当前工作目录。
包封/包围(动词 envelop 的简写)将系统中的环境变量作为对象列出。
execPath这是系统上 Node.js 可执行文件的路径。
退出([代码])用指定的代码发出退出事件。
格吉德()获取进程的组 ID。在 Windows 上不可用。
getgroups()获取进程补充组的组 ID 数组。在 Windows 上不可用。
它是()获取进程的用户 ID。在 Windows 上不可用。
hrtime([hrtime])过去任意时期以来的高分辨率时间数组(秒、纳秒)。这可以与先前的 hrtime 读数一起使用,以获得差值。
初始组读取/etc/groups。在 Windows 上不可用。
kill(进程 id,[信号])向进程 ID 发送信号。
maxTickDepth您可以使用它来设置在允许事件循环处理之前要运行的节拍数。这阻止了使用。锁定 I/O 后的下一个时钟周期
memoryUsage()进程中使用的内存字节数。
nextTick(回调)下一次在事件循环中,回调将被执行。可以这样做:

function definitelyAsync(arg, cb) { if (arg) { process.nextTick(cb); return; } fs.stat('file', cb); } | | pid | 进程标识符。 | | 平台 | 列出运行进程的平台。 | | setgid() | 设置进程的组 ID。在 Windows 上不可用。 | | 集合组 | 为进程设置组数组。在 Windows 上不可用。 | | setuid() | 设置进程的用户 ID。在 Windows 上不可用。 | | 标准错误 | 标准误差;这是一个可写流。 | | 标准输入设备 | 表示标准输入的可读流。

function log(msg) { if (typeof msg === 'object') { msg = JSON.stringify(msg); } process.stdout.write(msg + '\n'); } | | 标准输出 | 这是您的流程的标准输出,并且是一个可写流。您可以通过创建您的 log()函数来重新创建控制台日志记录:

function log(msg) { if (typeof msg === 'object') { msg = JSON.stringify(msg); } process.stdout.write(msg + '\n'); } | | 标题 | 流程的标题。 | | umask([mask]) | 进程的文件模式创建掩码的 Setter 和 getter。 | | 正常运行时间() | Node 已经运行的秒数(不是毫秒)。 | | 版本 | 打印进程正在使用的 Node.js 版本。 | | 版本 | 列出包含 Node.js 版本及其依赖项的对象。 |

7-6.使用计时器

问题

您希望能够在 Node.js 应用中利用计时器来控制流。

解决办法

控制任何应用中特定进程的时间是非常关键的,包括那些用 Node.js 构建的应用。如果您在 web 应用中使用过计时器,那么在 Node.js 中使用计时器应该很熟悉,因为有些方法在浏览器中也可以使用。

在这个解决方案中,您将创建一个应用,它将利用计时器来轮询一个虚构的远程资源。这个解决方案将代表一个场景,其中您需要从远程队列获取数据。有几种解决方案可以按时间间隔进行轮询,或者简单地利用计时器在事件循环中有效地调用方法。

清单 7-14 。使用计时器

/**
* Using Timers
*/

var count = 0;
var getMockData = function(callback) {
        var obj = {
                  status: 'lookin good',
                           data: [
                             "item0",
                             "item1"
                           ],
                            numberOfCalls: count++
                  };
        return callback(null, obj);
};

var onDataSuccess = function(err, data) {
        if (err) console.log(err);
        if (data.numberOfCalls > 15) clearInterval(intrvl);
        console.log(data);
};

// getMockData(onDataSuccess);
setImmediate(getMockData, onDataSuccess);

var tmr = setTimeout(getMockData, 2e3, onDataSuccess);
tmr.unref();
var intrvl = setInterval(getMockData, 50, onDataSuccess);

它是如何工作的

Node.js 中有几个可以使用的计时器。首先,有一组可以在 web 浏览器中找到的计时器。这些是setTimeoutsetInterval及其相应的clearTimeoutclearInterval功能。

setTimeout是一种在给定时间延迟后安排一次性事件的方式。一个setTimeout调用的结构至少有两个参数。第一个参数是您希望在计时器触发时执行的回调。第二个是等待回调执行的毫秒数。或者,您可以向函数添加额外的参数,这些参数将在计时器执行时应用于回调。通过调用clearTimeout并传递一个对初始超时定时器的引用,可以取消setTimeout

image 注意由于 JavaScript Node.js 事件循环,您无法直接依赖回调执行的时间。Node.js 将尝试在接近规定的时间执行回调,但它可能不会在精确的时间间隔执行。

setIntervalsetTimeout的亲戚。它通过提供一种将功能的执行延迟一段设定时间的机制,以类似的方式发挥作用。然而,使用setInterval,该函数将在相同的时间间隔内重复执行,直到clearInterval被调用。在上面的解决方案中,这是在长时间运行的流程中用于轮询的情况。理论上,您可以在 30 秒、3 分钟或每小时的长时间轮询中运行一个时间间隔,并让该过程继续运行。然而在解决方案中,您以很短的时间间隔(< 1 秒)运行该间隔,并在它执行了 15 次后清除它。

setIntervalsetTimeout方法都附加了两个额外的方法。这些是unref()ref()。如果计时器是 event.loop 上剩下的唯一计时器,那么unref()方法允许 Node.js 进程终止。而ref()方法则相反,它会暂停进程,直到计时器执行完毕。要看到这一点,您可以注意到,在解决方案中的两秒延迟setTimeout方法之后,您立即调用该计时器的unref()方法。这意味着这个定时器永远不会执行。因为从setInterval开始的时间间隔在两秒钟过去之前就已经结束并被清除,所以事件循环中不再有其他计时器,该过程优雅地退出。

setImmediate是 Node.js 的另一个计时机制。这对于调用近即时方法很有用,类似于process.nextTick()函数的操作方式。setImmediate会将函数排在当前事件循环中任何 I/O 绑定回调函数的后面进行处理。这与nextTick的操作略有不同,因为它会将其执行推到事件循环的前面。这意味着setImmediate是一种更好的方式来执行一个不会锁定 I/O 进程的方法。如果您正在运行一个需要一定程度的 CPU 使用率的递归函数,这将特别有用,因为这不会阻止这些操作在回调之间发生。

7-7.使用 V8 调试器

问题

您需要单步调试 Node.js 应用。

解决办法

Node.js 运行在 Google 的 V8 上,V8 有内置的调试机制。因此,Node.js 允许您利用该工具调试源代码。您将创建一个解决方案,帮助您了解调试器是如何工作的,以及它能为您的代码带来哪些启示。清单 7-15 显示了一个简单的 HTTP 服务器,它需要第二个模块(如清单 7-16 所示)。

清单 7-15 。HTTP 服务器

/**
* Debugging
*/

var http = require('http'),
    mod = require('./7-7-2');

server = http.createServer(function(req, res) {

        if (req.url === '/') {
                debugger;
                mod.doSomething(function(err, data) {
                        if (err) res.end('an error occured');

                        res.end(JSON.stringify(data));
                });
        } else {
                res.end('404');
        }
});

server.listen(8080);

清单 7-16 。所需模块

/**
* Debugging
*/

exports.doSomething = function(callback) {
        debugger;
        callback(null, { status: 'okay', data: ['a', 'b', 'c']});
};

有些东西可能与典型的 Node.js 应用略有不同;特别是你可以看到一些“调试器”陈述。这些指令告诉 V8 调试机制暂停程序的执行。这个过程从用“debug”标志启动 Node.js 应用开始。

它是如何工作的

Google 设计了支持 Node.js 的 V8 JavaScript 引擎,允许调试引擎中执行的 JavaScript。Node.js 以两种方式支持 V8 调试。一种方法是以创建调试器的方式实现 V8 调试器协议,监听 TCP 端口。如果您正在创建或使用协调使用该协议的第三方调试工具,这将非常有用。为此,使用命令$ node --debug 7-7-1.js启动 Node.js 应用 7-7-1.js。这将启动调试器,并在localhost:5858上监听调试器的挂钩。这允许创建与调试器通信的调试客户端。幸运的是,Node.js 自带了自己的 V8 调试器客户端。您可以通过在控制台中键入$ node debug 来访问在调试模式下使用- -debug 标志启动的应用。

通过使用“debug”参数启动 Node.js 应用,可以访问 Node.js 内置调试器。

清单 7-17 。启动 Node.js 调试 CLI

$ node debug 7-7-1.js

这将启动您的应用,但附加了调试器。控制台中的输出将显示调试器已经开始监听,并且它将显示 JavaScript 代码的第一行,默认情况下调试器将中断该行。

清单 7-18 。调试器的初始状态

< debugger listening on port 5858
connecting... ok
break in 7-7-1.js:5
  3 */
  4
  5 var http = require('http'),
  6     mod = require('./7-7-2');
  7
debug>

您现在有一个“debug>”提示。这是调试器的命令行界面。您可以按照下面的步骤完成调试的基础。首先,您可以向应用中的对象或属性添加观察器。要做到这一点,你可以输入“watch ”,然后输入任何你想观看的表情。

debug> watch('expression')

因此,在您的解决方案中,您可以通过使用 watch 命令并传递“req.url”作为表达式来监视请求 URL。

debug> watch('req.url')

您还可以列出调试器会话中当前活动的所有观察器。结果会将活动的观察器及其值打印到控制台。当前值被赋予 JavaScript 代码暂停的直接上下文。

debug> watchers
  0: req.url = "<error>"

回想一下,在您的应用代码中,您创建了两个名为'debugger;'的地方。它将在应用中的这些点暂停执行。但是,有时您可能不希望添加调试器语句,而只想在代码中设置断点。为此,调试器有几个可用的断点方法。要在当前行设置断点,只需在调试控制台中键入setBreakpoint()。或者,您可以使用简写的sb()来设置断点。setBreakpoint方法也接受一个行号,因此您可以预先确定一行来中断。您可以在代码中通过在server.listen(8080)方法上设置一个断点来做到这一点。

debug> sb(21)
  1 /**
  2 * Debugging
  3 */
  4
  5 var http = require('http'),
  6     mod = require('./7-7-2');
  7
  8 server = http.createServer(function(req, res) {
  9     if (req.url === '/') {
 10             debugger;

您还可以中断将加载到您的应用中的另一个文件。为此,将文件名和行号传递给setBreakpoint方法。

debug> sb('7-7-2.js', 5)
Warning: script '7-7-2.js' was not loaded yet.
  1 /**
  2 * Debugging
  3 */
  4
  5 var http = require('http'),
  6     mod = require('./7-7-2');
  7
  8 server = http.createServer(function(req, res) {
  9     if (req.url === '/') {
 10             debugger;

在这里您可以看到,您已经在文件 7-7-2.js 中的第一行代码上设置了断点。一旦您继续执行程序,一旦命中该行代码,断点将再次暂停程序的执行。

此时,您已经准备好使用调试器在应用中导航了。与大多数调试器一样,调试器公开允许您逐句通过并继续执行代码的方法。最细化的方法是命令中的步骤。这是通过键入'step''s'来调用的,简称为。从调试实例执行的开始,如果您单步执行,它会将您移动到下一个执行区域。在这个实例中,它已经移动到 module.js 文件中,并开始在源代码中添加您需要的模块。

debug> s
break in module.js:380
Watchers:
  0: req.url = "<error>"

 378
 379   function require(path) {
 380     return self.require(path);
 381   }
 382

从这里你会想继续。继续执行将一直运行,直到遇到下一个断点。如果没有其他断点,应用将正常运行,直到您用pause命令手动暂停它。可以通过“cont'”或“c'”来触发延续。在您的示例中,这将引导您完成模块导入代码,并到达断点,断点是您在“7-7-2.js”文件的第 5 行设置的。

debug> c
break in 7-7-2.js:5
Watchers:
  0: req.url = "<error>"

  3 */
  4
  5 exports.doSomething = function(callback) {
  6     debugger;
  7     callback(null, { status: 'okay', data: ['a', 'b', 'c']});
debug> c
break in 7-7-1.js:21
Watchers:
  0: req.url = "<error>"

 19 });
 20
*21 server.listen(8080);
 22
 23

再继续一次,会碰到你在‘7-7-1 . js’第 21 行设置的断点;这是您设置的最后一个断点。但是,一旦与 HTTP 服务器建立了连接,就会遇到一些调试器语句。继续完成后,您可以向您的 web 服务器发出请求,'``http://localhost:8080/’。因为有了debugger;语句,这将在连接监听器回调的精确位置暂停执行。

debug> c
break in 7-7-1.js:10
Watchers:
  0: req.url = "/"

  8 server = http.createServer(function(req, res) {
  9     if (req.url === '/') {
 10             debugger;
 11             mod.doSomething(function(err, data) {
 12                     if (err) res.end('an error occured');

从这里,你可以进入下一次执行。这是使用调试器中的“next”或“n”命令完成的。执行两次“??”,你就会在调试器中结束;' 7-7-2.js '模块中的语句。

debug> n
break in 7-7-1.js:11
Watchers:
  0: req.url = "/"

  9     if (req.url === '/') {
 10             debugger;
 11             mod. doSomething (function(err, data) {
 12                     if (err) res.end('an error occured');
 13
debug> n
break in 7-7-2.js:6
Watchers:
  0: req.url = "<error>"

  4
  5 exports.doSomething = function(callback) {
  6     debugger;
  7     callback(null, { status: 'okay', data: ['a', 'b', 'c']});
  8 };

现在,您可以使用'out'或(' ')'命令跳出此方法。

debug> o
break in 7-7-1.js:19
Watchers:
  0: req.url = "/"

 17             res.end('404');
 18     }
 19});
 20
 21 server.listen(8080);

除了单步、下一步、继续和退出,还有'pause'命令。这将暂停当时正在运行的任何代码的执行。

当您单步执行代码时,有时需要获得更多关于应用中发生的事情的信息。调试器对此也有实用工具。首先,当你在一个断点处暂停时,如果你想看到周围更多的代码,你可以通过使用'list(n)'命令来实现。这将显示当前暂停位置前后由“n”行包围的代码,这对于收集调试器中当前正在发生的事情的更多上下文非常有用。另一个有用的特性是'backtrace'(' ?? ')命令。这将显示程序中当前点的执行路径的轨迹。

清单 7-19 。7-7-2.js 模块的 doSomething 方法中的回溯示例

debug> bt
#0 exports.doSomething 7-7-2.js:6:2
#1 7-7-1.js:11:7

您也可以使用'scripts'命令查看加载的文件。重要的是,如果您需要更深入地研究代码,您可以通过使用'repl'命令来使用调试器的读取-评估-打印循环(REPL)模块。

使用内置命令行界面调试 Node.js 应用被视为高优先级,以便使用 V8 调试器调试您的应用。当您跟踪代码中的异常和错误时,您会发现这些工具非常有用。

7-8.解析 URL

问题

您希望能够解析 Node.js HTTP 服务器应用中的 URL。

解决办法

Node.js 附带了一个 URL 模块,可以用来解析 URL 并收集其中包含的信息。看看这是如何工作的一个解决方案(见清单 7-20 )将告诉你如何解析一个任意的 URL。

清单 7-20 。解析任意 URL

/**
* parse url
*/

var url = require('url');

var theurl = 'http://who:ami@hostname:1234/a/b/c/d/?d=e#f=g';

var urlParsed = url.parse(theurl, true, true);
console.log('protocol', urlParsed.protocol);
console.log('slashes', urlParsed.slashes);
console.log('auth', urlParsed.auth);
console.log('host', urlParsed.host);
console.log('port', urlParsed.port);
console.log('hostname', urlParsed.hostname);
console.log('hash', urlParsed.hash);
console.log('search', urlParsed.search);
console.log('query', urlParsed.query);
console.log('pathname', urlParsed.pathname);
console.log('path', urlParsed.path);
console.log('href', urlParsed.href);

console.log(url.resolve('/a/b/c/', 'd'));

7-20 的结果

$ node 7-8-2.js
protocol http:
slashes true
auth who:ami
host hostname:1234
port 1234
hostname hostname
hash #f=g
search ?d=e
query { d: 'e' }
pathname /a/b/c/d/
path /a/b/c/d/?d=e
href http://who:ami@hostname:1234/a/b/c/d/?d=e#f=g
/a/b/c/d

在实践中使用它,你可以想象一个 HTTP 服务器,不像清单 7-21 ,它需要 URL 被解析,这样你就可以协调正确的文件来服务于HTTP.response()中的客户端。

清单 7-21 。使用 URL 模块

/**
* Parsing URLS
*/

var http = require('http'),
        fs = require('fs'),
        url = require('url');

var server = http.createServer(function(req, res) {
        var urlParsed = url.parse(req.url,true, true);

        fs.readFile(urlParsed.path.split('/')[1], function(err, data) {
        if (err) {
            res.statusCode = 404;
            res.end(http.STATUS_CODES[404]);
        }

        var ursplit = urlParsed.path.split('.');
        var ext = ursplit[ursplit.length - 1];
        switch(ext) {
            case 'htm':
            case 'html':
                res.writeHead(200, {'Content-Type': 'text/html'});
                res.end(data);
                break;
            case 'js':
                res.writeHead(200, {'Content-Type': 'text/javascript'});
                res.end(data);
                break;
            case 'css':
                res.writeHead(200, {'Content-Type': 'text/css'});
                res.end(data);
                break;
            case 'json':
                res.writeHead(200, {'Content-Type': 'application/json'});
                res.end(data);
                break;
            default:
                res.writeHead(200, {'Content-Type': 'text/plain'});
                res.end(data);
        }
        });
}).listen(8080);

它是如何工作的

使用 URL 模块为您提供了三种处理 URL 的方法。首先你使用了url.parse()方法。该方法接受一个 URL 字符串,并返回一个经过解析的 URL 对象。解析后的 URL 对象可以采用表 7-4 中所示的属性。

表 7-4 。解析的 URL 对象属性

财产描述
。作家(author 的简写)URL 的授权部分。用户名:密码
。混杂URL 中存在的任何片段。
。宿主URL 的完整主机名和端口。
。主机名URL 中主机的全名。
。超链接完整的网址。
。小路路径名和搜索相结合。
。路径名URL 的主机名和端口部分后面的完整路径名。
。港口URL 中指定的端口。
。草案请求的协议。
。询问不带“?”的查询字符串。可以被解析为一个对象。
。搜索URL 的查询字符串部分。

HTTP 服务器示例中使用了解析的对象来解析路径,以便服务器可以读入文件类型并为内容提供适当的 mime 类型。URL 路由也有很好的用途,可以从解析 URL 中受益。

如果您正在处理一个解析的 URL,并且您想要将该对象转换回一个正确的 URL,无论您是将 URL 返回给客户端还是出于其他目的,您都可以通过调用该对象上的url.format()函数从一个解析的 URL 对象创建一个 URL。这将重新格式化对象,不包括返回到 URL 的href

第三种可以使用的方法是url.resolve(from, to)函数。该函数将尝试解析路径,就像 web 浏览器一样。

可以看到,如果要在 Node.js 应用中处理 URL,应该利用 URL 模块内置的特性。它提供了解析和格式化应用中需要的任何 URL 所需的工具。

7-9.使用控制台

问题

您希望利用控制台来记录 Node.js 应用中的细节、指标和断言。

解决办法

您可能熟悉一些控制台函数,因为大多数人在构建 Node.js 应用时至少会使用console.log()函数,并且您已经在本书其他部分的许多示例中看到了它的使用。为了了解如何使用控制台,清单 7-22 展示了 Node.js 开发人员可以使用的所有不同方法。

清单 7-22 。使用控制台

/**
* Console
*/

console.log('console usage in Node.js');

console.info('console.info writes the', 'same as console.log');

console.error('same as console.log but writes to stderr');

console.warn('same as console.err');

console.time('timer');

setTimeout(console.timeEnd, 2e3, 'timer');

console.dir({ name: 'console.dir', logs: ['the', 'string representation', 'of objects']});

var yo = 'yo';
console.trace(yo);

try {
        console.assert(1 === '1', 'one does not equal one');
} catch(ex) {
        console.error('an error occured: ', ex.message);
}

控制台结果

7|⇒ node 7-9-1.js
console usage in Node.js
console.info writes the same as console.log
same as console.log but writes to stderr
same as console.err
{ name: 'console.dir',
  logs: [ 'the', 'string representation', 'of objects' ] }
Trace: yo
    at Object.<anonymous> (/Users/gack/Dropbox/book/code/7/7-9-1.js:20:9)
    at Module._compile (module.js:456:26)
    at Object.Module._extensions..js (module.js:474:10)
    at Module.load (module.js:356:32)
    at Function.Module._load (module.js:312:12)
    at Function.Module.runMain (module.js:497:10)
    at startup (node.js:119:16)
    at node.js:901:3
an error occured:  one does not equal one
timer: 2001ms

它是如何工作的

Node.js 中的 console 对象是写入stdoutstderr 的方法。最常见的控制台功能有console.logconsole.errconsole.warnconsole.dirconsole.info。这些函数直接处理日志信息以向用户提供信息,它们直接处理 Node.js 进程的stdoutstderr

控制台对象源

Console.prototype.log = function() {
  this._stdout.write(util.format.apply(this, arguments) + '\n');
};
Console.prototype.info = Console.prototype.log;
Console.prototype.warn = function() {
  this._stderr.write(util.format.apply(this, arguments) + '\n');
};
Console.prototype.error = Console.prototype.warn;
Console.prototype.dir = function(object) {
  this._stdout.write(util.inspect(object, { customInspect: false }) + '\n');
};

在 Node.js 中实现这些方法并不重要。但是,还有一些其他的控制台方法可能会有用。

其中之一就是一对console.timeconsole.timeEnd。每个函数都有一个标签,告诉 Node.js 跟踪从调用console.time('label')console.timeEnd('label')之间的时间。曾经的console。它将记录事件之间经过的毫秒数。

Console.prototype.time = function(label) {
  this._times[label] = Date.now();
};

Console.prototype.timeEnd = function(label) {
  var time = this._times[label];
  if (!time) {
    throw new Error('No such label: ' + label);
  }
  var duration = Date.now() - time;
  this.log('%s: %dms', label, duration);
};

Console.trace() 是一个打印当前堆栈跟踪的函数,应用于作为标签传递的参数。这是通过基于当前堆栈创建一个新的错误对象并设置细节来实现的。

Console.prototype.trace = function() {
  // TODO probably can to do this better with V8's debug object once that is
  // exposed.
  var err = new Error;
  err.name = 'Trace';
  err.message = util.format.apply(this, arguments);
  Error.captureStackTrace(err, arguments.callee);
  this.error(err.stack);
};

Console.assert ,是assert.ok()的包装器,如果断言失败,它将抛出一个错误。在这个解决方案中,您创建了一个您知道会失败的断言,并且在捕获到异常时记录了错误消息。