速通passport源码以及策略源码

412 阅读8分钟

速通passport源码以及策略源码

前提

  • 具备知识:passport、express、最好有nest的基础知识

使用passport

  • 策略:以何种方式去校验用户
  • 定义策略
var passport = require('passport');
var LocalStrategy = require('passport-local');

passport.use(new LocalStrategy(function verify(username, password, callback) {
  db.get('SELECT * FROM users WHERE username = ?', [ username ], function(err, user) {
    // ... 校验username、password的逻辑
    
    // callback(error: any, user?: Express.User | false, info?: any)
    // error:错误,user:用户信息(成功传递),info:一般是失败信息(不算错误,但是失败的原因)
    
    callback(null, user); // 校验成功
    // 校验错误:callback(new Error("DB查无此人"));
    // 校验失败:callback(null, false, "账号密码不一致")
  });
}));

// express全局中间件上使用
app.use()
  • Express中间件的普通使用:默认没有定义认证回调函数的,都会执行next()或next(err)抛出异常
// local默认是LocalStrategy内置的
app.post('/login/password',
         // 后面的options有很多,表示成功、失败后如何处理,大多都是通用的
         // 如:failureRedirect表示失败后重定向地址
         // failureMessage:失败后在req.session.messages上增加失败原因
  passport.authenticate('local', { failureRedirect: '/login', failureMessage: true }),
  function(req, res) {
    res.redirect('/~' + req.user.username);
  });
  • Express中间件使用认证回调函数:要注意next()调用
app.get('/protected', function(req, res, next) { 
  passport.authenticate('local', function callback(err, user, info, status) { 
    if (err) { return next(err) } // 认证失败交给下一个中间件处理
    if (!user) { return res.redirect('/signin') } 
    res.redirect('/account'); // 认证成功跳转
  })(req, res, next); // 手动调用passport.authenticate()返回的中间件
}, (req, res) {
	// "/protected"接口逻辑
});
  • nestjs整合passport、passport-jwt策略
  • 定义策略,定义jwt策略中间件
// verify.middleware.ts
import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { MemoService } from "../memo/memo.service";
import { NextFunction, Request, Response } from "express";
import { JsonWebTokenError } from "jsonwebtoken";
import passport from "passport";
import { Strategy as JWTStrategy, VerifiedCallback } from "passport-jwt";

@Injectable()
export default class VerifyMiddleware {
  constructor(
		private readonly configService: ConfigService, // 配置服务
    private readonly memoService: MemoService // redis服务
  ) {
    passport.use(
      "jwt", // 虽然passport-jwt本身就叫"jwt",这里起到自定义作用
      new JWTStrategy(
        {
          // 1. 获取Token的方法
          jwtFromRequest: (req: Request) => {
            if (!req.headers["authorization"].includes("Bearer ")) {
              return null;
            }
            // 这里可以自定义,但是返回的结果一定是JWT,否则passport-jwt进行校验,失败则抛出异常
            // 抛出异常就会执行passport.fail() -> 下一个策略,没有时调用认证回调函数()
            return req.headers["authorization"].split("Bearer ")[1];
          },
          ignoreExpiration: false,
          secretOrKey: this.configService.get("JWT_SECRET"),
          passReqToCallback: true, // 钩子传入Request参数
        },

				// 2. 此策略的校验钩子(此时代表jwt校验是成功的,可以自己进行二次其他的校验)
        // TIP:
        // 这里最好写同步写法,不要async/await,因为passport-jwt中并未对异步做支持,要用异步就用Promise
        // done(err) -> passport.error() -> 认证回调()
        // done(null, payload) -> passport.success() -> 认证回调()
        async (req: Request, payload: IPayload, done: VerifiedCallback) => {
          if (!payload.userID || !payload.email || !payload.exp) {
            done(new JsonWebTokenError("伪造JWT"), null);
            return;
          }

          // 校验是否与redis中的accessToken一致
          const accessToken = req.headers["authorization"].split("Bearer ")[1];
          this.memoService.getLoginInfo(payload.userID).then(loginInfo => {
            if (!loginInfo) {
              done(null, null, { message: "用户已登出" });
              return;
            } else if (loginInfo.accessToken !== accessToken) {
              done(null, null, { message: "JWT非最新" });
              return;
            }
            // 此处调用passport.success()的回调函数,即下面的`authCallback()`
            done(null, payload);
          });
        }
      )
    );
  }

