认证流程
请查看这篇文章 NestJs - jwt 详细配置步骤 。
localStrategy 的配置流程跟 jwt 一样,按照流程,就可以快速实现本地策略了。
localStrategy 和 jwtStrategy 的区别:
localStrategy 应用于登录认证流程中,jwtStrategy 应用于资源守护中(比如:接口鉴权)。
在认证流程中,配置是这样子配置,但仔细看,会发现,配置的时候好像没有闭环:
-
接口 -> 增加认证守卫 -> 守卫调用了
canActivate
-
在 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 个文件,可以看出来 LocalStrategy
和 PassportModule
与守卫没有直接的关联,但它们都从 @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
也就是 Authenticator
的 use
方法(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
方法 )
总结
- 策略通过
passport.use
(也就是Authenticator.prototype.use
) 将本地策略(项目内的LocalStrategy
)挂到了passport
的对象_strategies
上(对应的结构是this._strategies[name] = strategy
); - 守卫通过调用
canActivate
方法,将对应的name
传下来,通过prototype = passport._strategy(layer)
匹配对应的Strategy
;而canActivate
一步步执行下来,其实是调用了strategy.authenticate(req, options)
。 @nestjs/passport
的PassportStrategy
的constructor
里的callback
方法会调用策略里的validate
方法;而callback
方法会放到PassportStrategy
传入的Strategy
那里,而callback
方法会在Strategy.prototype.authenticate
方法内调用(callback
即verify
方法)
他们的关系就是这样子串联起来了。
至此,我们之前的疑惑,就可以解开了。
参考
nestjs passport及@nestjs/passport源码级分析(三)
nestjs passport及@nestjs/passport源码级分析(四)