Contents
  1. 1. 前言
  2. 2. demo
    1. 2.1. 构建项目骨架
    2. 2.2. 测试代码
    3. 2.3. 测试代码下载
  3. 3. 源码分析
    1. 3.1. passport 核心组件
    2. 3.2. 初始化流程分析
      1. 3.2.1. Authenticator 初始化流程
        1. 3.2.1.1. Authenticator new 初始化流程
        2. 3.2.1.2. Authenticator initialize 初始化流程
    3. 3.3. 注册 Session 和 Session Strategy 认证流程
    4. 3.4. 注册其它 Strategy 验证流程

前言

本文是笔者所总结的有关 Nodejs Passport 系列之一;本文将从源码分析的角度,来深入剖析 passport 的初始化流程;

本文为作者原创作品,转载请注明出处;

demo

笔者将使用这个 demo 来对 Local Strategy 的源码进行剖析;

构建项目骨架

  1. 首先构建 express 项目骨架,这部分内容参考 Nodejs Express 系列之二:Express 骨架 Skeleton 中小节“如何使用 Express Application Generator” 构建项目骨架;
  2. 导入 passport 模块

    1
    $ npm install passport
  3. 导入 Local Strategy 模块

    1
    $ npm install passport-local
  4. 导入 express-session 模块

    1
    $ npm install express-session
  5. 将工程导入你所熟悉的开发工具中,这里笔者使用的是 IntellJ IDEA;

  6. 构建完成以后,我们得到的 package.json 文件的内容信息如下,
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    {
    "name": "passport-local-demo",
    "version": "0.0.0",
    "private": true,
    "scripts": {
    "start": "node ./bin/www"
    },
    "dependencies": {
    "body-parser": "~1.18.2",
    "cookie-parser": "~1.4.3",
    "debug": "~2.6.9",
    "ejs": "~2.5.7",
    "express": "~4.15.5",
    "express-session": "^1.15.6",
    "morgan": "~1.9.0",
    "passport": "^0.4.0",
    "passport-local": "^1.0.0",
    "serve-favicon": "~2.4.5"
    }
    }

测试代码

与 passport 初始化流程相关的代码如下,

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
49
50
51
/** 1. 初始化 passport **/
const passport = require('passport');
app.use(passport.initialize());
app.use(passport.session());
/** 2. 初始化 Local Strategy 相关 **/
// ====> 2.1 初始化 database,这里通过 hash table 模拟
const users = [
{ id: '1', username: 'bob', password: 'secret', name: 'Bob Smith' },
{ id: '2', username: 'joe', password: 'password', name: 'Joe Davis' },
];
var User = {};
User.findById = (id, done) => {
for (let i = 0, len = users.length; i < len; i++) {
if (users[i].id === id) return done(null, users[i]);
}
return done(null, null);
};
User.findByUsername = (username, done) => {
for (let i = 0, len = users.length; i < len; i++) {
if (users[i].username === username) return done(null, users[i]);
}
// return done(new Error('User Not Found')); // 如何是这样的话,failureRedirect 就会失效,而直接进入 500 页面
return done(null, null);
};
// ====> 2.2 配置 Local Strategy
const LocalStrategy = require('passport-local').Strategy;
passport.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);
});
}
));
passport.serializeUser((user, done) => done(null, user.id));
// 这里实际上就是接口了,你可以自定义如何查找 user 的方式;要注意,官方文档上说明的是,这种情况是使用 session 才有的序列化方式
passport.deserializeUser((id, done) => {
db.users.findById(id, (error, user) => done(error, user));
});
app.post('/login', passport.authenticate('local', { successReturnToOrRedirect: '/', failureRedirect: '/login' }))

主要分为两个流程,

  1. 初始化 passport
  2. 初始化 Local Strategy 相关

后续的源码分析将会分为了两个阶段进行深入的剖析;

测试代码下载

passport-local-demo.zip

源码分析

passport 核心组件

