Better

Ethan的博客,欢迎访问交流

Node与Express开发之进阶篇(一)

最近准备系统的学习Node和Express,学习笔记-进阶篇!主要内容有表单处理、Cookie与会话、中间件等。

表单处理

从用户那里收集信息的常用方法就是使用HTML表单,无论是使用浏览器提交表单,还是使用AJAX提交,底层机制仍旧是HTML表单。

发送数据

大体而言,向服务器发送数据有两种方式:查询字符串和请求正文。

一种普遍的误解是POST请求是安全的,而GET请求不安全。事实上如果使用HTTPS协议,两者都是安全的,如果不使用,则都不安全。因为不使用HTTPS协议,入侵者会向查看GET请求的查询字符串一样,轻松查看到POST请求的报文数据。

那么为什么推荐使用POST提交数据呢?

  1. GET请求,用户会在查询字符串中看到所有输入数据,这是丑陋而且凌乱的
  2. 浏览器会限制查询字符串的长度,对请求正文没有长度限制

HTML表单

基础

<form>标记中提交方法被明确地指定为POST,如果不这么做,默认进行GET提交。action的值被指定为用于接受表单数据的URL,如果忽略这个值,表单会提交到他被加载进来时的同一URL。

从服务器的角度来看,最重要的属性是<input>域中name属性,这样服务器才能识别字段。

编码

表单被提交时(浏览器或AJAX),某种程度上必须被编码,如果不明确指定编码,则默认为application/x-xxxform-urlencoded,这是一个冗长的用于URL编码的媒体类型。

如果需要上传文件,使用URL编码很难发送文件,所以你不得不使用multipart/form-data编码类型。

处理表单

无论使用什么路径处理表单,必须决定如何响应浏览器,下面是你的选项:

  • 直接响应html
  • 302重定向
  • 303重定向 - 用于响应表单提交请求的推荐方法

那么重定向指向哪里?

  • 重定向到专用的成功或失败页面
    • 优点:便于分析,容易实现
    • 缺点:必须针对每种可能性分配URL,意味着页面设计、编写复制和维护。用户体验欠佳,干扰用户的导航
  • 运用flash消息重定向到原位置
    最好的用户体验是不干扰用户的导航流,也就是说需要一个不用离开当前页面就能提交表单的方法(当然可以用AJAX),如果不用AJAX,或者希望备用机制可以能够提供一个好的用户体验,可以重定向回用户之前浏览的页面。
  • 运用flash消息重定向到新位置
    在这种情况下,需要考虑用户接下来想去哪儿,并相应的进行重定向。

Express表单处理

如果使用GET提交,表单域在req.query中。

如果使用POST提交,提要引入中间件解析URL编码体。安装body-parser,然后加入管道:app.use(require('body-parser')());一旦引入body-parser之后,req.body变得可用。

文件上传

对于复合表单处理,目前两种流行而健壮的选择(如今不推荐使用multipart):Busboy和Formidable。Formidable要稍微简单一些,因为它有一个方便的回调方法,能够提供包含字段和文件信息的对象。对于Buyboy而言,你必须对每个字段和文件事件进行监听。

注意:我们必须在form中指定enctype='multipart/form-data'来启用文件上传。也可以通过input file标签的accept属性来限制文件上传的类型。

Formidable中间件的使用

var formidable = require('formidable');
app.post('upload',function(req,res){
    var form = new formidable.IncomingForm();
    form.parse(req,function(err,fields,files){

    })
})

fields是一个有字段名称属性的对象。文件对象包含更多的数据,有文件大小,上传路径(通常是一个临时目录的随机名字),用户上传此文件的原始名字。接下来如何处理这个文件就取决于你了:

  • 保存到数据库
  • 复制到更持久的位置
  • 上传到云端文件存储系统(推荐)

Cookie和会话

HTTP是无状态协议,我们需要用某种办法在HTTP协议上建立状态,于是便有了Cookie和会话。

Cookie的名声并不好,因为人们用他做了些邪恶的事情。之所以说不幸,是因为cookie对现代Web的功能至关重要。关于cookie你需要知道的事情:

  • cookie对用户来说不是加密的
  • 用户可以删除或禁用cookie
  • 一般的cookie可以被篡改 - 要确保cookie不被篡改,请使用签名cookie
  • cookie可以用于攻击 - xss攻击,使用签名cookie和指明cookie只能由服务器修改,会更安全
  • 如果滥用cookie,用户会注意到 - 尽量把cookie的使用限制在最小范围
  • 如果可以选择,会话要优于cookie

cookie不是魔法,当服务器希望客户端保存一个cookie时,会发送一个响应头set-cookie,其中包含键值对。当客户端向服务器发送含有cookie的请求时,他会发送多个请求头cookie,其中包含这些cookie的值。

凭证外化

为了保证cookie的安全,必须有一个cookie密钥。cookie密钥是一个字符串,服务器知道它是什么,会在cookie发送到客户端时对cookie加密。

