NestJs - passport 解读

358 阅读6分钟

认证流程

请查看这篇文章 NestJs - jwt 详细配置步骤

localStrategy 的配置流程跟 jwt 一样,按照流程,就可以快速实现本地策略了。

localStrategy 和 jwtStrategy 的区别:

localStrategy 应用于登录认证流程中,jwtStrategy 应用于资源守护中(比如:接口鉴权)。

在认证流程中,配置是这样子配置,但仔细看,会发现,配置的时候好像没有闭环:

  1. 接口 -> 增加认证守卫 -> 守卫调用了 canActivate

  2. 在 auth 文件里增加了策略的文件(jwt.strategy.ts / local.strategy.ts)-> 策略类里有个 validate 方法 -> validate 方法调用了认证的方法 -> 认证通过返回数据,否则报错

可以看到,操作明显地分为 2 个步骤,但在并没有联系的节点。这让我感到非常的疑惑。

但其实可以推算得到,canActivate 是调用了策略里的 validate 方法去认证。也就是:

接口 -> 增加认证守卫 -> 守卫调用了 canActivate -> 策略类里有个 validate 方法 -> validate 方法调用了认证的方法 -> 认证通过返回数据,否则报错

创建和应用

那么,canActivate 怎么会调用到策略里的 validate 方法呢?在配置过程中,他们没有明显的调用关系。我们来捋一下:

首先我们看一下守卫的创建和应用:

 // 创建守卫 guard 文件
import { AuthGuard } from '@nestjs/passport';
export class LocalAuthGuard extends AuthGuard('local') {
  constructor(private readonly logService: LogService) {
    super();
  }
  context: ExecutionContext;
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    this.context = context;
    return super.canActivate(context);
  }
}

// 应用守卫 controller 文件
@Post('login')
@Public()
@UseGuards(LocalAuthGuard)
async login(
  @Body() reqLoginDto: ReqLoginDto,
  @Req() req: Request,
  ): Promise<ResLoginDto> {
    return await this.loginService.login(req);
}

接下来看一下策略:

import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor(private readonly authService: AuthService) {
    super({
      usernameField: 'username',
      passwordField: 'password',
      passReqToCallback: true, //设置回调函数第一个参数为 request
    });
  }

  async validate(request, username: string, password: string): Promise<any> {
    const body: ReqLoginDto = request.body; // 获取请求体
    await this.authService.checkImgCaptcha(body.uuid, body.code);
    const user = await this.authService.validateUser(username, password);
    return user; //返回值会被 守卫的  handleRequest方法 捕获
  }
}

然后看下 module

import { PassportModule } from '@nestjs/passport';
@Module({
  imports: [UserModule, PassportModule],
  controllers: [],
  providers: [AuthService, LocalStrategy, JwtStrategy],
  exports: [AuthService],
})
export class AuthModule {}

这是关键的 3 个文件,可以看出来 LocalStrategyPassportModule 与守卫没有直接的关联,但它们都从 @nestjs/passport 这个库导出相关的类,所以我们可以猜测它们在 @nestjs/passport 内部进行了关联了。

守卫

我们看下守卫:

AuthGuard 是从 @nestjs/passport 导出的,我们先看这个方法:

// lib\auth.guard.ts
export const AuthGuard: (type?: string | string[]) => Type<IAuthGuard> =
  memoize(createAuthGuard);

这个方法看不出来什么,可以看下 createAuthGuard :

function createAuthGuard(type?: string | string[]): Type<CanActivate> {
  class MixinAuthGuard<TUser = any> implements CanActivate {
    @Optional()
    @Inject(AuthModuleOptions)
    protected options: AuthModuleOptions = {};

    constructor(@Optional() options?: AuthModuleOptions) {
      this.options = options ?? this.options;
      if (!type && !this.options.defaultStrategy) {
        new Logger('AuthGuard').error(NO_STRATEGY_ERROR);
      }
    }

    async canActivate(context: ExecutionContext): Promise<boolean> {
      const options = {
        ...defaultOptions,
        ...this.options,
        ...(await this.getAuthenticateOptions(context))
      };
      const [request, response] = [
        this.getRequest(context),
        this.getResponse(context)
      ];
      const passportFn = createPassportContext(request, response);
      const user = await passportFn(
        type || this.options.defaultStrategy,
        options,
        (err, user, info, status) =>
          this.handleRequest(err, user, info, context, status)
      );
      request[options.property || defaultOptions.property] = user;
      return true;
    }
    // ... 省略其他无关的代码
  }
  const guard = mixin(MixinAuthGuard);
  return guard;
}

