前言
本文是笔者所总结的有关 Nodejs Passport 系列之一;本文将从源码分析的角度,来深入剖析 passport 的认证流程;备注,此部分流程依赖于 Nodejs Passport 系列之二:Passport 源码剖析之类图及初始化流程剖析作为前置条件;
本文为作者原创作品,转载请注明出处;
Demo
此部分将继续使用 Nodejs Passport 系列之二:Passport 源码剖析之类图及初始化流程剖析中的 LocalStrategy 的 demo 来深入分析如何使用 passport 来对请求进行用户身份认证的流程;
源码分析之验证流程分析
Loacl Strategy 验证流程分析
Nodejs Passport 系列之二:Passport 源码剖析之类图及初始化流程剖析中,分别完成了对 Session Strategy 和 Local Strategy 的初始化操作,本篇文章不打算对 Session Strategy 的验证流程进行分析,而是直接通过 Local Strategy 来深入分析 passport 的认证流程;当 /login 请求到达,会触发 authenticate.js 模块所 export 出来的与 Local Strategy 相关的 authenticate(req, res, next) 方法句柄,该部分的分析参考 Nodejs Passport 系列之二:Passport 源码剖析之类图及初始化流程剖析的章节“注册 Local Strategy 验证流程”的最后一小节的内容;
下面是将 Local Strategy 的验证流程注册到 Authenticator 中的代码,
1 | passport.use(new LocalStrategy( |
下面这行代码就是笔者要重点分析的代码了,它就是验证流程调用的入口,1
app.post('/login', passport.authenticate('local', { successReturnToOrRedirect: '/', failureRedirect: '/login' }))
其调用的核心流程图如下,
首先,注意以下几点,
在调用 authenticate.js 模块的 authenticate(req, res, next) 方法的时候的闭包,
names
1
{'local'}
在配置的时候,可以定义 names 数组,这样针对同一个请求就可以支持多种验证方案既 Strategies;
- options
1
{ successReturnToOrRedirect: '/', failureRedirect: '/login' }
authenticate.js 模块中的 strategy 实例既是 Local Strategy 实例;不过要注意它的初始化过程,见流程图中的步骤 1.1.2 和 1.1.3;
然后,我们来看一些核心流程,核心流程均用红色的箭头标出,
- 1.1.3 $\to$ 1.1.8 这里定义了一系列与验证相关的模板方法,注意三个方法,strategy.success、strategy.fail 和 strategy.success;
注意 loop 循环,在源码中是通过一个立即执行函数传输一个递增变量 i 实现的,该循环会依次遍历 names,也就是 Strategy 的注册名,然后加载对应的 Strategy 对象进行验证,因此,
1
2
3
4
5app.post('/token',
passport.authenticate(['basic', 'oauth2-client-password'], { session: false }),
oauth2.token(),
oauth2.errorHandler()
);当遇到上述这样的初始化方式的时候便不用再奇怪了,就相当于对 /token 请求分别依次执行 Basic Strategy 和 OAuth2 Client Password 的验证;
1.1.9 正是开始执行 Local Strategy 实例的验证流程,通过调用该实例的 authenticate(req, options) 方法,要注意这里用户自定义的回调方法是如何被调用的逻辑,
1
2
3
4
5
6
7
8
9
10passport.use(new LocalStrategy(
(username, password, done) => {
User.findByUsername(username, (error, user) => {
if (error) return done(error);
if (!user) return done(null, false);
if (user.password !== password) return done(null, false);
return done(null, user);
});
}
));通过初始化 LocalStrategy 实例注入了用户自定义的回调函数 $\alpha$,一个匿名回调函数;在上一篇博文中,我们看到,实例化 LocalStrategy 的时候会将用户自定义的函数 $\alpha$ 赋值给实例属性 this._verify;下面来看看其调用过程,1.1.9.1.4,开始回调用户的验证方法 $\alpha$,与 Local Strategy 的相关源码如下,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15function verified(err, user, info) {
if (err) { return self.error(err); }
if (!user) { return self.fail(info); }
self.success(user, info);
}
try {
if (self._passReqToCallback) {
this._verify(req, username, password, verified);
} else {
this._verify(username, password, verified);
}
} catch (ex) {
return self.error(ex);
}可以看到回调过程分为两种情况,如果 self._passReqToCallback 为真,则会将 request 做为参数回调给 $\alpha$;开始回调,执行 this._verify 方法也就是 $\alpha$,所以 $\alpha$ 中的方法参数 done 就是这里的 verfied 方法;
1
2
3
4
5
6
7
8(username, password, done) => {
User.findByUsername(username, (error, user) => {
if (error) return done(error);
if (!user) return done(null, false);
if (user.password !== password) return done(null, false);
return done(null, user);
});
}然后分别对应了认证异常、失败和成功的处理逻辑,然后分别会回调 strategy.error、strategy.fail 和 strategy.success 等模板方法进行处理;这里要特别注意的是认证成功以后的回调方法,也就是 strategy.success 的方法的执行逻辑,会回调 request.js 模块中的 logIn 方法在 req 对象中添加属性 user,具体过程参考流程图 1.1.9.1.4.1.1.3.1.3;
- 若验证通过,则执行 next(),调用下一个 handler 的业务逻辑;
Session Strategy 验证流程分析
原本 Session Strategy 发生在 Local Strategy 验证之前,应该在之前分析;但是之前总结的时候有意将这块漏掉了,现在意识到它的重要性,于是又将它补回来,所以新开辟一个章节来专门描述,补于 2018-07-26;
首先,要知道,Session Strategy 是随着 require(‘passport’); 的时候就注入 Authenticator 对象中的,这部分参考 require 流程章节的分析,所以,Session Strategy 是默认随着 passport 组件的载入而初始化的,
其次,Session Strategy 的认证动作是通过下面这行代码注册到 Express 中的,
1
app.use(passport.session());
它将 Session Strategy 的验证方法 authenticate 注册,因此,将来每一次请求,都会使用该方法进行验证;
最后,要能够完整的了解 Session Strategy 的验证流程,还需要对 initialize 流程进行调用过程分析,此部分的注册流程参考注册 initialize 流程章节内容;相关代码如下,
1
app.use(passport.initialize());
综上,要能够完整的把握住 Session Strategy 的验证流程,我们需要将下面这些部分串联起来分析,1
2
3
4app.use(passport.initialize());
app.use(passport.session());
...
app.post('/login', passport.authenticate('local', { successReturnToOrRedirect: '/', failureRedirect: '/login' }))
Ok,现在假设,有一个新的请求到达,那么它的执行流程是怎样的呢?归纳起来,
- 执行 initialize 流程
- 执行 Session Strategy 的验证流程
- 执行 Local Strategy 的验证流程
下面,笔者就这三个流程分别依次进行分析,
执行 initialize 流程
执行的相关代码如下,正如注册 initialize 流程中所分析的那样,它调用的是 initialize.js 模块中的 initialize 方法,1
2
3
4
5
6
7
8
9
10
11
12module.exports = function initialize(passport) {
return function initialize(req, res, next) {
req._passport = {};
req._passport.instance = passport;
if (req.session && req.session[passport._key]) {
// load data from existing session
req._passport.session = req.session[passport._key];
}
next();
};
};
正如之前的分析那样,该初始化的调用非常重要,
- 初始化 req._passport 对象,
- 将 Authenticator 实例通过 passport 参数将其赋值给了 req._passport.instance,
- 将该 Authenticator 实例所对应的 session 对象赋值给 req._passport.session;
这一步非常的关键,因为在 passport 的源码中,绝大多数地方,在引用相关的 session 的时候,都是直接使用的 req._passport.session 的方式引用的,笔者之前一直疑惑,怎么会从 req._passport 获得 session 对象,Express Session 框架命名告诉我们,session 是存放在 req.session 中的呀?这也解释了,为什么 req._passport.session 中的值会随着 session 值的失效而失效,因此,在 Session Strategy 验证的时候,虽然之前用户登录成功过,但是如果,超时既 Session 失效,那么 Session Strategy 验证失败;所以,这里需要牢记的是,req._passport.session 存储的就是与该 Authenticator 实例相关的 session 对象;
备注:passport._key 的值为 “passport”
执行 Session Strategy 的验证流程
归纳起来,其实就是从 session 中判断是否有 userid,若有,则将该 userid 作为参数调用用户自定义的 deserialize user 方法从数据源中找到是否有该 user 并返回,
- 若有,则将该 user 赋值给当前的 req 中,备注,这是验证成功的标志,request.js 模块中的 isAuthenticated 方法就是判断的该属性来判断用户是否已经认证的;
- 若无,则将该 userid 从 session 中删除!
在调用 authenticate.js 模板方法 authenticate 进行验证的时候,会通过 strategy name ‘session’ 首先匹配到 Session Strategy,然后调用它的 authenticate 方法进行验证,下面来看一下它的源码,
strategies/session.js
1 | SessionStrategy.prototype.authenticate = function(req, options) { |
说实话,第一眼看到上面的代码,我是一脸懵逼的,一堆问题;上面最核心的代码在 17 行,但是匿名函数 function 的 user 怎么来的?它是怎么样从用户自定义的 deserializeUser 中传递过来的?抱着种种疑问,决定,重头分析,
首先,我们来看看用户自定义的回调方法,
app.js1
2
3
4
5
6
7
8
9
10
11
12passport.deserializeUser((id, done) => {
db.users.findById(id, (error, user) => done(error, user));
});
...
User.findById = (id, callback) => {
for (let i = 0, len = users.length; i < len; i++) {
if (users[i].id === id) return callback(null, users[i]);
}
return callback(null, null);
};先不对上述做过多的分析,只需要知道,passport.deserializeUser 接收一个回调函数 done,然后再将该回调函数 done 封装成一个新的匿名函数 callback 作为参数调用 User.findById,无论成功或者失败都会回调该 callback 函数,然后执行 done 函数;不过为了描述方便,我们将这里用来 deserialize user 的 lambda 方法命名为 $\omega$,
1
2
3(id, done) => {
db.users.findById(id, (error, user) => done(error, user));
}其次,将 $\omega$ 注入到 Authenticator._deserializeUsers 队列中,看下面的代码片段,
1
2
3
4
5
6
7
8
9Authenticator.prototype.deserializeUser = function(fn, req, done) {
if (typeof fn === 'function') {
return this._deserializers.push(fn);
}
// private implementation that traverses the chain of deserializers,
// attempting to deserialize a user
var obj = fn;
...如果第一个参数 fn 是 function,那么将 fn 添加到 this._deserializers 中,正好满足 #1 的情况,将 $\omega$ 添加到了 this._deserializers 中;好了,下面的问题就是,$\omega$ 在什么时候被调用的呢?下面,我将来回答这个问题;
再次,调用过程,不正好就是调用 Session Strategy 的验证流程嘛,就是本章节开头部分,笔者所提到的部分;回到本小节一开头的部分 strategies/session.js 的代码中,它正是通过下面的代码的 this._deserializeUser 方法进行调用的,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23SessionStrategy.prototype.authenticate = function(req, options) {
...
if (req._passport.session) {
su = req._passport.session.user;
}
if (su || su === 0) {
this._deserializeUser(su, req, function(err, user) {
if (err) { return self.error(err); }
if (!user) {
delete req._passport.session.user;
} else {
// TODO: Remove instance access
var property = req._passport.instance._userProperty || 'user';
req[property] = user;
}
self.pass();
if (paused) {
paused.resume();
}
...
}
}如果 Authenticator session 中存在验证成功的且没有失效的 user,那么执行 this._deserializeUser 方法,那么 this._deserializeUser 方法从何而来的呢?这就要温习一下 SessionStrategy 的构造过程了,
1
2
3
4
5Authenticator.prototype.init = function() {
this.framework(require('./framework/connect')());
this.use(new SessionStrategy(this.deserializeUser.bind(this)));
this._sm = new SessionManager({ key: this._key }, this.serializeUser.bind(this));
};上述代码第 3 行,在构造 SessionStrategy 对象的时候,将 Authenticator.deserializeUser 作为构造函数传递进去了,并且在该构造函数中,SessionStrategy 将 Authenticator.deserializeUser 赋值给了 SessionStrategy._deserializeUser;Ok,所以,我们知道了,在 SessionStrategy 对象的 authenticate 方法中所调用的 _deserializeUser 方法实际上仍然是 Authenticator.deserializeUser 方法,和将 $\omega$ 注入到 Authenticator._deserializeUsers 队列中的方法是同一个方法;
最后,SessionStrategy autenticate 方法调用 Authenticator.deserializeUser
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49Authenticator.prototype.deserializeUser = function(fn, req, done) {
if (typeof fn === 'function') {
return this._deserializers.push(fn);
}
// private implementation that traverses the chain of deserializers,
// attempting to deserialize a user
var obj = fn;
// For backwards compatibility
if (typeof req === 'function') {
done = req;
req = undefined;
}
var stack = this._deserializers;
(function pass(i, err, user) {
// deserializers use 'pass' as an error to skip processing
if ('pass' === err) {
err = undefined;
}
// an error or deserialized user was obtained, done
if (err || user) { return done(err, user); }
// a valid user existed when establishing the session, but that user has
// since been removed
if (user === null || user === false) { return done(null, false); }
var layer = stack[i];
if (!layer) {
return done(new Error('Failed to deserialize user out of session'));
}
function deserialized(e, u) {
pass(i + 1, e, u);
}
try {
var arity = layer.length;
if (arity == 3) {
layer(req, obj, deserialized);
} else {
layer(obj, deserialized);
}
} catch(e) {
return done(e);
}
})(0);
};索性,将作者的源码全部贴了出来,要真的是逐字逐句的去推敲上面的代码,一定会让人疯掉的,不过作者的注释里面阐述得非常的清楚,就是逐个遍历之前用户所注册的 deserializer 方法比如之前我们所注册的 $\omega$,从 this._deserializers 中遍历出来,在这里执行,执行用户的回调方法 $\omega$,执行的上下文环境是,三个参数,
1
function(fn, req, done)
也就是由 #3 中 Session Strategy 的 authenticate 方法内部的调用过程中所传递过来的三个参数,相关代码摘要如下,
1
2
3
4
5
6
7
8
9
10
11
12
13
14this._deserializeUser(su, req, function(err, user) {
if (err) { return self.error(err); }
if (!user) {
delete req._passport.session.user;
} else {
// TODO: Remove instance access
var property = req._passport.instance._userProperty || 'user';
req[property] = user;
}
self.pass();
if (paused) {
paused.resume();
}
});可以看到,执行过程中,
this._deserializeUser 将 su, req 和 function(err, user){} 分别赋值给了 Authenticator#deserializeUser 的三个参数 fn, req 和 done;这里要特别特别注意的是,也是为什么源码不好动的地方就是,因为言不达意,这个时候,这里的参数 fn 代表的是 su,su 是什么,su = req._passport.session.user,就是 userid 呀,接着,方法内部将该 userid 赋值给了局部变量 obj;
1
var obj = fn;
而在 Authenticator#deserializeUser 的执行过程中,Authenticator#deserializeUser 又将 obj (就是 userid) 和 done 参数分别赋值给了上述 $\omega$ 方法中的参数 id 和 done;对应相关代码如下,
1
2
3
4
5if (arity == 3) {
layer(req, obj, deserialized);
} else {
layer(obj, deserialized);
}如果 $\omega$ 需要三个参数,则回调 layer(req, obj, deserialized) 方法,如果需要两个参数,则回调 layer(obj, deserialized) 方法;注,这里的 layer 就是从 this._deserializers 中 push 出来的用户自定义的 deserializeUser 方法既 $\omega$;这里得到一个不小的
收获
,那就是用户在定义 deserializeUser 回调方法的时候,还可以使用第三个参数,那就是 req,有些时候,如果需要从 req 或者 session 中读取默写信息来序列化 user 的情况下,将会非常有用;所以,我们得到一个很重要的结论,那就是,$\omega$,1
2
3passport.deserializeUser((id, done) => {
db.users.findById(id, (error, user) => done(error, user));
});中的回调函数 done 就是由 Session Strategy 中 authenticate 方法中所定义的回调函数,为了描述方便,我将其命名为 $\alpha$
1
2
3
4
5
6
7
8
9
10
11
12
13
14function(err, user) {
if (err) { return self.error(err); }
if (!user) {
delete req._passport.session.user;
} else {
// TODO: Remove instance access
var property = req._passport.instance._userProperty || 'user';
req[property] = user;
}
self.pass();
if (paused) {
paused.resume();
}
}因此,当用户的 deserialize user 的方法返回以后,会通过 $\alpha$ 的回调方法引用 done 被调用,可以看到,
- 如果没有用户返回,那么直接将用户从 session 中删除;
如果由用户返回,那么将用户赋值到 req 中,表示验证成功,可以看到是赋值给了 req.user;
注意,这里告诉了我们为什么在启用 Session 的情况下 passport 认为只要 req 中存在 user 对象既表示认证成功的根本原因!
回过头来,我们再来看看 request.js 模块是如何判断当前用户在当前请求是否是认证过的?1
2
3
4
5
6
7
8req.isAuthenticated = function() {
var property = 'user';
if (this._passport && this._passport.instance) {
property = this._passport.instance._userProperty || 'user';
}
return (this[property]) ? true : false;
};怎么样,就是判断当前的 req 中是否有 this._passport.instance._userProperty 或者 ‘user’ 的属性值;
综上,这里完成了一个重要的逻辑转换,既是,从 session userid $\to$ deserialize user $\to$ req.user,通过 Session Strategy 实际上完成了从 session 中获取用户凭证既 userid,然后使用 userid 通过 deserialize user 方法从数据源中获取真实的 user,最后填充到当前的 req 对象的 user 属性,完成 Session Strategy 的验证过程,注意,这里验证成功的标志是,当前的 req 中有 user 属性,而且该属性只在当前请求有效,既是 request scope 而不是 session scope;
总结
笔者将上面的步骤总结如下,
初始化过程,该过程既是将用户自定义的 deserialize user 方法 $\omega$ 注册到 Authenticator._deserializers 中;入口方法,
1
2
3passport.deserializeUser((id, done) => {
db.users.findById(id, (error, user) => done(error, user));
});调用过程,通过 Session Strategy 对象的 authenticate 方法进行调用,过程中,authenticate 方法执行过程中将 userid,req 和 done 分别作为参数,调用 Authenticator.prototype.deserializeUser 方法,
Authenticator.prototype.deserializeUser 方法执行过程中,从 Authenticator._deserializers 依次 push 出 $\omega$,然后将 Session Strategy 传递过来的参数 userid,req 和 done 作为 $\omega$ 的调用参数,因此
1
2
3passport.deserializeUser((id, done) => {
db.users.findById(id, (error, user) => done(error, user));
});中的 id $\to$ userid,done $\to$ Session Strategy 中所定义的 done,详情参考上面的第 4 点分析;
- 如果用户的 deserialize user 方法执行成功,则回调 done 方法,将 user 直接赋值给 req 对象( req.user ),若失败,则将 user 从 session 中移除;再来看一下这个关键的 done 方法,
1
2
3
4
5
6
7
8
9
10
11
12
13
14function(err, user) {
if (err) { return self.error(err); }
if (!user) {
delete req._passport.session.user;
} else {
// TODO: Remove instance access
var property = req._passport.instance._userProperty || 'user';
req[property] = user;
}
self.pass();
if (paused) {
paused.resume();
}
}
不过,笔者一直没有搞明白的,为什么非要把注册 deserialize user 的相关逻辑和执行 deserialize user 相关逻辑的用同一个 Authenticator#deserializeUser 方法,炫技?减少代码的行数?直接将1
2
3passport.deserializeUser((id, done) => {
db.users.findById(id, (error, user) => done(error, user));
});
改成1
2
3passport.addDeserializer((id, done) => {
db.users.findById(id, (error, user) => done(error, user));
});
不是更好,而且在方法的定义和调用上,至少命名上就不会有太多的歧义了;笔者感慨啊,一门过于灵活的语言,如果不在编程规范上做好定义,那代码真的是相当难度的… 至少这里就是这样的情况!
执行 Local Strategy 的验证流程
与 Session Strategy 的认证流程相关的地方,就是当 Local Strategy 验证用户成功以后,回调 Session Manager 的 logIn() 方法,这部分参考流程图中的步骤: 1.1.9.1.4.1.1.3.1.3.2 如果启用了 session,还会调用 SessionManager.logIn() 方法;该步骤中主要涉及到了 serialize user to session 的过程;由于 serialize user 调用的逻辑和 deserialize user 的逻辑大同小异,所以笔者这里不再对 serialize user 的流程做过多细节上的分析了;
用户在 app.js 中所定义的 serialize user 接口方法,
1
passport.serializeUser((user, done) => done(null, user.id));
为了描述方便,这里将用户自定义的 lambda 回调函数命名为 $\omega$;该行代码其实就是通过 passport.serializeUser 方法将 $\omega$ 添加到 Authenticator#_serializers 队列中;这里的 done 就是 SessionManager#logIn 方法中所定义的匿名回调函数,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17SessionManager.prototype.logIn = function(req, user, cb) {
var self = this;
this._serializeUser(user, req, function(err, obj) {
if (err) {
return cb(err);
}
if (!req._passport.session) {
req._passport.session = {};
}
req._passport.session.user = obj;
if (!req.session) {
req.session = {};
}
req.session[self._key] = req._passport.session;
cb();
});
}上述代码第 3 行所定义的匿名 function(err, obj),为了描述方便,将这个 done 回调函数命名为 $\alpha$
- 当 Local Strategy 认证成功以后,如果启动了 Session,那么会通过 requests.js#logIn 方法回调 SessionManager#logIn 方法进而触发 Authenticator#_serializeUser 方法,注意,这里调用它的参数为,user、req、$\alpha$;
执行 Authenticator#_serializeUser 方法,从 Authenticator#_serializers 中 push 出用户定义的回调方法 $\omega$,因为 $\omega$ 只有两个参数,因此将 user 和 $\alpha$ 作为调用参数调用 $\omega$,对应 Authenticator#_serializeUser 方法中的如下代码,
1
2
3
4
5
6
7
8
9Authenticator.prototype.serializeUser = function(fn, req, done) {
...
if (arity == 3) {
layer(req, user, serialized);
} else {
layer(user, serialized);
}
...
}其中 layer 就是 $\omega$,由此调用 $\omega$;因此可知,调用过程中 $\omega$ 的参数 user 既是由 Local Strategy 一层一层传递进来的已经认证成功以后的 user 对象,而回调函数 done 则是 $\alpha$,因此,再回过头来看看刚才用户所定义的回调函数 $\omega$ 的逻辑就非常的清晰了,
1
passport.serializeUser((user, done) => done(null, user.id));
serialize user 的执行过程中,$\omega$ 以参数 error=null, user=user.id 通过 done 回调 $\alpha$,再看看看 $\alpha$ 的代码,
1
2
3
4
5
6
7
8
9
10
11
12
13
14function(err, obj) {
if (err) {
return cb(err);
}
if (!req._passport.session) {
req._passport.session = {};
}
req._passport.session.user = obj;
if (!req.session) {
req.session = {};
}
req.session[self._key] = req._passport.session;
cb();
}obj $\to$ user.id,也就是说,当 Local Strategy 认证成功以后,通过 serialize user 的回调过程,将 user.id 以参数 user 的形式赋值给了当前的 passport session 中,见上述代码第 8 行,既向 session 中填充已经认证的用户信息;
总结
综合起来看,其实很简单,该流程就是当启用了 session 的情况下,当某个 Strategy (除开 Session Strategy)认证成功以后,将 user 信息填充至 session 中,Passport 并没有限制你怎么去填充,你可以在 $\omega$ 中填充完整的 user 对象,但是,Passport 推荐只需要填充 user 的一个唯一标识就可以了,比如 user.id、user.name、user.email 等等;
写在最后
Node.js 因为其语言及其灵活,所以,虽然它的编程风格更像是面向过程的开发方式,但是其逻辑非常的简单明了,一气呵成,以至于它能够在 authenticate.js 模块中的 authenticate 方法中不仅定义了与验证流程相关的所有模板方法而且抽象了验证的流程;