外化第三方凭证是一种常见的做法,比如cookie密钥,数据库密码和API令牌。不仅易于维护,还可以让你的版本控制系统忽略这些凭证文件。

比如将凭证外化文件放在一个JavaScript文件中,创建credential.js:

module.exports = {
    cookieSrcret:'jkafhkj'
}

在程序开始设置和访问cookie之前,需要引入中间件cookie-parser。

app.use(require('cookie-parser')(credentials.cookieSecret));
res.cookie('test','data');
res.cookie('test','data',{signed:true});

签名的cookie高于未签名的cookie,如果你将签名的cookie命名为aaa,那就不能在用这个名字再命名未签名cookie

服务器获取客户端cookie:req.cookies.aaa | req.signedCookies.signed_aaa

清除cookie:res.clearCookie('aaa')

设置cookie时选项详解:

  • domain
    控制跟cookie关联的域名,你可以将cookie分配给特定的子域名。注意:不能给cookie设置跟服务器所用域名不同的域名,因为那样他什么都不会做
  • path
    控制应用这个cookie的路径,注意:路径会隐含的通配其后的路径
  • maxAge
    指定客户端应该保存cookie多长时间,单位毫秒
  • secure
    只通过安全HTTPS连接发送
  • httpOnly
    设置为true表明这个cookie只能由服务器修改。有助于防范XSS攻击
  • signed
    设置为true会对这个cookie签名,那样就需要用res.signedCookies访问它。被篡改的签名cookie会被服务器拒绝,并且cookie值会重置为它的原始值。

会话

会话实际上只是更方便的状态维护方法。要实现会话,必须在客户端存些东西,否则服务器无法从一个请求到下一个请求中识别客户端。通常的做法使用一个包含唯一标识的cookie,然后服务器用这个标识获取相应的会话信息。

从广义上说,有两种实现会话的方法:

  1. 把所有东西都存在cookie中
    称为基于cookie的会话,不推荐使用
  2. 只在cookie中存一个唯一标识

内存存储

会话信息如何存储呢?入门级的选择是内存会话,他们非常容易设置,但是又巨大的缺陷:

  1. 重启服务器后,会话就消失了
  2. 扩展多条服务器后,每次请求可能由不同的服务器处理,会话数据就时有时无了

具体使用:安装express-session,并在cookie-parser之后链入。app.use(require('express-session')([option]))

中间件express-session接受带有如下选项的配置对象,常用的有:

  • key - 存放唯一会话标识的cookie名称
  • store - 会话存储实例,默认为一个MemoryStore的实例
  • cookie - 会话cookie的cookie设置

使用会话

会话设置好后,使用十分简单,直接使用请求对象的session变量的属性即可。

注意:对于会话而言,不同于cookie,都是在请求对象上操作的,响应对象是没有session属性的,删除会话,使用JavaScript的delete操作符。

用会话实现即显消息

即显消息只是在不破坏用户导航的前提下向用户提供反馈的一种办法。用回话实现即显消息是最简单的方式。

原理添加中间件:如果会话中有flash对象,就把它添加到上下文中,即显消息显示一次之后就需要从会话中删除,以免下次请求时继续显示,在路由之前添加如下代码:

app.use(function(req,res,next){
    res.locals.flash = req.session.flash;
    delete req.session.flash;
    next();
})

即便在前端做了输入验证,在后台也应该再做一次,因为恶意用户能够绕过前端验证。

会话用途

最常见的用途是提供用户验证信息。

尽管我建议优先选择会话而不是cookie,但是理解cookie的工作机制十分重要,特别是因为有cookie才能有会话。

中间件

从概念上讲,中间件是一种功能的封装方式,具体来说就是封装在程序中处理HTTP请求的功能。从实战讲,中间件知识有3个参数的函数:一个请求对象,一个响应对象和一个next函数。还有一种特别的4个参数的形式,用来做错误处理。

中间件是在管道中执行的。在Express中,通过调用app.use向管道中插入中间件。在Express4.0中,中间件和路由处理器是按他们的连入顺序调用的,顺序更清晰。

在管道的最后一个放一个”捕获一切“请求的处理是常见的做法,由它来处理跟前面其他所有路由都不匹配的请求。这个中间件一般会返回状态码404。

那么请求在管道中如何终止呢?这是由传递给每个中间件的next函数来实现的。如果不调用next(),请求就在那个中间件终止了。