主要是看 canActivate 方法里的 createPassportContext 方法:

const createPassportContext =
  (request, response) => (type, options, callback: Function) =>
    new Promise<void>((resolve, reject) =>
      passport.authenticate(type, options, (err, user, info, status) => {
        try {
          request.authInfo = info;
          return resolve(callback(err, user, info, status));
        } catch (err) {
          reject(err);
        }
      })(request, response, (err) => (err ? reject(err) : resolve()))
    );

可以看到,createPassportContext 执行后,会返回一个方法,方法的第一个参数是 type ,这里的 type 其实就是 local 。记住这个方法 passport.authenticate ,它是它们关联的关键点。

接下来顺藤摸瓜,看一下 passport.authenticate :

// lib\authenticator.js
// strategy 就是上面传入的 type,在这里的值是 local
Authenticator.prototype.authenticate = function(strategy, options, callback) {
  return this._framework.authenticate(this, strategy, options, callback);
};

追踪一下 this._framework :

// passsport 
// 1 lib\authenticator.js
function Authenticator() {
  // 省略无关代码
  this._framework = null;
  this.init();
}
// 2 lib\authenticator.js
Authenticator.prototype.init = function() {
  this.framework(require('./framework/connect')());
};
// 3 lib\authenticator.js
Authenticator.prototype.framework = function(fw) {
  this._framework = fw;
  return this;
};
// 4 从 require('./framework/connect')() 继续追踪
var authenticate = require('../middleware/authenticate');
exports = module.exports = function() {
  return {
    initialize: initialize,
    authenticate: authenticate
  };
};
// 5 追踪 '../middleware/authenticate'
module.exports = function authenticate(passport, name, options, callback) {
	// 省略无关代码
  if (!Array.isArray(name)) {
    name = [ name ];
    multi = false;
  }
  return function authenticate(req, res, next) {
    // 省略无关代码
    (function attempt(i) {
      var layer = name[i];
      // If no more strategies exist in the chain, authentication has failed.
      if (!layer) { return allFailed(); }
    
      // Get the strategy, which will be used as prototype from which to create
      // a new instance.  Action functions will then be bound to the strategy
      // within the context of the HTTP request/response pair.
      var strategy, prototype;
      if (typeof layer.authenticate == 'function') {
        strategy = layer;
      } else {
        // 这是关键代码点:将挂载到 passport 的 _strategy 的对应的策略取出来
        prototype = passport._strategy(layer);
        if (!prototype) { return next(new Error('Unknown authentication strategy "' + layer + '"')); }
        
        strategy = Object.create(prototype);
      }
      // 省略无关代码
      strategy.success = function(user, info) {};
      // 省略无关代码
      strategy.fail = function(challenge, status) {};
      // 省略无关代码
      strategy.redirect = function(url, status) {};
      // 省略无关代码
      strategy.pass = function() {};
      // 省略无关代码
      strategy.error = function(err) {};
      // 省略无关代码
      // 这里是 canActivate 最终调用的方法
      // 这里要看对应的 strategy 的 authenticate 方法,localstrategy 会在下面分析
      strategy.authenticate(req, options);
    })(0);
  }
}

在最后一步的时候,根据传入的 name,去寻找对应的 strategy,然后将它初始化:

// 关键代码
var layer = name[i];
var strategy, prototype;
prototype = passport._strategy(layer);
strategy = Object.create(prototype);

接下来得看一下 passport._strategy :

// passsport 
// lib\authenticator.js
// 1
Authenticator.prototype._strategy = function(name) {
  return this._strategies[name];
};
// 2  
function Authenticator() {
  this._key = 'passport';
  this._strategies = {};
}
// 3 
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;
};

我们如果看下 @nestjs/passport 就会发现有个代码跟 Authenticator.prototype.use 使用有点关系:

// @nestjs/passport
const passportInstance = this.getPassportInstance();
if (name) {
	passportInstance.use(name, this);
}
else {
	passportInstance.use(this);
}

this.getPassportInstance 返回的就是 passport :

// @nestjs/passport
import * as passport from 'passport';
function PassportStrategy(Strategy, name) {
    class MixinStrategy extends Strategy {
      constructor(...args) {
        xxxxxxx
      }
      getPassportInstance() {
        return passport;
      }
    }
  }

而恰好:

// passport index.js 将 Authenticator 导出为 Passport
var Passport = require('./authenticator')
exports = module.exports = new Passport();

// passport-local 策略初始化
function Strategy(options, verify) {
  this._usernameField = options.usernameField || 'username';
  this._passwordField = options.passwordField || 'password';
  this._verify = verify;
  this.name = 'local';
  this._passReqToCallback = options.passReqToCallback;
}

