Node.js 是专门为可伸缩的服务器端和网络应用而创建的。它具有久经考验的功能,可以有效地处理网络连接。这为社区构建成熟的应用服务器提供了基础。
在这一章中,我们将看看 Node.js 为创建 web 应用提供的核心功能。然后我们将回顾 connect 中间件框架,它允许您创建可重用的 web 服务器组件。最后,我们将看看用 HTTPS 保护你的 Web 服务器。
Node.js HTTP 的基础知识
以下是在 Node.js 中创建 web 应用的主要核心网络模块:
- net /
require('net'):为创建 TCP 服务器和客户端提供基础 - dgram /
require('dgram'):提供创建 UDP /数据报套接字的功能 - http /
require('http'):为 http 栈提供高性能的基础 - https /
require('https'):为创建 TLS / SSL 客户端和服务器提供 API
我们将从使用http模块创建我们的简单服务器来服务静态文件开始。从头开始创建我们的 web 服务器将使我们对社区 NPM 模块提供的功能有更深的理解,我们将在后面探讨这些功能。
注意我们将使用curl来测试我们的 web 应用。默认情况下,它在 Mac OS X / Linux 上可用。您可以将curl for windows 作为 Cygwin ( www.cygwin.com/)的一部分。
http模块有一个可爱的小函数createServer,它接受一个回调并返回一个 HTTP 服务器。对于每个客户端请求,回调通过两个参数传递——传入的请求流和传出的服务器响应流。要启动返回的 HTTP 服务器,只需调用它的listen函数,传递您想要监听的端口号。
清单 6-1 提供了一个简单的服务器)监听端口 3000,并简单地对每个 HTTP 请求返回“hello client!”。
清单 6-1 。1create/1raw.js
var http = require('http');
var server = http.createServer(function (request, response) {
console.log('request starting...');
// respond
response.write('hello client!');
response.end();
});
server.listen(3000);
console.log('Server running at http://127.0.0.1:3000/');要测试服务器,只需使用 Node.js 启动服务器,如清单 6-2 所示。
清单 6-2 。启动服务器
$ node 1raw.js
Server running at http://127.0.0.1:3000/然后在一个新窗口中使用curl测试一个 HTTP 连接,如清单 6-3 所示。服务器按照我们的预期发送数据。
清单 6-3 。使用 curl 发出客户端请求
$ curl http://127.0.0.1:3000
hello client!要退出服务器,只需在服务器启动的窗口中按 Ctrl+C。
检查割台
即使在这一点上,很多 HTTP 逻辑已经被默默地处理了。由curl发送的实际请求包含一些重要的 HTTP 头。为了看到这些,让我们修改服务器来记录在客户端请求中收到的头(由curl发送),如清单 6-4 所示。
清单 6-4 。1create/2defaultheaders.js
var http = require('http');
var server = http.createServer(function (req, res) {
console.log('request headers...');
console.log(req.headers);
// respond
res.write('hello client!');
res.end();
}).listen(3000);
console.log('server running on port 3000');现在启动服务器。我们还将要求curl使用-i(即在输出中包含协议头)选项注销服务器响应头,如清单 6-5 所示。
清单 6-5 。发出客户端请求并显示返回的响应头
$ curl http://127.0.0.1:3000 -i
HTTP/1.1 200 OK
Date: Thu, 22 May 2014 11:57:28 GMT
Connection: keep-alive
Transfer-Encoding: chunked
hello client!从curl发送的 HTTP 请求头由 Node.js HTTP 服务器处理,记录在服务器控制台上,如清单 6-6 所示。如您所见,req.headers是一个简单的 JavaScript 对象文字。您可以使用req['header-name']访问任何标题。
清单 6-6 。根据客户请求打印输出请求标题
$ node 2defaultheaders.js
server running on port 3000
request headers...
{ 'user-agent': 'curl/7.30.0',
host: '127.0.0.1:3000',
accept: '*/*',
connection: 'Keep-Alive' }
注意维基百科在http://en.wikipedia.org/wiki/List_of_HTTP_status_codes有很好的 HTTP 状态代码列表。这包括不属于 HTTP/1.1 规范的代码,这些代码将在http://tools.ietf.org/html/rfc2616中描述。
使用调试代理
帮助您探索和试验 HTTP 的一个很好的工具是 web 调试代理。调试代理是位于客户机和服务器之间的应用,它记录客户机和服务器之间交换的所有请求和响应。图 6-1 简要概述了这种交换将如何发生。
图 6-1 。web 调试代理位于客户端和服务器之间
一个非常受欢迎的免费调试代理是 fiddler ( www.telerik.com/fiddler),它有一个简单的一键安装程序,可用于 Windows 和 Mac OS X。(注意:在 Mac OS X 上,你需要安装 mono www.mono-project.com/download/才能安装 fiddler)。一旦启动 fiddler,默认情况下它会监听端口 8888。您需要告诉客户端应用使用代理来连接到服务器。对于curl,您可以使用-x(使用代理)选项来实现。启动我们刚刚创建的简单服务器并启动 fiddler。然后运行下面的命令(清单 6-7 )使用 fiddler 作为代理发出一个客户端请求。
清单 6-7 。发出指定代理服务器的 curl 请求
$ curl http://127.0.0.1:3000 -x 127.0.0.1:8888
hello client!由于 fiddler 正在运行,它将捕获请求和响应。正如你从图 6-2 中看到的,在服务器响应中从服务器发送的实际数据被略微编码,如清单 6-8 所示。
图 6-2 。Fiddler 展示了 HTTP 客户端请求和服务器响应的完整细节
清单 6-8 。服务器响应消息正文
d
hello client!
0这种编码是因为,默认情况下,Node.js 试图将响应流式传输到客户机。您会看到Transfer-Encoding服务器响应头被设置为chunked。分块传输编码是 HTTP 协议的一种数据传输机制,允许你使用块(也称为流)发送数据。在分块传输中,传输的大小(十六进制)在数据块本身之前发送,因此接收方可以知道它何时完成了该数据块的数据接收。Node.js 发送了 d(十进制 13),因为那是'【T2]'的长度。通过发送长度为 0 的块(因此结尾为 0)来终止传输。所有这些都由内置的 Node.js HTTP 服务器来完成。
响应流的关键成员
除了响应实现了一个可写的流之外,您还需要了解一些其他有用的方法。响应分为两部分:编写头部和编写主体。这是因为主体可能包含需要流式传输的大量数据。报头指定了该数据将如何呈现,并且需要由客户端在这样的流式传输可以开始之前进行解释。
一旦您调用了response.write或response.end,您设置的 HTTP 头就会被发送,随后是您想要编写的正文部分。此后,您不能再修改标题。在任何时候,您都可以使用只读的response.headersSen t 布尔值来检查报头是否被发送。
设置状态代码
默认情况下,状态代码将为 200 OK。只要没有发送消息头,就可以使用statusCode响应成员显式设置状态代码(例如,要发送 404 NOT FOUND,可以使用下面的代码:
response.statusCode = 404;设置标题
您可以使用response.setHeader(name, value)成员函数将响应中的任何 HTTP 头显式排队。您需要设置的一个常见头是响应的Content-Type,以便客户端知道如何解释服务器在主体中发送的数据。例如,如果您正在向客户端发送一个 HTML 文件,您应该将Content-Type设置为text/html,您可以使用以下代码来实现:
response.setHeader("Content-Type", "text/html");内容类型头的值的正式术语是 MIME 类型。一些关键内容类型的 MIME 类型如表 6-1 所示。
表 6-1 。流行的 MIME 类型
|
名字
|
MIME 类型
| | --- | --- | | 超文本标记语言 | 文本/html | | 层叠样式表 | 文本/css | | Java Script 语言 | 应用/javascript | | JavaScript 对象符号(JSON) | 应用/json | | JPEG 图像 | 图像/jpeg | | 便携式网络图形(PNG) | 影像/png |
对于这个以及更多的 mime 类型,有一个简单的 NPM 包叫做 mime ( npm install mime),你可以用它从一个文件扩展名中获取官方的 MIME 类型。清单 6-9 展示了如何使用它。
清单 6-9 。演示使用哑剧 NPM 包
var mime = require('mime');
mime.lookup('/path/to/file.txt'); // => 'text/plain'
mime.lookup('file.txt'); // => 'text/plain'
mime.lookup('.TXT'); // => 'text/plain'
mime.lookup('htm'); // => 'text/html'回到我们的头讨论,您可以使用response.getHeader函数获得一个排队等待发送的头:
var contentType = response.getHeader('content-type');您可以使用response.removeHeader功能从队列中删除标题:
response.removeHeader('Content-Encoding');仅发送邮件头
当您想要显式地发送消息头(而不仅仅是将它们排队)并将响应移入仅主体模式时,您可以调用response.writeHead成员函数。该函数获取状态代码和可选的头,这些头将被添加到您可能已经使用response. setHeader 排队的任何头中。例如,下面的代码片段将状态代码设置为200,并为提供 HTML 设置了Content-Type标题:
response.writeHead(200, { 'Content-Type': 'text/html' });请求流的主要成员
请求也是一个可读的流。这对于客户端希望将数据传输到服务器的情况非常有用,例如文件上传。客户端 HTTP 请求也被分成头部和主体部分。我们可以得到关于客户端请求 HTTP 头的有用信息。例如,我们已经看到了request.headers属性,它只是标题名和值的只读映射(JavaScript 对象文字)(如清单 6-10 所示)。
清单 6-10 。演示读取请求头的代码片段
// Prints something like:
//
// { 'user-agent': 'curl/7.30.0',
// host: '127.0.0.1:3000',
// accept: '*/*' }
console.log(request.headers);要检查单个头,可以像索引任何其他 JavaScript 对象文字一样索引该对象:
console.log(request.headers['user-agent']); // 'curl/7.30.0'响应请求时需要的一条关键信息是客户机发出请求时使用的 HTTP 方法和 URL。为了创建 RESTful web 应用,这些信息是必需的。您可以从request.method只读属性中获取使用的 HTTP 方法。您可以使用request.url属性获得客户端请求的 URL。例如,考虑以下客户端请求:
GET /resources HTTP/1.1
Accept: */*在这种情况下,request.method将是GET,request.url将是/resources。
创建自己的文件 Web 服务器
提供基本 HTML
既然我们对响应流和 MIME 类型有了更深的理解,我们可以创建一个简单的 web 服务器,从文件夹中返回 HTML 文件。创建一个简单的 HTML 文件,名为index.html ,我们计划在每次请求获取服务器上的“/”时返回该文件,如清单 6-11 中的所示。
清单 6-11 。2server/public/index.html
<html>
<head>
<title>Hello there</title>
</head>
<body>
You are looking lovely!
</body>
</html>首先,让我们创建几个实用函数。将功能分解成独立的函数总比分解成一整块代码要好。如果我们收到一个我们不接受的url请求,我们应该返回一个 404(未找到)HTTP 响应。清单 6-12 提供了一个功能来做这件事。
清单 6-12 。一个实用函数返回 404 未找到 HTTP 响应
function send404(response) {
response.writeHead(404, { 'Content-Type': 'text/plain' });
response.write('Error 404: Resource not found.');
response.end();
}如果我们能够满足请求,我们应该返回 HTTP 200 以及内容的 MIME 类型。返回 HTML 文件非常简单,只需创建一个读取文件流,并通过管道将其发送到响应。清单 6-13 显示了完整的服务器代码。
清单 6-13 。来自 2server/server.js 的代码
var http = require('http');
var fs = require('fs');
function send404(response) {
response.writeHead(404, { 'Content-Type': 'text/plain' });
response.write('Error 404: Resource not found.');
response.end();
}
var server = http.createServer(function (req, res) {
if (req.method == 'GET' && req.url == '/') {
res.writeHead(200, { 'content-type': 'text/html' });
fs.createReadStream('./public/index.html').pipe(res);
}
else {
send404(res);
}
}).listen(3000);
console.log('server running on port 3000');如果你启动服务器(从第六章/第二章服务器目录运行node server.js)并在http://localhost:3000打开浏览器,你会看到我们之前创建的 HTML 页面(图 6-3 )。
图 6-3 。浏览器显示 index.html 已成功返回
类似地,如果你访问localhost上的任何其他 URL,你会得到一个 404 错误信息(图 6-4 )。
图 6-4 。浏览器显示对不存在的资源请求返回的错误
服务目录
对于快速手写静态文件服务器来说,这是一个好的开始。但是,它只服务于一个文件。让我们把它打开一点,为一个目录的所有内容提供服务。首先,创建一个简单的客户端 JavaScript 文件,在 HTML 加载完成后附加到主体,如清单 6-14 所示。我们计划向服务器请求这个 JavaScript 文件。
清单 6-14 。3serverjs/public/main.js 中的代码
window.onload = function () {
document.body.innerHTML += '<strong>Talk JavaScript with me</strong>';
}让我们通过在<head>中添加一个脚本标签来加载客户端 JavaScript 文件,从而修改我们的简单 HTML 文件
<script src="./main.js"></script>现在,如果我们运行相同的旧服务器,当我们的浏览器解析index.html并试图从服务器加载main.js时,我们将得到 404。为了支持 JavaScript 加载,我们需要做以下工作:
- 使用 path 模块根据
request.url属性解析文件系统上文件的路径 - 查看我们是否为请求的文件类型注册了 MIME 类型
- 在我们尝试从文件系统中读取文件之前,请确保该文件存在
基于我们已经知道的,我们可以编写如清单 6-15 所示的服务器。
清单 6-15 。3serverjs/server.js 中的代码
var http = require('http');
var fs = require('fs');
var path = require('path');
function send404(response) {
response.writeHead(404, { 'Content-Type': 'text/plain' });
response.write('Error 404: Resource not found.');
response.end();
}
var mimeLookup = {
'.js': 'application/javascript',
'.html': 'text/html'
};
var server = http.createServer(function (req, res) {
if (req.method == 'GET') {
// resolve file path to filesystem path
var fileurl;
if (req.url == '/') fileurl = '/index.html';
else fileurl = req.url;
var filepath = path.resolve('./public' + fileurl);
// lookup mime type
var fileExt = path.extname(filepath);
var mimeType = mimeLookup[fileExt];
if (!mimeType) {
send404(res);
return;
}
// see if we have that file
fs.exists(filepath, function (exists) {
// if not
if (!exists) {
send404(res);
return;
};
// finally stream the file
res.writeHead(200, { 'content-type': mimeType });
fs.createReadStream(filepath).pipe(res);
});
}
else {
send404(res);
}
}).listen(3000);
console.log('server running on port 3000');示例中的大部分代码都是不言自明的,并且突出显示了有趣的部分。如果你现在打开浏览器并访问localhost:3000,你会看到 HTML 被请求,JavaScript 被成功加载并运行(图 6-5 )。
图 6-5 。显示客户端 JavaScript 被成功请求和呈现的浏览器
我们当前的实现仍然缺少很多功能。首先,它无法抵御恶意 URL。例如,您可以利用我们的实现中有一个简单的path.resolve来从服务器文件系统请求任何文件,如清单 6-16 所示(这里我们从服务器请求服务器代码)。
清单 6-16 。演示我们的简单文件服务器中的文件系统列表漏洞
$ curl 127.0.0.1:3000/../server.js
var http = require('http');
var fs = require('fs');
var path = require('path');
...truncated... the rest of server.js然后是错误处理和文件缓存,这两者在我们的实现中都是缺乏的。拥有从头构建自己的 Node.js web 服务器的知识是非常宝贵的,但是您不必从头构建 web 服务器。社区已经为您完成了,我们将在稍后探索这些选项。
介绍 Connect
正如我们所看到的,core Node.js 模块为构建您自己的 web 应用提供了基本但重要的特性。NPM 上有很多基于此的 web 框架。很流行的一个是 connect ( npm install connect),这是一个中间件框架。
中间件 基本上是位于你的应用代码和一些低级 API 之间的任何软件。Connect 扩展了内置的 HTTP 服务器功能,并添加了一个插件框架。插件充当中间件,因此 connect 是一个中间件框架。
Connect 最近进行了一次彻底检查(connect 3.0),现在核心 connect 只是中间件框架。每个中间件都是自己独立的 NPM 模块,是更大的 Connect/ExpressJS 生态系统的一部分。我们将在下一章探讨这些中间件。在这里,我们重点关注使用 connect 和创作我们自己的中间件。
创建一个基本的连接应用
connect 的核心是connect功能。调用此函数将创建连接调度程序。connect dispatcher 只是一个接受请求/响应参数的函数,这意味着 dispatcher 可以用作http.createServer(我们前面已经看到)的参数。清单 6-17 显示了基本的连接应用。
清单 6-17 。4connect/1basic.js
var connect = require('connect')
, http = require('http');
// Create a connect dispatcher
var app = connect();
// Register with http
http.createServer(app)
.listen(3000);
console.log('server running on port 3000');如果您运行此服务器,它将为每个客户端请求返回 404(未找到)。这是 connect 提供的内置行为。如果没有一些中间件来处理客户端请求,connect 将返回 404。我们将在下一节创建中间件。
除了可以接受请求和响应对象的函数之外,connect dispatcher 还有一个成员函数use,用于向 connect 注册中间件。当我们创建自己的连接中间件时,我们将很快看到这个函数。
一个效用函数是listen 。我们将在内部调用http.createServer,用它注册 connect dispatcher,就像我们之前展示的那样,(即http.createServer(app)),最后调用创建的服务器的listen函数。所以你可以简单地做清单 6-18 所示的事情,但是知道它仍然是 Node 核心http之上的一个调度器是很有用的。
清单 6-18 。4 连接/2 简单者. js
var connect = require('connect');
// Create a connect dispatcher and register with http
var app = connect()
.listen(3000);
console.log('server running on port 3000');创建连接中间件
要向 connect 注册中间件,请调用 connect dispatcher 上的'use'成员方法,传递一个函数,该函数有三个参数—请求、响应和下一个回调:
- 请求派生自我们前面看到的 Node.js HTTP 请求类
- response 来自我们前面看到的 Node.js HTTP 响应类
- next 允许您可选地将控制传递给注册了 connect 的下一个中间件,或者通知 connect 一个错误
最简单的无操作(no operation)中间件是不查看请求、不修改响应、不简单地将控制权移交给下一个中间件的中间件,如清单 6-19 所示。
清单 6-19 。5 中间件/1 OOP . js
var connect = require('connect');
// Create a connect dispatcher and register with http
var app = connect()
// register a middleware
.use(function (req, res, next) { next(); })
.listen(3000);
console.log('server running on port 3000');现在我们已经熟悉了中间件的基础,让我们创建一个记录客户端请求的method和url的中间件,如清单 6-20 所示。
清单 6-20 。5 中间件/2logit.js
var util = require('util');
// a simple logging middleware
function logit(req, res, next) {
util.log(util.format('Request recieved: %s, %s', req.method, req.url));
next();
}
var connect = require('connect');
connect()
.use(logit)
.listen(3000);让我们创建另一个中间件,它将客户机请求回显给客户机。由于客户端请求是一个读流,而响应是一个写流,我们可以简单地通过管道传输这两个流,如清单 6-21 所示。
清单 6-21 。5 中间件/3echo.js
function echo(req, res, next) {
req.pipe(res);
}
var connect = require('connect');
connect()
.use(echo)
.listen(3000);现在,如果您运行这个应用并发出 curl 请求,请求体(-d,即curl的数据参数)将成为响应体:
$ curl http://127.0.0.1:3000/ -d "hello world!"
hello world!通过路径前缀挂载中间件
use函数采用可选的第一个参数来指定将触发指定中间件的端点。这被称为挂载,因为它类似于操作系统磁盘挂载。例如,假设我们希望仅在请求“/echo”时回应。对于所有其他请求,我们将返回消息“Wassup”。这可以实现,如清单 6-22 所示。
清单 6-22 。5 中间件/4prefix.js
function echo(req, res, next) {
req.pipe(res);
}
var connect = require('connect');
connect()
.use('/echo', echo)
.use(function (req, res) { res.end('Wassup!'); })
.listen(3000);所有以“/echo”开头的请求将由echo中间件处理,而其他请求将被传递给我们的Wassup!响应器。正如你在清单 6-23 中看到的,它的行为和预期的一样。
清单 6-23 。演示安装
$ curl http://127.0.0.1:3000/echo -d "hello world!"
hello world!
$ curl http://127.0.0.1:3000/ -d "hello world!"
Wassup!需要路径前缀的一个简单例子是在特定的前缀(例如,'/public ')托管静态文件中间件。
挂载的另一个优点是,它允许您轻松地更改 URL,而不需要更新中间件。你的中间件应该而不是检查req.url。假设它被安装在需要进行处理的地方。
使用对象作为中间件
作为一个中间件作者,您可以选择使用一个对象(而不是一个简单的函数)来创建一个中间件,只要该对象有一个handle方法。例如,echo中间件作为一个对象将如清单 6-24 所示。
清单 6-24 。5 中间件/5object.js
var echo = {
handle: function (req, res, next) {
req.pipe(res);
}
};
var connect = require('connect');
connect()
.use(echo)
.listen(3000);这允许您使用类实例作为中间件,只要它们有一个句柄成员函数。这只是为了方便起见,你可以放心地忽略这一点。
创建可配置的中间件
您可以使用 JavaScript 闭包的力量来创建可配置的中间件。例如,在清单 6-25 中,我们展示了一个中间件,它总是根据它的配置返回相同的消息。配置message由我们返回的函数在闭包中捕获。
清单 6-25 。5 中间件/6configurable.js
// Configurable middleware creator
function greeter(message) {
return function (req, res, next) {
res.end(message);
};
}
var helloWorldGreeter = greeter('Hello world!');
var heyThereGreeter = greeter('Hey there!');
var connect = require('connect');
connect()
.use('/hello', helloWorldGreeter)
.use('/hey', heyThereGreeter)
.listen(3000);结果显示在清单 6-26 中。
清单 6-26 。演示如何使用已配置的中间件
$ curl http://127.0.0.1:3000/hello
Hello world!
$ curl http://127.0.0.1:3000/hey
Hey there!连锁的力量
中间件的链接很棒有很多原因。例如,它允许中间件共享处理请求和响应的功能。您还可以使用它来提供授权和身份验证。让我们考虑几个实际的例子。
共享请求/响应信息
传递到每个中间件的请求和响应对象是可变的和共享的。您可以利用这一点让一个中间件为您部分处理请求,使它更容易被后来的中间件使用。作为一个例子,考虑一个简单的中间件,如果它检测到这是一个 JSON 请求,它试图将主体处理成一个 JavaScript 对象,如清单 6-27 所示。
清单 6-27 。来自 6chain/1parse.js 的片段
function parseJSON(req, res, next) {
if (req.headers['content-type'] == 'application/json') {
// Load all the data
var readData = '';
req.on('readable', function () {
readData += req.read();
});
// Try to parse
req.on('end', function () {
try {
req.body = JSON.parse(readData);
}
catch (e) { }
next();
})
}
else {
next();
}
}下面是它的工作原理:
- 它只是检查客户机请求是否属于类型
application/json。如果没有,它将控制权传递给下一个中间件。 - 否则,它等待客户机请求完全传输到服务器,一旦完成,就尝试使用
JSON.parse解析数据。 - 如果成功,则设置
req.body。 - 无论 JSON 是否被解析和
req.body是否被设置,我们仍然将控制权传递给下一个中间件。
因为链接了在我们的parseJSON之后出现的任何中间件,如果请求包含有效的 JSON,中间件将访问在req.body中解析的 JSON 对象。在清单 6-28 中,我们有一个简单的连接服务器,它添加了一个中间件,如果找到有效的 JSON,这个中间件使用parseJSON的结果告诉客户端req.body.foo的值。
清单 6-28 。来自 6chain/1parse.js 的片段
var connect = require('connect');
connect()
.use(parseJSON)
.use(function (req, res) {
if (req.body) {
res.end('JSON parsed!, value of foo: '+ req.body.foo);
}
else {
res.end('no JSON detected!');
}
})
.listen(3000);如果您使用curl测试它,您将看到传递的 JSON 对象的foo成员的值(如果存在的话)。否则,如果你传递一个无效的 JSON 或非 JSON 请求,你将得到“没有检测到 JSON”的消息,如清单 6-29 所示。
清单 6-29 。parseJSON 中间件的运行演示
$ curl http://127.0.0.1:3000/ -H "content-type: application/json" -d "{\"foo\":123}"
JSON parsed!, value of foo: 123
$ curl http://127.0.0.1:3000/ -H "content-type: application/json" -d "{\"foo\":123,}"
no JSON detected!链接示例:验证请求/限制访问
因为我们需要通过调用 next 显式地将控制传递给下一个中间件,所以我们可以通过不调用 next 并自己终止响应(res.end)来随时选择性地停止执行。
让我们实现一个基本访问授权中间件,如果客户端请求没有正确的凭证,它将返回 401 未授权。基本授权是一个简单的标准化协议,其中每个客户端请求都需要包含一个Authorization头。标头需要按如下方式构建:
- 用户名和密码组合成一个字符串:“用户名:密码”。
- 然后使用 Base64 对结果字符串进行编码。
- 然后,授权方法和一个空格(即“Basic ”)被放在编码字符串的前面。
一个示例客户端报头是Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==。
此外,为了通知客户机它需要添加一个Authorization头,服务器应该在拒绝客户机请求的响应中发送一个WWW-Authenticate头。这很容易做到,我们可以创建一个如清单 6-30 所示的效用函数。
清单 6-30 。实用程序函数发送 401 未授权的 HTTP 响应,请求基本授权
function send401(){
res.writeHead(401 , {'WWW-Authenticate': 'Basic'});
res.end();
}为了解码客户端的Authorization头,我们反向执行创建步骤。换句话说,我们通过在空间上拆分的方式去掉授权方法,也就是“”,加载第二段为 Base64,转换成一个简单的字符串。最后,我们使用“:”来获取用户名/密码。代码如清单 6-31 所示。
清单 6-31 。读取客户端发送的基本身份验证凭据的代码段
var auth = new Buffer(authHeader.split(' ')[1], 'base64').toString().split(':');
var user = auth[0];
var pass = auth[1];我们现在有足够的信息来创建添加基本访问授权的中间件,如清单 6-32 所示。
清单 6-32 。7auth/1auth.js 中列出身份验证中间件的代码段
function auth(req, res, next) {
function send401(){
res.writeHead(401 , {'WWW-Authenticate': 'Basic'});
res.end();
}
var authHeader = req.headers.authorization;
if (!authHeader) {
send401();
return;
}
var auth = new Buffer(authHeader.split(' ')[1], 'base64').toString().split(':');
var user = auth[0];
var pass = auth[1];
if (user == 'foo' && pass == 'bar') {
next(); // all good
}
else {
send401();
}
}作为一个演示,这个中间件目前只接受username = foo和password = bar,但是如果我们想的话,我们可以很容易地使它可配置。请注意,只有当访问被授权时,我们才调用next(),这样就可以使用它来提供针对不良凭证的保护。我们在清单 6-33 中演示了如何使用这个中间件。
清单 6-33 。来自 7auth/1auth.js 的代码段演示了如何使用身份验证中间件
var connect = require('connect');
connect()
.use(auth)
.use(function (req, res) { res.end('Authorized!'); })
.listen(3000);我们来测试一下(从7auth/1auth.js开始启动服务器)。如果你在http://localhost:3000点打开浏览器,你会看到一个熟悉的用户名/密码提示。因为中间件响应了一个 401 未授权响应和一个WWW-Authenticate头,所以浏览器要求您提供凭证。(参见图 6-6 )。
图 6-6 。当服务器请求基本身份验证时,浏览器内置对话框
如果您键入了错误的用户名/密码,它将继续提示您输入凭据,因为我们将不断返回 401,直到成功进行身份验证尝试。在这种情况下,我们的中间件将控制权传递给下一个中间件,后者只是返回消息“Authorized!”如图图 6-7 所示。
图 6-7 。浏览器发送有效凭据时的服务器响应
我们可以重用这个认证中间件来限制特定的区域。(例如,在清单 6-34 中,只有'/admin '受到限制。)默认情况下,它会落到公共处理程序。请注意,我们根本不需要更改中间件代码来实现这一点。
清单 6-34 。来自 7auth/2authArea.js 的代码段演示了如何为管理区域安装
connect()
.use('/admin', auth)
.use('/admin', function (req, res) { res.end('Authorized!'); })
.use(function (req, res) { res.end('Public') })
.listen(3000);这很好地总结了一个可链接的、可选的中间件框架的力量。
引发连接错误
最后值得一提的是,您可以选择向'next'传递一个参数,通知 connect 您的中间件发生了错误。链中没有其他中间件被调用,错误消息被发送到客户端请求,HTTP 状态代码为 500 内部服务器错误。清单 6-35 是演示这一点的一个简单例子。
清单 6-35 。8 错误/1 错误. js
var connect = require('connect');
connect()
.use(function (req, res, next) { next('An error has occurred!') })
.use(function (req, res, next) { res.end('I will never get called'); })
.listen(3000);如果您运行此服务器并发出请求,您将收到错误消息“发生了错误!”:
$ curl http://127.0.0.1:3000
An error has occurred!可以看到第二个中间件从未被调用过。此外,通常使用实际的Error对象,而不是我们在这里使用的字符串——换句话说,next(new Error('An error has occurred'))。
如果您想处理其他中间件错误,您可以注册一个带有四个参数的中间件(而不是我们已经看到的三个req、res、next)。这种情况下的第一个参数是错误,所以error,req,res,next是四个参数。这样的中间件只有出错时才会调用。清单 6-36 展示了它是如何工作的。
清单 6-36 。8 错误/2 错误处理程序. js
var connect = require('connect');
connect()
.use(function (req, res, next) { next(new Error('Big bad error details')); })
.use(function (req, res, next) { res.end('I will never get called'); })
.use(function (err, req, res, next) {
// Log the error on the server
console.log('Error handled:', err.message);
console.log('Stacktrace:', err.stack);
// inform the client
res.writeHead(500);
res.end('Unable to process the request');
})
.listen(3000);如果您运行此服务器并发出请求,您将仅获得我们在响应中有意发送的信息:
$ curl http://127.0.0.1:3000
Unable to process the request然而,服务器日志的信息更加丰富,如清单 6-37 所示(因为我们使用了new Error)。
清单 6-37 。展示来自中间件错误处理程序的服务器日志
node 2errorHandler.js
Error handled: Big bad error details
Stacktrace: Error: Big bad error details
at Object.handle (2errorHandler.js:4:43)
at next (/node_modules/connect/lib/proto.js:194:15)
... truncated ...还要注意,这个错误处理程序是为在这个错误处理程序之前发生在中间件任何地方的所有错误调用的。比如在清单 6-38 中,我们故意抛出一个错误,而不是next(error),它仍然被正确处理。
清单 6-38 。8error/3throwErrorHandler.js
var connect = require('connect');
connect()
.use(function () { throw new Error('Big bad error details'); })
.use(function (req, res, next) { res.end('I will never get called'); })
.use(function (err, req, res, next) {
console.log('Error handled:', err.message);
res.writeHead(500);
res.end('Server error!');
})
.listen(3000);这种错误处理方法实际上使 connect 比 raw http.createServer更安全,在 rawhttp.createServer中,未处理的错误会使服务器崩溃。
请注意,错误处理程序仅在出现错误时调用。例如,在清单 6-39 所示的服务器中,它永远不会被调用。所以你永远不需要检查错误处理程序中的错误——如果错误处理程序被调用,它应该一直在那里。
清单 6-39 。8error/4onlyCalledOnError.js
var connect = require('connect');
connect()
.use(function (req, res, next) { next(); })
.use(function (err, req, res, next) {
res.end('Error occured!');
})
.use(function (req, res, next) { res.end('No error'); })
.listen(3000);最后,值得注意的是,您可以选择从错误处理程序调用 next,并将控制权传递给链中的任何其他中间件。
安全超文本传输协议
HTTPS 是在许多以前的 web 框架中难以实现的东西之一。Node.js 对 HTTPS 内置有一流的支持。在我们展示通过 Node.js 使用 HTTPS 有多简单之前,我们将为初学者提供一个快速概述。
不对称密码术
使 HTTPS 成为可能的基本概念是公钥加密(也称为非对称加密)。对于这种类型的加密,您需要两个加密密钥:
- 一个每个人都知道的公钥,甚至可能是恶意用户
- 只有你知道的私钥
而且,
- 公钥用于加密。这意味着每个人都可以和你交谈。
- 解密需要一个私钥。这意味着只有你能理解别人说的话!
你可以看到,它让每个人都可以安全地与你交谈,而没有偷听的机会。(参见图 6-8 )。
图 6-8 。使用公钥/私钥组合可以让别人安全地与你交谈
出于您的兴趣,值得一提的是,有大量的算法可以轻松计算这样的密钥对。优势在于,从相应的公钥中确定一个正确生成的私钥几乎是不可能的(在计算上是不可行的)。
通过两种方式确保通信安全
因此,共享公钥使得与服务器开始对话变得安全。你如何安全地回嘴?很简单。用户(基本上是浏览器)即时生成一个预主密钥,并以加密消息(用服务器公钥加密)的形式安全地发送给服务器。预主密钥用于生成会话密钥,该密钥仅对客户端和服务器之间的会话有效。现在,如果服务器和客户机用这个共享的会话密钥加密消息,它们就可以互相交谈了。
这是 SSL(或称为 TLS 的新版本)握手的简化描述。握手之后,实际的标准 HTTP 对话发生,其中整个 HTTP 消息(包括头)使用会话密钥加密。HTTPS 就是通过 SSL 安全通信通道进行的 HTTP 通信。
生成密钥
在本节中,我们将自己生成公钥/私钥对。为此,我们将使用 OpenSSL 命令行工具,它在 Mac OS X 和 Windows 上都可用(作为 Cygwin 的一部分,我们在curl中也需要它)。
为了生成私钥,我们指定一个加密算法及其复杂性。使用清单 6-40 中的命令生成一个 1024 位的 RSA 密钥。创建的key.pem文件的一个样本如清单 6-41 所示。
清单 6-40 。生成私钥
$ openssl genrsa 1024 > key.pem清单 6-41 。生成的私钥示例
-----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQDJW6ZZLTawfDyhR8v6/nQMX+PIGtPMO8n7OwRdv1AqqW7a+5Au
... truncated ...
0j/PimhgOvsD0TDxccytEsLgoldWcx4YLGjzDtoyyaVj
-----END RSA PRIVATE KEY-----接下来,我们需要生成相应的公钥,您可以使用以下命令来完成:
$ openssl req -x509 -new -key key.pem > cert.pem一旦你运行这个命令,你会被问一些问题,比如你的国家名称,所有这些都有一个简单的答案。这创建了一个我们可以与世界共享的公共证书,看起来像清单 6-42 中的代码。
清单 6-42 。公共证书样本
-----BEGIN CERTIFICATE-----
MIIDLjCCApegAwIBAgIJAMdFJbVshZIGMA0GCSqGSIb3DQEBBQUAMG4xCzAJBgNV
... truncated ...
9i+ULx/F6dKgwTLV5L5urT4kIOitM6+QyT+bd1uZ3MXeKaaaJ+dh93aFuFVvxZ3d
t2E=
-----END CERTIFICATE-----现在让我们用这些钥匙。
创建一个 HTTPS 服务器
Node.js 有一个核心的https模块,您可以使用require('https')来加载它。它有一个createServer函数,其行为与http.createServer完全相同,除了它额外采用了第一个'options'参数,您可以用它来提供公钥/私钥。
清单 6-43 是我们在本章前面看到的简单 HTTP 服务器,更新后可以使用 HTTPS。
清单 6-43 。9ssl/1basic.js
var https = require('https');
var fs = require('fs');
var options = {
key: fs.readFileSync('./key.pem'),
cert: fs.readFileSync('./cert.pem')
};
https.createServer(options, function (req, res) {
res.end('hello client!');
}).listen(3000);如您所见,侦听器功能完全相同—它传递一个我们已经熟悉的请求和响应对象。你可以使用curl来测试,如清单 6-44 中的所示。k 参数允许不安全的(insekure)/未验证的证书工作,因为我们自己创建了我们的证书。
清单 6-44 。从 curl 测试我们的 HTTPS 服务器
$ curl https://localhost:3000 -k
hello client!因为http.createServer和https.createServer的监听器函数是相同的,所以在 HTTPS 中使用 connect 就像在 HTTP 中使用它一样简单。这在清单 6-45 中有演示。
清单 6-45 。9ssl/2connect.js
var https = require('https');
var fs = require('fs');
var options = {
key: fs.readFileSync('./key.pem'),
cert: fs.readFileSync('./cert.pem')
};
var connect = require('connect');
// Create a connect dispatcher
var app = connect();
// Register with https
https.createServer(options, app)
.listen(3000);对于面向公众的网站,您需要从可信(用户信任的)认证机构(例如,VeriSign 和 Thawte)获得 SSL 证书。证书颁发机构可以保证这个公钥对于跟你说话来说是唯一安全的。这是因为在没有这种权威的情况下,可能有人坐在客户端和服务器之间(称为中间人 MitM 攻击)并声称自己是服务器,向你发送他们的公钥,而不是服务器公钥。这样,MitM 可以解密您的消息(因为他们有相应的私钥),监视它,然后通过用服务器公钥重新加密它,将它转发给服务器。
默认情况下使用 HTTPS
如果您只使用 HTTPS 并将所有 HTTP 流量重定向到使用 HTTPS,那么创建一个安全的网站会更容易确保。现在很多网站都这么做。例如,如果你访问http://www.facebook.com,它会向你发送一个 301(永久移动)HTTP 响应,其中Location头设置为https://www.facebook.com。你可以自己试试;如果在浏览器中打开http://www.facebook.com,地址将变为https://www.facebook.com。因此,只要有可能,就使用 HTTPS 并将所有 HTTP 流量重定向到 HTTPS。
虽然可能,但在同一个端口上运行 HTTP 和 HTTPS 服务器并不容易。然而,你不必。按照惯例,当您请求一个 HTTP 网站而没有指定端口(例如, http ://127.0.0.1)时,客户端会尝试连接到端口 80。然而,如果你请求一个 HTTPS 网站而没有指定端口(比如httpsT1),客户端会尝试连接到服务器上的端口 443。这允许您在端口 443 上运行一个 HTTPS 服务器,在端口 80 上运行一个 HTTP 服务器,简单地重定向客户端请求以使用 HTTPS。完成这项工作的代码如清单 6-46 所示。
清单 6-46 。10 重定向/secure.js
var https = require('https');
var fs = require('fs');
var options = {
key: fs.readFileSync('./key.pem'),
cert: fs.readFileSync('./cert.pem')
};
https.createServer(options, function (req, res) {
res.end('secure!');
}).listen(443);
// Redirect from http port 80 to https
var http = require('http');
http.createServer(function (req, res) {
res.writeHead(301, { "Location": "https://" + req.headers['host'] + req.url });
res.end();
}).listen(80);现在运行这个服务器。(在 Mac OS X 上,您需要使用sudo—也就是sudo node 10redirect/secure.js—来运行,因为只有超级用户可以监听端口 80 和端口 443)。如果你在浏览器中访问http://127.0.0.1,你会注意到网址变成了https://127.0.0.1,你会得到消息“安全!”。这是因为浏览器知道如何处理 HTTP 重定向响应,并为您无声地完成它。要查看内部工作原理,你可以使用curl。你可以分别在清单 6-47 的和清单 6-48 的中看到 HTTP 和 HTTPS 场景。
清单 6-47 。向我们的安全服务器发出 HTTPS 请求
$ curl https://127.0.0.1 -k
secure!清单 6-48 。向我们的安全服务器发出 HTTP 请求会得到重定向响应
$ curl http://127.0.0.1 -i
HTTP/1.1 301 Moved Permanently
Location: https://127.0.0.1/
Date: Sun, 01 Jun 2014 06:15:16 GMT
Connection: keep-alive
Transfer-Encoding: chunked如果您的所有通信都通过 HTTPS 进行,您可以使用基本的 HTML 输入表单接受客户端密码,而不用担心被拦截。
摘要
本章开始时,我们对 core Node.js 中的内置 HTTP 功能进行了彻底的检查。这种仔细的检查是必要的,因为社区框架依赖它来提供高级功能,并且很好地掌握基础知识将有助于使您成为一名成功的 Node.js 开发人员。
随后,我们深入研究了连接中间件框架。Connect 允许您共享和创建可管理的 HTTP 插件。这有助于您管理复杂性,从而提高您的生产力,因为您可以划分并征服软件需求。Connect 专注于 HTTP 应用,而不是非常专注于网站,这是 express js(connect 团队的另一个框架)非常适合的。我们将在本书的后面讨论 ExpressJS。但是要知道所有的 connect 中间件都可以与 ExpressJS 一起使用,因为它具有相同的中间件约定(即use成员函数)。
虽然我们在这一章中写了一些中间件,但是 Node.js 社区已经写了很多优秀的(经过测试的和安全的)中间件。我们将在下一章研究这些。
最后,我们展示了 Node.js 中的 HTTPS 有多简单。只要有可能,您应该对所有服务器使用 HTTPS。
Node.js 专注于应用服务器。这意味着你可以深深地嵌入 HTTP,并完全接受网络提供的所有创新。事实上,网络协议开发人员转向 Node.js 进行原型开发并不少见,这是因为 node . js 的访问级别较低,但内存管理非常好。