  // nest中间件
  use(req: Request, res: Response, next: NextFunction) {
    passport.authenticate(
      "jwt",
      {
        session: false,
      },
      function authCallback(
        err: Error,
        user: IPayload,
        info: { message: string }
      ) {
        // 错误处理
        const forbidden = err || info;
        if (forbidden) {
          let tip = "";
          if (info) {
            tip = info.message;
          } else {
            tip = err.message;
          }
          const errRet = ResponseUtil.error(tip);
          return res.status(400).json(errRet).end();
        }

        // 执行下一个中间件
        req.user = user;
        next(); // 一定要调用,passport不会为我们自动调用的
      }
    )(req, res, next);
  }
}
  • 挂载nest中间件
// app.module.ts
import VerifyMiddleware from "./middleware/verify.middleware";

export class AppModule {
  async configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(VerifyMiddleware)
      .exclude("/auth/register", "/auth/login", "/auth/refresh")
      .forRoutes("*");
  }
}

图解

Picsee-20231027134831

图片内容较多,不清晰右键在新标签中浏览。

passport、passport-jwt源码分析

  • passport-jwt/strategy.js:是一个策略构造函数

  • passport/authenticator.js#use():注册Strategy构造函数

// use():设置Strategy策略
// use("custom", JwtStrategy) 自定义名称的策略
// use("custom", JwtStrategy) 用JwtStrategy.name作为策略名
// 最后挂载到_strategies策略集合上,便于后面调用这些策略
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;
};
// 所以我们得以如下形式调用
passport.use(new JwtStrategy(...));
  • 🔥核心:passport/middleware/authenticate.js#authenticate():执行预配置,并返回一个中间件函数