学习如何灵活的考虑中间件和路由处理器是了解Express如何工作的关键。

  • 路由处理器(app.VERB)可以看作只处理特定HTTP谓词的中间件,中间件可以看作处理全部HTTP谓词的路由处理器(app.all)
  • 路由处理器的第一个参数必须是路径。如果你想让某个路由匹配所有路径,只需用/。中间件也可以将路径作为第一个参数,但是它是可选的,如果忽略参数,他会匹配所有路径,就像指定了/一样。
  • 路由处理器和中间件的参数都有回调函数,函数有2个、3个或4个参数。如果是2个或3个参数,头两个是请求和响应对象,第三个参数是next函数, 如果有4个参数,他就变成了错误处理中间件,第一个参数是错误对象,然后一次后推一个。
  • 如果不调用next(),管道就会被终止,也不会再有处理器或中间件做后续处理。如果不调用next(),则应该发送一个响应到客户端(res.send,res.json,res.render);如果不这样做,客户端会被挂起并最终导致超时。
  • 如果调用了next(),一般不宜再发送响应到客户端,如果你发送了,管道中后续的中间件或路由处理器还会执行,但它们发送的任何响应都会被忽略。

例子:

app.get('/b', function (req, res, next) {
    console.log('/b:路由未终止');
    next();
});

app.get('/b', function (req, res, next) {
    console.log('/b part2:抛出错误');
    throw new Error('b 失败');
});

// 传递错误得到500
app.use('/b', function (err, req, res, next) {
    console.log('/b 检测到错误并传递');
    next(err);
});

app.get('/c', function (res, req) {
    console.log('/c:抛出错误');
    throw new Error('c 失败');
});

// 不传递得到404
app.use('/c', function (err, req, res, next) {
    console.log('/c 检测到错误不传递');
    next();
});

app.use(function (err, req, res, next) {
    console.log('500 检测到未处理错误:' + err.message);
    res.send('500');
});

app.use(function (req, res) {
    console.log('未处理的路由 404');
    res.send('404');
});

app.listen(3000, function () {
    console.log('launch success');
});

注意:中间件必须是一个函数.

模块可以输出一个函数,而这个函数又可以直接用作中间件。不过更常见的做法是输出一个以中间件为属性的对象。

// 直接输出函数
modules.exports = function(req,res,next){

}
app.use(require('./lib.aaa.js'));
// 输出对象
modules.exports = {
    fun1: function(req,res,next){

    }
}
var object = require('./lib/aaa.js');
app.use(object.fun1);

注意:在开发时发现URL请求路由时,会发起两个请求,一个进入正常路由,一个总是进入404处理器,百思不得其解404处理器为何会响应。于是在路由前添加一个中间件,中间件作用很简单,就是打印请求的path属性,这才知道原因,是由于我学习中没有设置静态目录,没有放置favicon.ico图片,浏览器渲染时会自动请求favicon.ico文件,从而触发404错误,不要被浏览器窗口上Express提供的缺省图片欺骗了。

常用中间件

在Express 4.0之前,Express中捆绑了Connect,它包含了大部分常用的中间件。到Express 4.0,Connect从Express中移除了。一些Connect中间件也从Connect中分离出来,变成了独立的项目。唯一保留在Express中的中间件只剩下static了。从Express中剥离中间件可以让Express不用在维护那么多的依赖,并且这些独立的项目可以独立于Express而自行发展成熟。

大多数之前捆绑在Express中的中间件都十分集成,所以有必要知道"它去哪里了"以及如何得到他。

  • basicAuth
    • app.use(connect.basicAuth())
    • 基本授权访问 限HTTPS
  • body-parser
    • app.use(require(body-parser))
    • 连入json和urlencoded
  • json
    • body-parser的json部分
    • 解析json编码请求题
  • urlencoded
    • body-parser的urlencoded部分
    • 解析application/x-www-form-urlencoded请求体
  • multipart
    • 解析multipart/form-data请求体
    • 已废弃,使用Busbody或Formidable代替
  • compress
    • app.use(connect.compress)
    • 用gzip压缩响应数据
  • cookie-parser
    • app.use(require(cookie-parser))
    • 提供对cookie的支持
  • cookie-session
    • app.use(require(cookie-session()))
    • 提供cookie存储的会话支持,不推荐这种存储方式的会话
  • express-session
    • app.use(require(express-session()))
    • 提供会话ID的会话支持
  • csurf
    • app.use(require(csurf)())
    • 防范跨域请求伪造(CSRF)攻击,因为它需要会话,需要放在express-session中间件后面
  • directory
    • app.use(connect.directory())
    • 提供静态文件的目录清单支持
  • errorhandler
    • app.use(require(errorhandler)())
    • 为客户端提供栈追踪和错误信息,不要在生产环境中连入它,不安全
  • static-favicon
    • app.use(require(static-favicon)(path_to_favicon))
    • 提供favicon,不是必须,你可以简单的在static目录下放一个favicon.ico,但这个中间件可以提升性能。可以使用出favicon.ico之外的其他文件名
  • morgan
    • app.use(require(morgan))
    • 提供自动日志记录支持:所有的请求都会被记录
  • query
    • Express隐含连入,不需要手动连入
    • 解析查询字符串
  • static
    • app.use(express.static(path_to_static_files)())
    • 提供静态文件的支持

资料



留言