passport 中间件学习

Passportjs: nodejs 身份验证中间件

本文资料来源

  1. passport官网

  2. passport strategy

  3. 伤神的博客

  4. 知乎-吴成琦

一、Passport介绍

Passport 是 基于 Express框架的Node.js 的身份验证中间件。 Passport 极其灵活和模块化,可以不显眼地放入任何基于 Express 的 Web 应用程序中。

认证请求流程(以配置localStrategy为例):

当我们通过 接口 /login 进行身份验证时候,发生以下情况:

    1. 发起 /login 请求时,执行我们设置好的 passport.authenticate
    1. 开始进行认证,因为我们配置的是 localStrategy , 所以passport 将调用 localStrategy 进行逻辑认证;
    1. Passport 获取到 请求中的usernamepassword(一般是放在body中,在这里我放在了路径里方便查看),将其传递给策略中的verification function

    1. 加载用户传过来的数据并检查是否符合我们的判定条件
    1. 判定条件结果:
    • 判定条件未执行完成:比如在验证的逻辑中出现数据库查询失败,调用其他服务报错等等,调用 done(err),将错误信息抛出;
    • 数据不符合判定条件:当用户输入的参数不符合判定条件,调用 done(null, false)
    • 符合判定逻辑:验证通过,调用 done(null, user)
    1. 如果 5 验证成功,passport.authenticate() 自动调用 req.login() 函数,该函数主要在用户注册时使用,在此期间可以调用 req.login() 来自动记录新注册的用户。

    1. 之后(验证通过)继续调用 passport.serializeUser 方法(之前定义好的),将用户信息序列化,目的放到 req.session.passport.user
    1. 用户信息最终也会放到 req.user
    1. 流程结束

后续经过身份验证的请求流程

在用户登录之后,用户访问被保护的路由时,会经过如下的流程:

    1. express 框架加载 session 数据,由于之前登录的用户信息已经被passport 保存在了session中,所以可以从req.session.passport.user 中找到
    1. 这个时候 之前定义好的passport.initialize 函数就会被调用,如果找到了之前认证过的session信息,就会将passport.user 也放到req中;如果请求没有经过认证,就将 passport.user = {} 放到req中
    1. 接下来,passport.session就会被触发(每一个请求都会触发这个中间件),逻辑: 如果该中间件在请求中找到了序列化的用户信息,那么这个请求就是已经经过认证了的;
    1. passport.session 会回调之前定义好的 passport.deserializeUser ,将反序列化的用户对象放到req.user 中

Passport 的方法介绍

  • passport.initialize : 每个请求进来都会被调用,可以确保session 中有 passport.user(用户信息 || {} )
  • passport.session:passport.session 中间件是一个 Passport 策略,如果服务端找到序列化的用户信息,它将序列化的用户信息加载到 req.user 上
  • passport.deserializeUser: 每个请求都会通过 passport.session 调用该函数,用户信息或者其他的信息可以通过这个函数加载到req.user 上
  • 策略什么时候调用: 只有在路由中使用 了 passport.authenticate 中间件才会调用
  • passport.serializeUser 只会在验证期间才会调用,以此来存储对应的用户信息

其他的一些方法

  • req.login()
  • req.logout()
  • req.isAuthenticated()
  • req.isUnAuthenticated()

策略示例

  1. passport-wechat
const passport = require('passport');

const WechatStrategy = require('passport-wechat');

passport.use(new WechatStrategy({
    appID:'', // {APPID},
    name:'wechat-test-demo', // {默认为wechat,可以设置组件的名字}
    appSecret:'', // {APPSECRET},
    client:'', // {wechat|web},
    callbackURL:'/emma/wechat/callback', // {CALLBACKURL},
    scope:'', // {snsapi_userinfo|snsapi_base},
    state:'', // {STATE},
    getToken:'', // {getToken},
    saveToken:'', // {saveToken}
  }, function(accessToken, refreshToken, profile, done) {
    return done(err,profile);
  }
));

router.get('/emma/wechat', passport.authenticate('wechat-test-demo', passportOption));
router.get('/emma/wechat/callback', passport.authenticate('wechat-test-demo', {
    failureRedirect: '/auth/fail',
    successReturnToOrRedirect: '/',
}));

passportOption

state —— 重定向后会带上 state 参数,开发者可以填写a-zA-Z0-9的参数值,最多128字节
callbackURL —— 授权后重定向的回调链接地址, 需要使用 urlEncode 对链接进行处理
scope —— snsapi_base (不弹出授权页面,直接跳转,只能获取用户openid),snsapi_userinfo (弹出授权页面,可通过 openid 拿到昵称、性别、所在地。并且, 即使在未关注的情况下,只要用户授权,也能获取其信息 )
  1. passport-oauth2
const OAuth2Strategy = require('passport-oauth2');

passport.use(new OAuth2Strategy({
    authorizationURL: 'https://divineswordvilla.xyz/oauth2/authorize',
    tokenURL: 'https://divineswordvilla.xyz/oauth2/token',
    clientID: EXAMPLE_CLIENT_ID,
    clientSecret: EXAMPLE_CLIENT_SECRET,
    callbackURL: "http://divineswordvilla.xyz/auth/emma/callback"
},function(accessToken, refreshToken, profile, cb) {
    // 业务逻辑处理
    if (err) return cb(err);
    return cb(null, profile);
}));

router.get('/emma/login', passport.authenticate('oauth2'));
router.get('/auth/emma/callback', passport.authenticate('oauth2', { failureRedirect: '/login' }), function (req, res) {
    // 认证成功之后,跳转到业务需求的界面
    res.redirect('/');
});
  1. passport-jwt
    使用JSON web token (jwt) 对端进行认证,可用于一些没有用session的restful 端点
const passport = require('passport');
const JwtStrategy = require('passport-jwt').Strategy;
const ExtractJwt = require('passport-jwt').ExtractJwt;

passport.use(new JwtStrategy({
    jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
    secretOrKey: 'secret', 
    name: '', // {}
    issuer: '',
    audience: '',
}, function(jwt_payload, done) {
    // 根据物业需求添加逻辑
    User.findOne({id: jwt_payload.sub}, function(err, user) {
        if (err) {
            return done(err, false);
        }
        if (user) {
            return done(null, user);
        } else {
            return done(null, false);
        }
    });
}));

app.post('/profile', passport.authenticate('jwt', { session: false }),
    function(req, res) {
        res.send(req.user.profile);
    }
);
  1. passport-local
const passport = require('passport');
const 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); }
            if (!user.verifyPassword(password)) { return done(null, false); }
            return done(null, user);
          });
    }
));

app.post('/login', passport.authenticate('local', { failureRedirect: '/login' }),
  function(req, res) {
    // 业务逻辑
    res.redirect('/');
});

遗留的问题

  • initialize 和 策略 的先后顺序是否会对 passport的使用有影响?
  • 当不符合验证(符合验证) 会出现一些什么信息
  • 登录完之后,其他请求进入为完整的200;
  • 受保护的路由不传用户名密码也能返回200
  • 受保护的路由,在验证失败或者过期之后返回 401
  • 序列化反序列化目的?
  • 序列化反序列化使用场景?
  • 什么时候会执行序列化和反序列化函数?
  • name 用法
    demo
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
禁止转载,如需转载请通过简信或评论联系作者。

推荐阅读更多精彩内容