本文是笔者所总结的有关 Nodejs Passport 系列之一,本篇文章主要是对其基本的构建原理进行梳理;
本文为作者原创作品,转载请注明出处;
Passport 构建得及其的简单,它被设计为 Nodejs 的中间件;具体的使用过程是将 passport 作为中间件嵌入到某 Express 的请求之前或者之后,用来验证用户的身份;而针对不同的验证方式,Passport 通过提供不同的验证策略(Strategies),通过这些不同的 Strategies 来提供不同的验证逻辑和方式,这些 Strategies 在 Passport 中被定义成不同的模块通过依赖包的形式进行载入,并作为 Passport 的中间件,在需要的时候注入;常用的 Strategies 有,
Local Strategy
用来验证最传统的使用用户名和密码来进行登录的方式;使用下面的方式导入 Local Strategy 的依赖包,
1 | $ npm install passport-local |
具体使用方式,笔者会在后续内容做做简单的介绍;
上图以 Local Strategy 和 Basic Strategy 为例,描绘了 passport 所相关的核心组件;可以看到,passport 最主要的就是两大模块,一个是 passport 自身,另外一个就是 Strategy 策略;
首先,要将需要使用到的 Strategy 注册到 passport 对象中,这一步的关键是,提供回调方法接口给用户,使得用户可以自定义扩展的能力;那么用户如何实现扩展呢?看下面一个简单的例子,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | var passport = require('passport') , LocalStrategy = require('passport-local').Strategy; passport.use(new LocalStrategy( function(username, password, done) { User.findOne({ username: username }, function (err, user) { if (err) { return done(err); } if (!user) { return done(null, false, { message: 'Incorrect username.' }); } if (!user.validPassword(password)) { return done(null, false, { message: 'Incorrect password.' }); } return done(null, user); }); } )); |
首先,加载 passport-local 模块所暴露的 Strategy 对象,
然后,通过 passport.use 方法引入一个用户自定义的 LocalStrategy 实例,通过用户所实现的回调方法来实现自定义,在该回调方法中,代码第 6 到 15 行,通过用户自定义的 User.findOne 方法来根据 username 进行查找;
passport 对象通过调用方法 authenticate(identifier, callback) 来对用户身份进行认证;来看一个最简单的例子,
1 2 3 | app.post('/login', passport.authenticate('local', { successRedirect: '/', failureRedirect: '/login' })); |
可以看到,passport.authenticate 方法中的 identifier 是一个字符串 'local',该字符串对应的就是 Local Strategy 实例,表示通过 Local Strategy 来对用户的身份进行认证;如果成功则返回首页,不成功则跳转回 login 页面;
Passport 作为 Express 的中间件被 Express 所使用,那么该中间件该如何初始化并注入到 Express 实例中呢?下面是使用 Express 4.x 的情况,
1 2 3 4 5 6 7 8 9 | var passport = require('passport'), session = require("express-session"), bodyParser = require("body-parser"); app.use(express.static("public")); app.use(session({ secret: "cats" })); app.use(bodyParser.urlencoded({ extended: false })); app.use(passport.initialize()); app.use(passport.session()); |
可见,通过 app.use 方法将 passport 实例注入到 Express 实例 app 中,要注意的是,
Strategy 是 passport 的一个核心模块,用来实现不同的验证方式,本小节笔者主要描述如何配置和使用 Local Strategy;
首先安装 Local Strategy 模块包,
1 | $ npm install passport-local |
通过 passport.use 方法引入用户自定义的 Local Strategy 模块,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | var passport = require('passport') , LocalStrategy = require('passport-local').Strategy; passport.use(new LocalStrategy( function(username, password, done) { User.findOne({ username: username }, function(err, user) { if (err) { return done(err); } if (!user) { return done(null, false, { message: 'Incorrect username.' }); } if (!user.validPassword(password)) { return done(null, false, { message: 'Incorrect password.' }); } return done(null, user); }); } )); |
通过用户自定义的回调方法来初始化 LocalStrategy 实例,后续的认证过程中,将会使用用户自定逻辑来查找用户是否存在;注意,默认情况下,使用 'username' 来进行查找,如果想要指定成其它的字段,使用如下的方式,
1 2 3 4 5 6 7 8 | passport.use(new LocalStrategy({ usernameField: 'email', passwordField: 'passwd' }, function(username, password, done) { // ... } )); |
这样,我们就配置好了我们本地的 passport 实例,只是该实例只支持通过 Local Strategy 对用户的身份进行认证;那么,该 passport 实例是如何作为 Express 的中间件执行的呢?看下一小节,
用户自定义的回调方法中,要能够通过一种有效的方式通知 passport,让它知道用户的认证是成功还是失败了,这里是通过 verify callback 方法 done() 来实现的,
如果用户认证成功,通过如下的方式通知 passport 用户已经认证成功了,
1 | return done(null, user); |
如果认证失败,通过如下的方式通知 passport 用户认证失败了,
1 | return done(null, false); |
如果需要明确失败的原因,
1 | return done(null, false, { message: 'Incorrect password.' }); |
通过上一章节配置好了 passport 实例以后,那么我们该如何将该使用了 Local Strategy 的 passport 实例应用到 Express 的实例上呢?很简单,
1 2 3 4 5 | app.post('/login', passport.authenticate('local', { successRedirect: '/', failureRedirect: '/login', failureFlash: true }) ); |
将 passport.authenticate 方法作为 Express app 实例的 /login router 的 handler 就好了,并且定义了相应的验证成功或者失败的 redirect 规则;要注意的是,如果认证成功,用户 user 将会被保存在对象 req.user 中,不过要注意两种场景,
默认情况下,当用户身份认证成功以后,passport 会开启一个 session 来维持登录用户的身份状态;session 通过一个 Session ID 保存在服务器端和用户的浏览器端,这样来维持该用户的登录的身份状态,那么现在的问题是,passport 是如何维持该用户的身份状态的呢?在服务器端,为了尽量减少内存的占用,在 session 中只会保存用户的 ID,当已认证的用户再次发起某个请求以后,当服务器端调用 req.user 的时候,会通过 passport.deserializeUser() 根据用户的 ID 加载 user,这样既可做到内存占用的最小化以及需要的时候,才会从数据库中加载用户的信息;为了然上述的方式得以生效,我们需要为 user 添加有关序列化的配置代码,
向 session 中写入 user ID,对应的是 serialize 流程,
1 2 3 | passport.serializeUser(function(user, done) { done(null, user.id); }); |
向 session 中载入 user,对应的是 deserialize 流程,
1 2 3 4 5 | passport.deserializeUser(function(id, done) { User.findById(id, function(err, user) { done(err, user); }); }); |
备注,如果禁用了 session,在 authenticate 方法验证成功以后,user 是不是也是通过上述的方式写入 req.user 的?需要验证一下…
同样,我们可以显式的为某个请求指定不使用 session,比如,
1 2 3 4 5 | app.get('/api/users/me', passport.authenticate('basic', { session: false }), function(req, res) { res.json({ id: req.user.id, username: req.user.username }); }); |
设置 passport.authenticate 方法的第二个参数设置为 session = false 即可;注意,这样设置以后,便不会再有 session 的特性了,那么每次新的请求都需要对用户的身份进行重新验证,并加载用户(用户的身份信息将会被保存到 req.user 实例中);
遗憾的是,passport 只负责对用户进行认证,它并不会进行权限的控制,也就是说,哪些权限( Role )可以访问到哪些资源;这部分需要其它的解决方案了!