// 一开始调用并非它,而核心最后执行的是该方法
module.exports = function authenticate(passport, name, options, callback) {
  if (typeof options == 'function') {
    callback = options;
    options = {};
  }
  options = options || {};
  var multi = true;
  
  // 判断策略是否是单个还是多个,便于后面决定是否执行下一个策略
  if (!Array.isArray(name)) {
    name = [ name ];
    multi = false;
  }

  // 返回的中间件
  return function authenticate(req, res, next) {
    // 这些都是passport内置的方法(passpot/http/request.js内)
    // 具备session 且 passport.authenticate("session")下,passport.serializeUser()、passport.deserializeUser()才有作用
    // req.login(user)相当于req.user = user;
    req.login =
    req.logIn = req.logIn || IncomingMessageExt.logIn;
    // req.login(user)相当于req.user = undefined;
    req.logout =
    req.logOut = req.logOut || IncomingMessageExt.logOut;
    // 根据options.userProperty设置req[options.userProperty]作为登录依据。存在即为true,反之为false
    req.isAuthenticated = req.isAuthenticated || IncomingMessageExt.isAuthenticated;
    req.isUnauthenticated = req.isUnauthenticated || IncomingMessageExt.isUnauthenticated;
    req._sessionManager = passport._sm;

    // 每一个策略失败都会push一个错误原因
    var failures = [];

    // 所有策略结束即调用该方法,如果存在认证回调则传入失败(info参数而不是err参数)
    function allFailed() {
      if (callback) {
        if (!multi) {
          return callback(null, false, failures[0].challenge, failures[0].status);
        } else {
          var challenges = failures.map(function(f) { return f.challenge; });
          var statuses = failures.map(function(f) { return f.status; });
          return callback(null, false, challenges, statuses);
        }
      }

      // 下面是根据options设置相关内容
      var failure = failures[0] || {}
        , challenge = failure.challenge || {}
        , msg;

      if (options.failureFlash) {
        var flash = options.failureFlash;
        if (typeof flash == 'string') {
          flash = { type: 'error', message: flash };
        }
        flash.type = flash.type || 'error';

        var type = flash.type || challenge.type || 'error';
        msg = flash.message || challenge.message || challenge;
        if (typeof msg == 'string') {
          req.flash(type, msg);
        }
      }
      if (options.failureMessage) {
        msg = options.failureMessage;
        if (typeof msg == 'boolean') {
          msg = challenge.message || challenge;
        }
        if (typeof msg == 'string') {
          req.session.messages = req.session.messages || [];
          req.session.messages.push(msg);
        }
      }
      if (options.failureRedirect) {
        return res.redirect(options.failureRedirect);
      }

      // When failure handling is not delegated to the application, the default
      // is to respond with 401 Unauthorized.  Note that the WWW-Authenticate
      // header will be set according to the strategies in use (see
      // actions#fail).  If multiple strategies failed, each of their challenges
      // will be included in the response.
      var rchallenge = []
        , rstatus, status;

      for (var j = 0, len = failures.length; j < len; j++) {
        failure = failures[j];
        challenge = failure.challenge;
        status = failure.status;

        rstatus = rstatus || status;
        if (typeof challenge == 'string') {
          rchallenge.push(challenge);
        }
      }

      res.statusCode = rstatus || 401;
      if (res.statusCode == 401 && rchallenge.length) {
        res.setHeader('WWW-Authenticate', rchallenge);
      }
      if (options.failWithError) {
        return next(new AuthenticationError(http.STATUS_CODES[res.statusCode], rstatus));
      }
      res.end(http.STATUS_CODES[res.statusCode]);
    }
    // allFailed()结束

    // 自执行第一个策略
    (function attempt(i) {
      var layer = name[i];
      if (!layer) { return allFailed(); } // 不存在策略了,即调用上面的失败函数

      // 处理策略,strategy变量就是能运行的策略函数
      var strategy, prototype;
      if (typeof layer.authenticate == 'function') {
        strategy = layer;
      } else {
        prototype = passport._strategy(layer);
        if (!prototype) { return next(new Error('Unknown authentication strategy "' + layer + '"')); }

        strategy = Object.create(prototype);
      }

			// 为策略添加实例方法
      // 成功:如果有认证回调函数则执行,否则执行options配置的内容并调用next();
      strategy.success = function(user, info) {
        if (callback) {
          return callback(null, user, info);
        }

        info = info || {};
        var msg;

        if (options.successFlash) {
          var flash = options.successFlash;
          if (typeof flash == 'string') {
            flash = { type: 'success', message: flash };
          }
          flash.type = flash.type || 'success';

          var type = flash.type || info.type || 'success';
          msg = flash.message || info.message || info;
          if (typeof msg == 'string') {
            req.flash(type, msg);
          }
        }
        // ...
      };

      // 失败:递归下一个策略
      strategy.fail = function(challenge, status) {
        if (typeof challenge == 'number') {
          status = challenge;
          challenge = undefined;
        }
        // 推送错误进failures数组
        failures.push({ challenge: challenge, status: status });
        attempt(i + 1); // 递归执行下一个策略
      };

      // 重定向(这里是兼容Express 4-版本)
      strategy.redirect = function(url, status) {
        // 可以看看注释
        // NOTE: Do not use `res.redirect` from Express, because it can't decide
        //       what it wants.
        //
        //       Express 2.x: res.redirect(url, status)
        //       Express 3.x: res.redirect(status, url) -OR- res.redirect(url, status)
        //         - as of 3.14.0, deprecated warnings are issued if res.redirect(url, status)
        //           is used
        //       Express 4.x: res.redirect(status, url)
        //         - all versions (as of 4.8.7) continue to accept res.redirect(url, status)
        //           but issue deprecated versions
        res.statusCode = status || 302;
        res.setHeader('Location', url);
        res.setHeader('Content-Length', '0');
        res.end();
      };

      // 跳出所有策略校验,直接去执行下一个中间件
      strategy.pass = function() {
        next();
      };

      // 错误:要么执行认证回调函数,要么next(err)交给下一个中间件
      strategy.error = function(err) {
        if (callback) {
          return callback(err);
        }

        next(err);
      };
      strategy.authenticate(req, options); // 执行策略并传入options
    })(0); // attempt
  };
};

⚠️ 这就是为什么我们使用了认证回调后,必须手动next()的原因

app.get('/protected', function(req, res, next) { 
  passport.authenticate('local', function callback(err, user, info, status) { 
    if (err) { return next(err) } // 认证失败交给下一个中间件处理
    next(); // ⚠️
  })(req, res, next);
}, (req, res) {
	// "/protected"接口逻辑
});

对于想深入Session策略部分的同学,移步到官网即可。Passport: Sessions

  • 🔥 passport-jwt核心代码 passport-jwt/lib/strategy.js#JwtStrategy.prototype.authenticate()
JwtStrategy.prototype.authenticate = function(req, options) {
    var self = this;

    var token = self._jwtFromRequest(req); // 获取jwt

    if (!token) {
        return self.fail(new Error("No auth token"));
    }

    // 校验jwt
    this._secretOrKeyProvider(req, token, function(secretOrKeyError, secretOrKey) {
        if (secretOrKeyError) {
            self.fail(secretOrKeyError)
        } else {
            // Verify the JWT
            JwtStrategy.JwtVerifier(token, secretOrKey, self._verifOpts, function(jwt_err, payload) {
                if (jwt_err) {
                    return self.fail(jwt_err); // 校验失败,执行下一个中间件
                } else {
                    // Pass the parsed token to the user
                    var verified = function(err, user, info) {
                        if(err) {
                            // 有错误
                            return self.error(err);
                        } else if (!user) {
                            // user为false
                            return self.fail(info);
                        } else {
                            // 成功,交给passport给策略挂载的sucess()
                            return self.success(user, info);
                        }
                    };

                    try {
                        if (self._passReqToCallback) {
                            // 具备options.passReqToCallback时,多传递request对象
                            // verified是上面这个函数
                            self._verify(req, payload, verified);
                        } else {
                            self._verify(payload, verified);
                        }
                    } catch(ex) {
                        // 遇到错误调用passport给策略挂载的error()
                        self.error(ex);
                    }
                }
            });
        }
    });
};