passport 的初始化流程比较的复杂,如果没有对它的核心模块有一个整体上的认知,是很难摸清它的底层交互逻辑的,基于此,笔者绘制了该核心组件图,将各个核心组件的内在逻辑以及相互之间的关联关系梳理出来;
passport core components.png

passport 总共有 9 大核心模块,其它的扩展都是基于这 9 大核心模块构建起来的;其基本逻辑是,

  1. 在加载 index.js 模块的时候,便会初始化 authenticator.js 模块所 exports 的 Authenticator 对象,并作为 index.js 模块的 exports;对应上述测试代码的第 2 行;
  2. 在初始化 Authenticator 对象的时候,会通过 this.init() 方法分别初始化
    1. connect 模块
    2. Session Strategy 模块
    3. Session Manager 模块
  3. 在 #2 初始化 connect 模块的时候,会初始化
    1. initialize 模块,该模块主要负责对 passport 对象做进一步的初始化动作;
    2. authenticate 模块,这个模块非常的重要,某个请求的 router 就是通过该方法对用户身份进行验证的;
  4. 然后是 request.js 模块,该模块定义了 request 相关的方法,login、logout 和 isAuthenticated,isAuthenticated 属性注明了该用户是否已经认证通过了;
  5. 最后来看看 passport 在 ./node_modules 中的包结构,
    nodejs passport package structure.png

初始化流程分析

Authenticator 初始化流程

该部分的初始化流程对应测试代码第 2 到 3 行,如下,

1
2
const passport = require('passport');
app.use(passport.initialize());

相关流程图如下,
01 - Authenticator init process.png

上面的流程非常的简单直接,只是要注意的是,当通过 1.1.1.1 加载 session 模块的时候,返回的是 SessionStrategy 模板对象,并赋值到 index 模块的 exports.strategies.SessionStrategy 对象中,随着 exports 的返回而返回;通过 require(‘passport’) 返回的是一个 Authenticator 实例并赋值给 passport;下面笔者重点要分析的是 Authenticator new 初始化流程和 Authenticator initialize 初始化流程,

Authenticator new 初始化流程

先来看下 Authenticator 对象的代码,

1
2
3
4
5
6
7
8
9
10
11
function Authenticator() {
this._key = 'passport';
this._strategies = {};
this._serializers = [];
this._deserializers = [];
this._infoTransformers = [];
this._framework = null;
this._userProperty = 'user';
this.init();
}

可以看到,在通过关键字 new 初始化 Authenticator 的时候,创建了很多关键的属性,包括 _strategies、_serializers、_deserializers 以及 _framework 等,但最为关键的是,其调用 init() 方法的流程,先来看看 init() 方法里面做了什么,

1
2
3
4
5
Authenticator.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));
};

011 - Authenticator new process.png
在读这个流程图的时候,首先要注意到的是,authenticator 实例就是 app.js 中的 passport 实例;整个 init 过程分为三个子流程,

  1. init framework
    注意 Step 2.1,authenticator._framework 的值是 connect 所 exports 出来的 anonymous function

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    exports = module.exports = function() {
    // HTTP extensions.
    exports.__monkeypatchNode();
    return {
    initialize: initialize,
    authenticate: authenticate
    };
    };
  2. init Session Strategy
    通过 authenticator.use() 方法从 session.js 模块中加载了默认的 Session Strategy 并赋值给 authenticator._strategies 对象,相关代码逻辑如下,

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    Authenticator.prototype.use = function(name, strategy) {
    if (!strategy) {
    strategy = name;
    name = strategy.name;
    }
    if (!name) { throw new Error('Authentication strategies must have a name'); }
    this._strategies[name] = strategy;
    return this;
    };

    session.js 模块中主要创建了 SessionStrategy 对象以及 SessionStrategy 的 authenticate() 方法用来进行用户身份验证;

  3. init Session Manager
    SessionManager 默认提供了一系列管理 Session 的方法,logIn() 和 logOut(),

