深入了解Nest的守卫

2,874 阅读5分钟

为项目添加一个守卫(脚手架命令)

nest g gu auth

守卫源代码分析

通过上面的命令,我们创建一个守卫对象:

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    return true;
  }
}

守卫有两个关键点:@Injectable()装饰器,这个我们已经不在陌生,其功能就是自身的依赖与被依赖关系交由Nest托管;CanActivate接口,对应找到相应的源代码,如下:

export interface CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean>;
}

ExecutionContext是当前请求的上下文,这个对象先前在过滤器中也曾有类似的接口被使用到(ArgumentsHost),有不了解的同学可以详细看《过滤器》节。ExecutionContext接口扩展了ArgumentsHost接口:

export interface ExecutionContext extends ArgumentsHost {
  getClass<T = any>(): Type<T>;
  getHandler(): Function;
}

ExecutionContext接口分别被用于守卫的canActiviate()方法和拦截器(后文)的intercept()方法。相比较ArgumentsHost接口,被扩展出来的两个方法:

  • getClass(),类(控制器)类型,例如 getClass().name 返回 AppController
  • getHandler() ,方法类型,例如 getHandler().name 返回 getHello

这些信息正是用了装饰器后,通过@SetMetadata()被写入对象的元数据中。

绑定守卫

这个过程与异常过滤器较为类似,通过装饰器UseGuards(),对类或者方法装饰过程将某个守卫指派给一组路由。也可以通过将守卫注册到module中,作为全局使用。先了解一下绑定守卫装饰器源代码:

export function UseGuards(
  ...guards: (CanActivate | Function)[]
): MethodDecorator & ClassDecorator {
  return (
    target: any,
    key?: string | symbol,
    descriptor?: TypedPropertyDescriptor<any>,
  ) => {
    const isGuardValid = <T extends Function | Record<string, any>>(guard: T) =>
      guard &&
      (isFunction(guard) ||
        isFunction((guard as Record<string, any>).canActivate));

    if (descriptor) {
      validateEach(
        target.constructor,
        guards,
        isGuardValid,
        '@UseGuards',
        'guard',
      );
      extendArrayMetadata(GUARDS_METADATA, guards, descriptor.value);
      return descriptor;
    }
    validateEach(target, guards, isGuardValid, '@UseGuards', 'guard');
    extendArrayMetadata(GUARDS_METADATA, guards, target);
    return target;
  };
}

装饰器的源代码逻辑与异常过滤器较为类似,同样是验证有效性后作为源数据加入到类数据中。继续先前的守卫代码,我们稍微增加一些守卫的业务过程:

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    switch (context.getType()) {
      case 'http':
        const [request, response, next] = context.getArgs();
        console.log(
          `${request.protocol}://${request.hostname}${
            request.path
          }${JSON.stringify(request.query)} => ${context.getClass().name}.${
            context.getHandler().name
          }`,
        );
        break;
      case 'ws':
        break;
      case 'rpc':
        break;
    }
    return true;
  }
}

再将这个守卫绑定到AppController上:

import { Controller, Get, UseGuards } from '@nestjs/common';
import { AppService } from './app.service';
import { AuthGuard } from './auth.guard';

@Controller()
@UseGuards(AuthGuard)
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }

  @Get('/a')
  getA(): string {
    return 'A';
  }
}

启动程序,用crul试图访问服务,见程序控制台有如下输出:

http://localhost/{} => AppController.getHello

http://localhost/a{} => AppController.getA

围绕Nest提供的这些功能就可以实现对部分内容进行保护的业务逻辑操作。

全局守卫

再任意一个模块中注册守卫,全部控制器(全局)都会经过这个守卫逻辑。

import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';

@Module({
  providers: [
    {
      provide: APP_GUARD,
      useClass: AuthGuard,
    },
  ],
})
export class AppModule {}

另外一种是在main.ts中设置

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { UnauthorizedFilter } from './unauthorized.filter';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalGuards(new AuthGuard());
  await app.listen(3000);
}
bootstrap();

两者实际效果相同,差别在于前者的依赖关系由Nest来管理,后者不是。

多个守卫的逻辑

通过上面的源代码我们看到,守卫装饰器是可以只是输入多个守卫类的,例如@UseGuards(Guard1,Guard2 ...),很显然,他们的执行顺序是视输入的顺序而定,其中任何一个守卫返回false,接下去的过程都会终止执行。请求过程返回403的状态码。

尝试将守卫分别以全局、类和方法的三种方式进行绑定,观察执行过程的先后顺序。其过程将是先执行全局,然后是控制器,最后是方法。(有兴趣的同学可以克隆源码观察这个过程,这里不再赘述)

装饰器与元信息

关于守卫的概念,其实到上面一个小节已经讲完了,这篇文章的字数较少,为了上首页推送这里就多讲一些扩展内容。上文提到守卫的上下文对象扩展了ArgumentHost接口,扩展出来getHandler()getClass()主要是针对通过装饰器设置相关的元数据来判定权限方面的操作。假如某个方法需要特别权限:

  @Get('/a')
  @SetMetadata('User-Agent', ['chrome','firefox'])
  @UseGuards(Auth2Guard)
  getA(): string {
    return 'A';
  }

SetMetadata()是Nest框架中设置函数、类、参数对象的元数据的方法。相关概念可以参考这篇文章。它提供一个输入key value的绑定到函数、类和参数对象上,在需要时进行调取使用。以后在介绍自定义装饰器的时候再详细说。以上代码意思就是给getA方法绑定了一个User-Agent属性,其值为一个数组。然后我们稍微修改一下Auth2Guard

@Injectable()
export class Auth2Guard implements CanActivate {
  constructor(private reflector: Reflector) {} // 依赖注入

  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const [request, response, next] = context.getArgs();
    return (
      this.reflector
        .get<string[]>('user-agent', context.getHandler())
        .findIndex(
          (_r) =>
            request.headers['user-agent']
              .toLowerCase()
              .indexOf(_r.toLowerCase()) > -1,
        ) > -1
    );
  }
}

注意第三行(是原先没有的),需要添加Reflector的注入。在第10行中,将user-agent字段值给取出来,并加以判断。我们可以分别用chrome或firefox浏览器打开,再以命令方式访问http://localhost:3000/a来体会守卫的作用。一般情况下,自定义的元数据是通过自定义装饰器,以静态(或Hardcode)或动态根据配置文件写入到相关的方法中的。这方面内容有兴趣的同学请期待下一篇文章。

结合先前的Provider章节,控制器(类)中的权限配置可以通过配置文件甚至数据库(类)的方式注入,委托给Nest进行依赖管理,这样大大提高了权限配置方面的灵活性和开发效率。