所以,在策略那里是调用了 Passport 也就是 Authenticatoruse 方法(passportInstance.use(this)),将本地策略挂到了 this._strategies 上,也就是:

// Strategy 是 passport-local 导出的
this._strategies['local'] = Strategy

小结:

守卫

-> canActivate

-> createAuthGuard

-> createPassportContext

-> passport.authenticate

-> Authenticator.prototype.authenticate

-> this._framework

-> this.init()

-> this.framework(require('./framework/connect')())

-> require('../middleware/authenticate')

-> prototype = passport._strategy(layer)

-> strategy.authenticate(req, options)

prototype = passport._strategy(layer) :

Authenticator.prototype._strategy

-> Authenticator.prototype.use

-> @nestjs/passport: PassportStrategy: constructor

-> this.getPassportInstance()

-> passportInstance.use(this)

-> this._strategies[name] = strategy

-> 挂载到 passport._strategy

strategy.authenticate(req, options) 可以看 passport-local 的分析。

passport-local 分析

从上面的分析可知,守卫的 canActivate 方法,最终会执行到 strategy.authenticate(req, options) ,在这里 strategy 就是 passport-local 导出的 strategy 。那么我们分析一下,将最后一步(调用策略的 validate 方法 )走通。

// 1
export class LocalStrategy extends PassportStrategy(Strategy) {
	// 检验方法
  async validate(request, username: string, password: string): Promise<any>{}
}
// 2 @nestjs/passport
export function PassportStrategy<T extends Type<any> = any>(
  Strategy: T,
  name?: string | undefined,
  callbackArity?: true | number
): {
  new (...args): InstanceType<T>;
} {
  abstract class MixinStrategy extends Strategy {
    abstract validate(...args: any[]): any;

    constructor(...args: any[]) {
      const callback = async (...params: any[]) => {
        try {
          // 这里调用了 validate
          const validateResult = await this.validate(...params);
        } catch (err) {
          done(err, null);
        }
      };
      // callback 方法传入了 Strategy
      super(...args, callback);
    }
  }
  return MixinStrategy;
}
// 3 passport-local
function Strategy(options, verify) {
  if (typeof options == 'function') {
    verify = options;
    options = {};
  }

  this.name = 'local';
  // 这里的 verify 就是上个步骤的 callback
  this._verify = verify;
  this._passReqToCallback = options.passReqToCallback;
}
// 4 passport-local
// 这里就是 canActivate 最终调用的 strategy.authenticate(req, options) 的方法来源
Strategy.prototype.authenticate = function(req, options) {
  options = options || {};
  var self = this;
  function verified(err, user, info) {
    if (err) { return self.error(err); }
    if (!user) { return self.fail(info); }
    self.success(user, info);
  }
  
  try {
    if (self._passReqToCallback) {
      // 这个调用了 _verify 也就是步骤 2 传入的 callback
      this._verify(req, username, password, verified);
    } else {
      // 这个调用了 _verify 也就是步骤 2 传入的 callback
      this._verify(username, password, verified);
    }
  } catch (ex) {
    return self.error(ex);
  }
};

小结:

策略

-> validate

-> callback -> await this.validate(...params) -> super(...args, callback)

-> passport-local: this._verify = verify

-> Strategy.prototype.authenticate (还记得 strategy.authenticate(req, options) 吗?)

-> this._verify(req, username, password, verified) (此处的 verified 就是前面的 validate 方法 )

总结

  1. 策略通过 passport.use (也就是 Authenticator.prototype.use) 将本地策略(项目内的 LocalStrategy )挂到了 passport 的对象 _strategies 上(对应的结构是 this._strategies[name] = strategy);
  2. 守卫通过调用 canActivate 方法,将对应的 name 传下来,通过 prototype = passport._strategy(layer) 匹配对应的 Strategy ;而 canActivate 一步步执行下来,其实是调用了 strategy.authenticate(req, options)
  3. @nestjs/passport PassportStrategyconstructor 里的 callback 方法会调用策略里的 validate 方法;而 callback 方法会放到 PassportStrategy 传入的 Strategy 那里,而 callback 方法会在 Strategy.prototype.authenticate 方法内调用(callbackverify 方法)

他们的关系就是这样子串联起来了。

至此,我们之前的疑惑,就可以解开了。

参考

Authentication

nestjs passport及@nestjs/passport源码级分析(三)

nestjs passport及@nestjs/passport源码级分析(四)

nestjs passport及@nestjs/passport源码级分析(五)

passport

@nestjs/passport

passport-local

OSZAR »