Authenticator initialize 初始化流程

1
app.use(passport.initialize());

passport.initialize()

1
2
3
4
5
6
Authenticator.prototype.initialize = function(options) {
options = options || {};
this._userProperty = options.userProperty || 'user';
return this._framework.initialize(this, options);
};

可见,它调用的是 this._framework 对象的 initialize 方法,有上一部分可知,this._framework 引用的是 connect.js 模块所 exports 出来的对象,因此,可知,this._framework.initialize(this, options) 调用的就是 initialize.js 模块的 initialize 方法,如下,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module.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 对象以及相关的属性 req._passport.session;

注册 Session 和 Session Strategy 认证流程

此部分对应流程图中的 step 3,相关代码如下,

1
app.use(passport.session());

先来分析 passport.session() 的执行流程,

1
2
3
Authenticator.prototype.session = function(options) {
return this.authenticate('session', options);
};
1
2
3
Authenticator.prototype.authenticate = function(strategy, options, callback) {
return this._framework.authenticate(this, strategy, options, callback);
};

可以看到,最终实际上调用的就是 this._framework.authenticate() 方法,由前面的分析可知,this._framework 对应的就是 connect.js 模块所 exports 的对象,其中就有 authenticate 方法属性,由此,这里实际上调用的是 authenticate.js 模块的 authenticate() 方法,

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
module.exports = function authenticate(passport, name, options, callback) {
if (typeof options == 'function') {
callback = options;
options = {};
}
options = options || {};
var multi = true;
// Cast `name` to an array, allowing authentication to pass through a chain of
// strategies. The first strategy to succeed, redirect, or error will halt
// the chain. Authentication failures will proceed through each strategy in
// series, ultimately failing if all strategies fail.
//
// This is typically used on API endpoints to allow clients to authenticate
// using their preferred choice of Basic, Digest, token-based schemes, etc.
// It is not feasible to construct a chain of multiple strategies that involve
// redirection (for example both Facebook and Twitter), since the first one to
// redirect will halt the chain.
if (!Array.isArray(name)) {
name = [ name ];
multi = false;
}
return function authenticate(req, res, next) {
if (http.IncomingMessage.prototype.logIn
&& http.IncomingMessage.prototype.logIn !== IncomingMessageExt.logIn) {
require('../framework/connect').__monkeypatchNode();
}
...
这部分代码非常的重要,是用户身份认证的模板代码;
...
}
}

备注,上面被省略掉的代码非常的重要,它是执行用户身份认证的模板代码,标准流程,这部分内容笔者将会在下一篇博文中重点进行阐述;上面的方法的执行逻辑很简单,调用此方法,返回一个新的方法 authenticate(req, res, next),不过要注意的是,此新的方法有当前方法所形成的闭包,闭包里面存放了一个关键的变量 name,作为 authenticate(req, res, next) 方法调用时的上下文环境;

最后,将 authenticate(req, res, next) 作为 Middleware 注册到当前的 Express 实例 app 中;这也是为什么在每次执行其它 Strategy 验证逻辑的时候,首先都会执行 Session Stratey 的 authenticate 方法的原因,因为它已经作为 app 的中间件,在每次发生调用之前都会执行 Session Strategy 的 authenticate 流程;

注册其它 Strategy 验证流程

