Nodejs Passport 系列之一:基础概念

前言

本文是笔者所总结的有关 Nodejs Passport 系列之一,本篇文章主要是对其基本的构建原理进行梳理;

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

概述

Passport 构建得及其的简单,它被设计为 Nodejs 的中间件;具体的使用过程是将 passport 作为中间件嵌入到某 Express 的请求之前或者之后,用来验证用户的身份;而针对不同的验证方式,Passport 通过提供不同的验证策略(Strategies),通过这些不同的 Strategies 来提供不同的验证逻辑和方式,这些 Strategies 在 Passport 中被定义成不同的模块通过依赖包的形式进行载入,并作为 Passport 的中间件,在需要的时候注入;常用的 Strategies 有,

核心组件

nodejs passport component diagram.png

上图以 Local Strategy 和 Basic Strategy 为例,描绘了 passport 所相关的核心组件;可以看到,passport 最主要的就是两大模块,一个是 passport 自身,另外一个就是 Strategy 策略;

  1. 首先,要将需要使用到的 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 进行查找;

  2. 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;

  1. 首先安装 Local Strategy 模块包,

    1
    $ npm install passport-local
  2. 通过 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() 来实现的,

  1. 如果用户认证成功,通过如下的方式通知 passport 用户已经认证成功了,

    1
    return done(null, user);
  2. 如果认证失败,通过如下的方式通知 passport 用户认证失败了,

    1
    return done(null, false);
  3. 如果需要明确失败的原因,

    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 中,不过要注意两种场景,

  1. 启用 session
    如果启用了 session,已认证的用户的后续请求将可以直接通过 req.user 获取用户的身份信息,并且不再需要再次认证了;不过这得益于 session 的用户序列化的机制,参考 session 章节;
  2. 禁用 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 )可以访问到哪些资源;这部分需要其它的解决方案了!

References

http://www.passportjs.org/docs/