express.js/passport.js相关
0
首先,一个很好的项目例子:express-4.x-local-example。
1
如果用到了session来存储登陆的信息的话(默认是用了的),必须实现passport.serializeUser
和passport.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]
- 用uid-safe生成一个唯一id,记为sessionid,保证每次不重复;
- 把上面的connect.sid制作成
's:' + sessionid + '.' + sessionid.sha256(secret).base64()
的形式,实现在node-cookie-signature的sign函数;- 把sessionid用set-cookie返回给前端;
若本次cookie中包含connect.sid,则验证它是否是本服务器生成的 [用secret验证connect.sid]
- 取出cookie中的connect.sid,形式是上面的
's:' + sessionid + '.' + sessionid.sha256(secret).base64()
;- 从connect.sid中截取出sessionid=connect.sid.slice(2, connect.sid.indexOf(’.’));
- 用取出的sessionid再算一次 sessionid.sha256(secret).base64() 记为 mac;
- 截取connect.sid中’.’后的部分与mac对比;node-cookie-signature的unsign函数(用上次计算的sha256值和这次计算的sha256值进行比较,只要secret一样,结果就一样);
- 验证成功的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.session
是undefined
的。
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,这里的每一层其实都是一个中间件。
每一层会调用它的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。
Comments