图解源代码

Picsee-20231027140240

图解passport-oauth2源码

Picsee-20231104200844

推荐在新标签中打开,配合debug源码查看流程!

简单分析passport-oauth2的StateStore逻辑

  • passport-oauth2/strategy.js:创建Store容器
function OAuth2Strategy(options, verify) {
  if (typeof options == 'function') {
    verify = options;
    options = undefined;
  }
  options = options || {};

  // ... 必填参数校验
  passport.Strategy.call(this);
  
  // ...策略参数写入
  this.name = 'oauth2';
  this._verify = verify;
  this._oauth2 = new OAuth2(options.clientID,  options.clientSecret,
                            '', options.authorizationURL, options.tokenURL, options.customHeaders);
  // ...

  // 设置stateStore容器,这很重要(🚀 自定义)
  if (options.store && typeof options.store == 'object') {
    // 自己实现的stateStore
    this._stateStore = options.store;
  } else if (options.store) {
    // 这里和下面的分支一个样,都是用到了session,我们分析一个就可以了
    // 源码: state/store.js
    this._stateStore = options.pkce ? new PKCEStateStore({ key: this._key }) : new StateStore({ key: this._key });
  } else if (options.state) {
    // 这里和上面的分支一个样,都是用到了session
    this._stateStore = options.pkce ? new PKCEStateStore({ key: this._key }) : new NonceStore({ key: this._key });
  } else {
    // 该Store的源码为不去生成/校验state随机值
    if (options.pkce) { throw new TypeError('OAuth2Strategy requires `state: true` option when PKCE is enabled'); }
    this._stateStore = new NullStore();
  }
}
  • passport-oauth2/strategy.js(283行):调用Store容器的store方法
// 容器执行完store后会被调用,并发出302重定向去进行第三方站点认证授权
function stored(err, state) {
  if (err) { return self.error(err); }

  if (state) { params.state = state; }
  var parsed = url.parse(self._oauth2._authorizeUrl, true);
  utils.merge(parsed.query, params);
  parsed.query['client_id'] = self._oauth2._clientId;
  delete parsed.search;
  var location = url.format(parsed);
  self.redirect(location);
}

try {
  // 调用store方法
  var arity = this._stateStore.store.length;
  if (arity == 5) {
    this._stateStore.store(req, verifier, state, meta, stored);
  } else if (arity == 4) {
    this._stateStore.store(req, state, meta, stored);
  } else if (arity == 3) {
    this._stateStore.store(req, meta, stored);
  } else { // arity == 2
    this._stateStore.store(req, stored);
  }
} catch (ex) {
  return this.error(ex);
}
  • passport-oauth2/state/store.js:存储state、校验state
SessionStore.prototype.store = function(req, state, meta, callback) {
  var key = this._key; // 一个表示passport-oauth2存储的key
  var sstate = {
    handle: uid(24) // 随机uid
  };
  if (state) { sstate.state = state; }
  if (!req.session[key]) { req.session[key] = {}; }
  req.session[key].state = sstate; // 存储
  callback(null, sstate.handle); // 去执行上面代码的回调
};

// 校验并删除session[key]的值
SessionStore.prototype.verify = function(req, providedState, callback) {
  // ...
  var key = this._key;
  if (!req.session[key]) {
    return callback(null, false, { message: 'Unable to verify authorization request state.' });
  }

  var state = req.session[key].state;
  if (!state) {
    return callback(null, false, { message: 'Unable to verify authorization request state.' });
  }

  delete req.session[key].state;
  if (Object.keys(req.session[key]).length === 0) {
    delete req.session[key];
  }

  if (state.handle !== providedState) {
    return callback(null, false, { message: 'Invalid authorization request state.' });
  }

  return callback(null, true, state.state);
};

🚀 综上所述,如果你想自定义一个Store,直接按照这个文件为模板编写即可!

引用

  1. Passport官方文档:Sessions
  2. Passport Gihub Dev 源码
  3. passport-jwt Gihub Dev 源码
  4. Github测试代码:verify.middleware.ts

这个是我项目测试的代码