前言
本文是笔者所总结的有关 Nodejs Passport 系列之一,本篇文章主要是对其基本的构建原理进行梳理;
本文为作者原创作品,转载请注明出处;
概述
Passport 构建得及其的简单,它被设计为 Nodejs 的中间件;具体的使用过程是将 passport 作为中间件嵌入到某 Express 的请求之前或者之后,用来验证用户的身份;而针对不同的验证方式,Passport 通过提供不同的验证策略(Strategies),通过这些不同的 Strategies 来提供不同的验证逻辑和方式,这些 Strategies 在 Passport 中被定义成不同的模块通过依赖包的形式进行载入,并作为 Passport 的中间件,在需要的时候注入;常用的 Strategies 有,
-
Local Strategy
用来验证最传统的使用用户名和密码来进行登录的方式;使用下面的方式导入 Local Strategy 的依赖包,1
$ npm install passport-local
具体使用方式,笔者会在后续内容做做简单的介绍;
- Session Strategy
基于 Session 的验证方式; - Basic Strategy
RFC 2617 定义了 Http Basic 的验证方式,这种方式将 username 和 password 写入 Header 中的字段 Authentication 进行验证,具体使用方式参考 Documentation: Basic & Digest - Digest Strategy
具体方式参考 Documentation: Basic & Digest - OpenID Strategy
Documentation: OpenID - Bear Strategy
用于验证 OAuth 2 请求中的 access token 的协议;Documentation: OAuth 2.0
核心组件
上图以 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 初始化
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
Local Strategy
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 的中间件执行的呢?看下一小节,
Verify Callback
用户自定义的回调方法中,要能够通过一种有效的方式通知 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 中,不过要注意两种场景,
- 启用 session
如果启用了 session,已认证的用户的后续请求将可以直接通过 req.user 获取用户的身份信息,并且不再需要再次认证了;不过这得益于 session 的用户序列化的机制,参考 session 章节; - 禁用 session
这种情况下,req.user 只在当前已认证的请求中有效,下次请求仍然需要再次认证,req.user 需要再次被填充;也就是每次认证通过以后进行填充;
Sessions
session
默认情况下,当用户身份认证成功以后,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
同样,我们可以显式的为某个请求指定不使用 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 )可以访问到哪些资源;这部分需要其它的解决方案了!