NestJs學習之旅(7)——路由守衛

歡迎持續關注NestJs學習之旅系列文章typescript

img

傳統的Web應用中去檢測用戶登陸、權限判斷等等都是在控制器層或者中間件層作的,而在目前比較推薦的模塊化與組件化架構中,不一樣職責的功能建議拆分到不一樣的類文件中去。數據庫

經過前幾篇的學習能夠發現NestJs在這方面作的很好,傳統的express/koa應用中,須要開發者去思考項目結構以及代碼組織,而NestJs不須要你這樣作,下降了開發成本,另外也統一了開發風格。express

路由守衛

熟悉Vue,React的夥伴應該比較熟悉這個概念,通俗的說就是在訪問指定的路由以前回調一個處理函數,若是該函數返回true或者**調用了next()**就會放行當前訪問,不然阻斷當前訪問。json

NestJs中路由守衛也是如此,經過繼承CanActive接口便可定義一個路由守衛。bootstrap

img

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()**裝飾器來注入路由守衛。支持全局守衛、控制器級別守衛、方法級別守衛。

下面以一個實際的例子來演示路由守衛的工做過程。

登陸流程

  1. 用戶輸入帳號密碼後進行登陸,若是登陸成功下發Token
  2. 客戶端在請求頭Authorization中加入第1步下發的Token進行請求
  3. 路由守衛讀取當前請求的Authorization信息並與數據庫的進行比對,若是成功則放行,不然阻斷請求

定義token校驗業務類

// 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在以前的異常處理文章中已經提到過了,這裏再也不贅述。

  • getClass() 獲取當前訪問的Controller對象(不是實例),T爲調用時傳入的具體控制器對象泛型參數
  • getHandler() 獲取當前訪問路由的方法

例如訪問 /user/info 時,getClass()將返回UserController對象(不是實例),getHandler()將返回info()函數的引用。

這個特性有什麼做用呢?

NestJs中可使用反射來獲取定義在方法、屬性、類等等上面的自定義屬性,這一點和Java的註解有點相似。

反射示例——基於角色的權限驗證(RBAC)

定義角色裝飾器

被角色裝飾器裝飾的控制器或者方法在訪問時,路由守衛會讀取當前用戶的角色,與裝飾器傳入的角色相匹配,若是匹配失敗,將阻斷請求,不然將放行請求。

// 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更多的知識,歡迎加羣討論!

20190827145318
相關文章
相關標籤/搜索