为项目添加一个守卫(脚手架命令)
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 返回 AppControllergetHandler(),方法类型,例如 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进行依赖管理,这样大大提高了权限配置方面的灵活性和开发效率。