这里以 Local Strategy 为例来演示相关的注册流程,相关代码如下,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const LocalStrategy = require('passport-local').Strategy;
passport.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);
});
}
));
app.post('/login', passport.authenticate('local', { successReturnToOrRedirect: '/', failureRedirect: '/login' }))
  • 代码第 1 行,从 passport-local 模块中获得 exports 出来的 Strategy 对象,然后赋值给 LocalStrategy;看看 passport-local 模块中定义了什么,

    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
    function Strategy(options, verify) {
    if (typeof options == 'function') {
    verify = options;
    options = {};
    }
    if (!verify) { throw new TypeError('LocalStrategy requires a verify callback'); }
    this._usernameField = options.usernameField || 'username';
    this._passwordField = options.passwordField || 'password';
    passport.Strategy.call(this);
    this.name = 'local';
    this._verify = verify;
    this._passReqToCallback = options.passReqToCallback;
    }
    /**
    * Authenticate request based on the contents of a form submission.
    *
    * @param {Object} req
    * @api protected
    */
    Strategy.prototype.authenticate = function(req, options) {
    options = options || {};
    var username = lookup(req.body, this._usernameField) || lookup(req.query, this._usernameField);
    var password = lookup(req.body, this._passwordField) || lookup(req.query, this._passwordField);
    ...
    }
    /**
    * Expose `Strategy`.
    */
    module.exports = Strategy;

    首先,定义了一个 Strategy 对象,该对象就是 Local Strategy,主要用来构造通过用户名和密码验证的 Strategy,此部分要注意将该 Strategy 命名为 ‘local’,该名字将会被作为关键字注入 passport._strategies 供 authenticate.js 模块中的 authenticate() 模板方法进行调用;

    再次,定义了实例方法 authenticate(),该方法将会被 authenticate.js 模块中的 authenticate() 模板方法进行回调,进而实现对用户身份的认证以及相关上下文的创建,具体相关的验证流程笔者将会在另外一篇博文中进行阐述,这里需要记住的是,passport-local 模块主要负责定义 Local Strategy 的验证策略;还要注意的是,上述的代码 11 行是 javascript 的面向对象中的“继承”的写法;

  • 代码第 3 到 12 行,实例化 LocalStrategy 对象并注入 passport;注意里面用户自定义的方法,

    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);
    });
    }

    上述的 lambda 方法将会作为 options 参数用来构建 passport-local 模块中的 Strategy 对象,也就是这里的 LocalStrategy,从其构造函数中可以看到,用户自定义的 lambda 回调方法赋值给了 LocalStrategy 实例的私有属性 _verify,_verify 将会在 LocalStrategy 执行 authenticate 的时候被回调;

    passport.use(LocalStrategy instance)
    将 LocalStrategy instance 以关键字 ‘local’ 注入 passport._strategies 供 authenticate.js 模块中的 authenticate() 模板方法进行调用;

  • 代码第 14 行,passport.authenticate(‘local’, {…})

    1
    passport.authenticate('local', { successReturnToOrRedirect: '/', failureRedirect: '/login' })

    该注册过程与注册 Session 认证流程类似,得到一个闭包上下文 name = ‘local’ 的属于 authenticate.js 模块的 authenticate(req, res, next) 方法句柄;不过要注意的是,这里可以通过数组的方式使用多个 Strategies 来进行验证,比如,

    1
    2
    3
    4
    5
    app.post('/token',
    passport.authenticate(['basic', 'oauth2-client-password'], { session: false }),
    oauth2.token(),
    oauth2.errorHandler()
    );

    这样的话,表示当请求 /token 访问的时候,会使用 Basic Strategy 和 OAuth2 Client Password Strategy 对用户的身份进行验证,要注意的是,这里会根据 Strategy 注册的顺序依次进行验证,所以,会首先通过 basic 验证,若没有通过,则再通过 OAuth2 Client Password Strategy 对用户身份进行验证;

Contents
  1. 1. 前言
  2. 2. demo
    1. 2.1. 构建项目骨架
    2. 2.2. 测试代码
    3. 2.3. 测试代码下载
  3. 3. 源码分析
    1. 3.1. passport 核心组件
    2. 3.2. 初始化流程分析
      1. 3.2.1. Authenticator 初始化流程
        1. 3.2.1.1. Authenticator new 初始化流程
        2. 3.2.1.2. Authenticator initialize 初始化流程
    3. 3.3. 注册 Session 和 Session Strategy 认证流程
    4. 3.4. 注册其它 Strategy 验证流程