欢迎持续关注NestJs学习之旅系列文章typescript
传统的Web应用中去检测用户登陆、权限判断等等都是在控制器层或者中间件层作的,而在目前比较推荐的模块化与组件化架构中,不一样职责的功能建议拆分到不一样的类文件中去。数据库
经过前几篇的学习能够发现NestJs在这方面作的很好,传统的express/koa应用中,须要开发者去思考项目结构以及代码组织,而NestJs不须要你这样作,下降了开发成本,另外也统一了开发风格。express
熟悉Vue,React的伙伴应该比较熟悉这个概念,通俗的说就是在访问指定的路由以前回调一个处理函数,若是该函数返回true或者**调用了next()**就会放行当前访问,不然阻断当前访问。json
NestJs中路由守卫也是如此,经过继承CanActive接口便可定义一个路由守卫。bootstrap
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
class AppGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
return true;
}
}
复制代码
路由守卫本质上也是中间件的一种,koa或者express开发中接口鉴权就是基于中间件开发的,若是当前请求是不被容许的,当前中间件将不会调用后续中间件,达到阻断请求的目的。架构
可是中间件的职责是不明确的,中间件能够干任何事(数据校验,格式转化,响应体压缩等等),这致使只能经过名称来识别中间件,项目迭代比较久之后,有比较高的维护成本。app
因为单一职责的关系,路由守卫只能返回true和false来决定放行/阻断当前请求,不能够修改request/response对象,由于一旦破坏单一职责的原则,排查问题比较麻烦。框架
若是须要修改request对象,能够结合中间件一块儿使用。koa
路由守卫在全部中间件执行完毕以后开始执行。async
如下是一个结合路由守卫和中间件的例子。
// auth.middleware.ts
// 中间件职责:读取请求头Authorization,若是存在且有效的话,设置user对象到request中
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response } from 'express';
@Injectable()
export class AuthMiddleware implements NestMiddleware<Request|any, Response> {
constructor(private readonly userService: UserService) {}
async use(req: Request|any, res: Response, next: Function) {
const token = req.header('authorization');
if(!token) {
next();
return;
}
const user = await this.userService.getUserByToken(token);
if(!user) {
next();
return;
}
request.user = user;
next();
}
}
复制代码
// user.guard.ts
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';
import { Request } from 'express';
@Injectable()
export class UserGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest<Request | any>();
// 直接检测是否有user对象,由于无user对象证实无token或者token无效
return !!request.user;
}
}
复制代码
以上例子是笔者经常使用的一种方法,这样职责比较清晰,并且user对象能够在其余中间件中读取。
NestJs使用**@UseGuards()**装饰器来注入路由守卫。支持全局守卫、控制器级别守卫、方法级别守卫。
下面以一个实际的例子来演示路由守卫的工做过程。
// user.service.ts
@Injetable()
export class UserService {
// 模拟校验,这里直接返回true,实际开发中自行实现便可
validateToken(token: string) {
return true;
}
}
复制代码
// user.guard.ts
@Injetable()
export class UserGuard implements CanActive {
constructor(private readonly userService: UserService) {}
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest<Request>();
// 读取token
const authorization = request.header('authorization');
if (!authorization) {
return false;
}
return this.userService.validateToken(authorization);
}
}
复制代码
@Controller('user')
export class UserController {
// 请求登陆
@Post('login')
login() {
return {token:'fake_token'}; // 直接下发token,真实场景下须要验证帐号密码
}
// 查看当前用户信息
@Get('info')
@UseGuards(UserGuard) // 方法级路由守卫
info() {
return {username: 'fake_user'};
}
}
复制代码
一个完整的路由守卫应用实例就已经出来了,虽然我们的路由守卫没啥逻辑都是直接放行的,可是实际开发中也是基于这种思路去开发的,只不过校验的逻辑不同罢了。
该级别会对被装饰控制器的全部路由方法生效。
@Controller('user')
@UseGuards(UserGuard)
export class UserController {
// 查看当前用户信息
@Get('info')
info() {
return {username: 'fake_user'};
}
}
复制代码
该级别只对被装饰的方法生效。
@Get('info')
@UseGuards(UserGuard)
info() {
return {username: 'fake_user'};
}
复制代码
与全局异常过滤器相似,该级别对全部控制器的全部路由方法生效。该方法与全局异常过滤器同样不会对WebSocket和GRPC生效。
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 因为main.ts启动时并未初始化依赖注入容器,因此依赖必须手动传入,通常状况下不建议使用全局守卫,由于依赖注入得本身解决。
app.useGlobalGuards(new UserGuard(new UserService()));
await app.listen(3000);
}
bootstrap();
复制代码
CanActive接口的方法中有一个ExecutionContext对象,该对象为请求上下文对象,该对象定义以下:
export interface ExecutionContext extends ArgumentsHost {
getClass<T = any>(): Type<T>;
getHandler(): Function;
}
复制代码
能够看到继承了ArgumentHost,ArgumentHost在以前的异常处理文章中已经提到过了,这里再也不赘述。
例如访问 /user/info 时,getClass()将返回UserController对象(不是实例),getHandler()将返回info()函数的引用。
这个特性有什么做用呢?
NestJs中可使用反射来获取定义在方法、属性、类等等上面的自定义属性,这一点和Java的注解有点相似。
被角色装饰器装饰的控制器或者方法在访问时,路由守卫会读取当前用户的角色,与装饰器传入的角色相匹配,若是匹配失败,将阻断请求,不然将放行请求。
// roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
复制代码
假设咱们有一个只容许管理员访问的建立用户的接口:
@Post('create')
@Roles('admin')
async create(@Body() createUserDTO: CreateUserDTO) {
this.userService.create(createUserDTO);
}
复制代码
// role.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';
import { Reflector } from '@nestjs/core';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
// 获取roles元数据,roles与roles.decorator.ts中SetMetadata()第一个参数一致
const roles = this.reflector.get<string[]>('roles', context.getHandler());
if (!roles) { // 未被装饰器装饰,直接放行
return true;
}
const request = context.switchToHttp().getRequest();
const user = request.user; // 读取请求对象的user,该user对象能够经过中间件来设置(本文前面有例子)
const hasRole = () => user.roles.some((role) => roles.includes(role));
return user && user.roles && hasRole();
}
}
复制代码
以上就是读取自定义装饰器数据开发RBAC的例子,写的比较简陋,可是原理是同样的,代码量少的话便于理解核心。
路由守卫返回false时框架会抛出ForbiddenException,客户端收到的默认响应以下:
{
"statusCode": 403,
"message": "Forbidden resource"
}
复制代码
若是须要抛出其余异常,好比UnauthorizedException,能够直接在路由守卫的canActive()方法中抛出。
另外,在这里抛出的异常时能够被异常过滤器捕获而且处理的,因此咱们能够自定义异常类型以及输出自定义响应数据。
本文除了路由守卫以外另外一个重要的知识是【自定义元数据装饰器】的使用,基于该装饰器能够开发不少使人惊艳的功能,这个就看各位看官的实现了。
若是您以为有所收获,分享给更多须要的朋友,谢谢!
若是您想交流关于NestJs更多的知识,欢迎加群讨论!