0

首先,一个很好的项目例子:express-4.x-local-example

1

如果用到了session来存储登陆的信息的话(默认是用了的),必须实现passport.serializeUserpassport.deserializeUser两个函数,分别用于从user对象中提取一个唯一能代表该user的字符串(从而生成session key),以及通过之前提取的字符串反过来得到user对象。

2

passport.deserializeUser会在passport.session()返回的中间件中被调用,用来产生req.user属性。即,每次有请求进来,只要经过passport.session()中间件就会调用passport.deserializeUser,所以渲染一个页面可能会调用多次哦。这个函数的优化也就比较重要了,如果每次都要去数据库中查找用户信息不光效率低,数据库的压力也比较大,一种优化方法是把最近用到的一些加载到内存中(或者直接使用in-memory database,如果整个数据量不是特别大的话)。

3

passport.js对于登陆成功或失败提供了选项来redirect页面到不同的地方:

app.post(
  '/login',
  passport.authenticate('local', {successRedirect: '/', failureRedirect: '/login', failureFlash: true}),
);

以上,登陆成功会跳转到根路径,而失败则仍跳转到/login页面。如果你希望登陆失败不要去刷新页面(redirect,即302状态码必定会导致浏览器刷新整个页面),那么,首先需要去掉passport.js中上面的设置选项,即让后端不要返回302的response;其次,前端代码中不要用默认的提交表单的方式发送请求,像下面这样:

<form action="/login" method="post">
	<div>
	<label>Username:</label>
	<input type="text" name="username"/><br/>
	</div>
	<div>
	<label>Password:</label>
	<input type="password" name="password"/>
	</div>
	<div>
	<input type="submit" value="Submit"/>
	</div>
</form>

Submitting a form loads an entirely new page, which usually ends the current execution context for the script.

The exception is if you submit the form to a frame, in which case an onload event will fire.

解决方法是,自己实现一个”submit”的事件函数并自己发送请求(注意要event.preventDefault),当返回登陆失败的信息时重新渲染部分页面即可。

4

form的submit事件接口本身提供了一个机制,当事件函数返回false时则不提交表单请求,这个机制主要用于表单的validation。所以,阻止表单提交请求可以这样:

<form action="/login" method="post" onsubmit="return false;">
</form>

也可以这样:

<form action="/login" method="post" onsubmit="event.preventDefault()">
</form>

5

默认session key在客户端存储在名为connect.sid的cookie中,服务端会根据该值来计算并验证登陆信息,具体的流程为:

若本次cookie中没有connect.sid,则生成一个 [用secret生成connect.sid]
  1. uid-safe生成一个唯一id,记为sessionid,保证每次不重复;
  2. 把上面的connect.sid制作成 's:' + sessionid + '.' + sessionid.sha256(secret).base64() 的形式,实现在node-cookie-signature的sign函数;
  3. 把sessionid用set-cookie返回给前端;
若本次cookie中包含connect.sid,则验证它是否是本服务器生成的 [用secret验证connect.sid]
  1. 取出cookie中的connect.sid,形式是上面的 's:' + sessionid + '.' + sessionid.sha256(secret).base64()
  2. 从connect.sid中截取出sessionid=connect.sid.slice(2, connect.sid.indexOf(’.’));
  3. 用取出的sessionid再算一次 sessionid.sha256(secret).base64() 记为 mac;
  4. 截取connect.sid中’.’后的部分与mac对比;node-cookie-signature的unsign函数(用上次计算的sha256值和这次计算的sha256值进行比较,只要secret一样,结果就一样);
  5. 验证成功的sessionid继续往下走。
总结

用secret进行签名保证存在cookie中的connect.sid是本服务器上次生成的。除非知道secret,不然没办法伪造connect.sid中的sessionid,避免知道了sessionid生成算法的人(uid-safe)使用sessionid随便试探来攻击网站。

6

对应到express中,express-session这个中间件就是用来将cookie中session相关的东西提取出来,并放在req对象上新创建的session属性中。因此,如果passport.js中用到了session来存储登陆的信息的话,就必须加载express-session中间件,否则你会发现req.sessionundefined的。

7

express中中间件加载的顺序很重要,先加载的中间件会被先调用执行(见Writing middleware for use in Express apps)。因此,如果某个中间件要依赖另一个中间件,则必须在它之后再加载。比如express-session要用到cookie-parser解析过的req.cookies属性,那么加载时就应该这样:

app.use(require('cookie-parser')());
app.use(require('express-session')());

并且,通常会把所有全局应用的中间件加载的代码放到最前面执行,然后再绑定route对应的处理代码,否则之前绑定的route是不会被之后加载的中间件处理的。比如下面就是一个错误的例子:

app.get('/', function (req, res) {
  res.send('Hello World!')
})
app.use(require('cookie-parser')());

可以这样理解:当一个请求进入时,会按照中间件加载时的顺序“走”一遍所有起作用的中间件(route其实也是中间件,只不过只对某个route起作用),直到走到某个中间件,它告诉请求说你不用再往下走了,直接返回(结果)吧。就和搭积木一样,一定要把“结束单元”搭到最下面,否则“结束单元”下面的单元是不起作用的。

8

Debug到express的源码中也能看到,每一个请求过来时都会从一个列表中每一层处理一遍,直到某一层结束处理了再返回一个response,这里的每一层其实都是一个中间件。

每一个中间件作为一个Layer放在stack中

每一层会调用它的handle_request函数,查看它的源码,可以看到函数签名和我们的中间件是一样的。事实上,this.handle是构建Layer时传递进去的函数,也就是中间件函数。

Layer.prototype.handle_request = function handle(req, res, next) {
  var fn = this.handle;

  if (fn.length > 3) {
    // not a standard request handler
    return next();
  }

  try {
    fn(req, res, next);
  } catch (err) {
    next(err);
  }
};

9

Node.js中环境变量存储在process.env这个特殊的对象中,很多项目会用process.env.NODE_ENV来判断当前代码运行环境是开发环境(development)还是实际生产环境(production),比如在开发环境中可以将webpack中的sourcemap打开,便于debug。而有一些项目则会使用__DEV__这个变量来表示是否为开发环境,这个变量其实并不是什么特殊的变量,通常是通过Webpack的DefinePlugin来定义的全局变量(更恰当地说是定义了一个别名,Webpack在打包时会直接替换为某个值),比如默认情况下设为true,而在启动脚本后面添加上--release再把这个变量设为false。