NestJS 守衛(wèi)

2023-09-08 11:52 更新

守衛(wèi)是一個(gè)使用 @Injectable() 裝飾器的類。 守衛(wèi)應(yīng)該實(shí)現(xiàn) CanActivate 接口。

10

守衛(wèi)有一個(gè)單獨(dú)的責(zé)任。它們根據(jù)運(yùn)行時(shí)出現(xiàn)的某些條件(例如權(quán)限,角色,訪問(wèn)控制列表等)來(lái)確定給定的請(qǐng)求是否由路由處理程序處理。 這通常稱為授權(quán)。在傳統(tǒng)的 Express 應(yīng)用程序中,通常由中間件處理授權(quán)。中間件是身份驗(yàn)證的良好選擇。到目前為止,訪問(wèn)限制邏輯大多在中間件內(nèi)。這樣很好,因?yàn)橹T如 token 驗(yàn)證或?qū)?nbsp;request 對(duì)象附加屬性與特定路由沒(méi)有強(qiáng)關(guān)聯(lián)。

中間件不知道調(diào)用 next() 函數(shù)后會(huì)執(zhí)行哪個(gè)處理程序。另一方面,守衛(wèi)可以訪問(wèn) ExecutionContext 實(shí)例,因此確切地知道接下來(lái)要執(zhí)行什么。它們的設(shè)計(jì)與異常過(guò)濾器、管道和攔截器非常相似,目的是讓您在請(qǐng)求/響應(yīng)周期的正確位置插入處理邏輯,并以聲明的方式進(jìn)行插入。這有助于保持代碼的簡(jiǎn)潔和聲明性。

守衛(wèi)在每個(gè)中間件之后執(zhí)行,但在任何攔截器或管道之前執(zhí)行。

授權(quán)守衛(wèi)

正如前面提到的,授權(quán)是保護(hù)的一個(gè)很好的用例,因?yàn)橹挥挟?dāng)調(diào)用者(通常是經(jīng)過(guò)身份驗(yàn)證的特定用戶)具有足夠的權(quán)限時(shí),特定的路由才可用。我們現(xiàn)在要構(gòu)建的 AuthGuard 假設(shè)用戶是經(jīng)過(guò)身份驗(yàn)證的(因此,請(qǐng)求頭附加了一個(gè)token)。它將提取和驗(yàn)證token,并使用提取的信息來(lái)確定請(qǐng)求是否可以繼續(xù)。

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

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest();
    return validateRequest(request);
  }
}

validateRequest() 函數(shù)中的邏輯可以根據(jù)需要變得簡(jiǎn)單或復(fù)雜。本例的主要目的是說(shuō)明保護(hù)如何適應(yīng)請(qǐng)求/響應(yīng)周期。

每個(gè)守衛(wèi)必須實(shí)現(xiàn)一個(gè)canActivate()函數(shù)。此函數(shù)應(yīng)該返回一個(gè)布爾值,指示是否允許當(dāng)前請(qǐng)求。它可以同步或異步地返回響應(yīng)(通過(guò) Promise 或 Observable)。Nest使用返回值來(lái)控制下一個(gè)行為:

  • 如果返回 true, 將處理用戶調(diào)用。
  • 如果返回 false, 則 Nest 將忽略當(dāng)前處理的請(qǐng)求。

執(zhí)行上下文

canActivate() 函數(shù)接收單個(gè)參數(shù) ExecutionContext 實(shí)例。ExecutionContext 繼承自 ArgumentsHost 。ArgumentsHost 是傳遞給原始處理程序的參數(shù)的包裝器,在上面的示例中,我們只是使用了之前在 ArgumentsHost上定義的幫助器方法來(lái)獲得對(duì)請(qǐng)求對(duì)象的引用。有關(guān)此主題的更多信息。你可以在這里了解到更多(在異常過(guò)濾器章節(jié))。

ExecutionContext 提供了更多功能,它擴(kuò)展了 ArgumentsHost,但是也提供了有關(guān)當(dāng)前執(zhí)行過(guò)程的更多詳細(xì)信息。

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

getHandler()方法返回對(duì)將要調(diào)用的處理程序的引用。getClass()方法返回這個(gè)特定處理程序所屬的 Controller 類的類型。例如,如果當(dāng)前處理的請(qǐng)求是 POST 請(qǐng)求,目標(biāo)是 CatsController上的 create() 方法,那么 getHandler() 將返回對(duì) create() 方法的引用,而 getClass()將返回一個(gè)CatsControllertype(而不是實(shí)例)。

基于角色認(rèn)證

一個(gè)更詳細(xì)的例子是一個(gè) RolesGuard 。這個(gè)守衛(wèi)只允許具有特定角色的用戶訪問(wèn)。我們將從一個(gè)基本模板開(kāi)始,并在接下來(lái)的部分中構(gòu)建它。目前,它允許所有請(qǐng)求繼續(xù):

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

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

綁定守衛(wèi)

與管道和異常過(guò)濾器一樣,守衛(wèi)可以是控制范圍的、方法范圍的或全局范圍的。下面,我們使用 @UseGuards()裝飾器設(shè)置了一個(gè)控制范圍的守衛(wèi)。這個(gè)裝飾器可以使用單個(gè)參數(shù),也可以使用逗號(hào)分隔的參數(shù)列表。也就是說(shuō),你可以傳遞幾個(gè)守衛(wèi)并用逗號(hào)分隔它們。

@Controller('cats')
@UseGuards(RolesGuard)
export class CatsController {}

@UseGuards() 裝飾器需要從 @nestjs/common 包導(dǎo)入。

上例,我們已經(jīng)傳遞了 RolesGuard 類型而不是實(shí)例, 讓框架進(jìn)行實(shí)例化,并啟用了依賴項(xiàng)注入。與管道和異常過(guò)濾器一樣,我們也可以傳遞一個(gè)實(shí)例:

@Controller('cats')
@UseGuards(new RolesGuard())
export class CatsController {}

上面的構(gòu)造將守衛(wèi)附加到此控制器聲明的每個(gè)處理程序。如果我們決定只限制其中一個(gè), 我們只需要在方法級(jí)別設(shè)置守衛(wèi)。為了綁定全局守衛(wèi), 我們使用 Nest 應(yīng)用程序?qū)嵗?nbsp;useGlobalGuards() 方法:

為了設(shè)置一個(gè)全局守衛(wèi),使用Nest應(yīng)用程序?qū)嵗?nbsp;useGlobalGuards() 方法:

const app = await NestFactory.create(AppModule);
app.useGlobalGuards(new RolesGuard());

對(duì)于混合應(yīng)用程序,useGlobalGuards() 方法不會(huì)為網(wǎng)關(guān)和微服務(wù)設(shè)置守衛(wèi)。對(duì)于“標(biāo)準(zhǔn)”(非混合)微服務(wù)應(yīng)用程序,useGlobalGuards()在全局安裝守衛(wèi)。

全局守衛(wèi)用于整個(gè)應(yīng)用程序, 每個(gè)控制器和每個(gè)路由處理程序。在依賴注入方面, 從任何模塊外部注冊(cè)的全局守衛(wèi) (如上面的示例中所示) 不能插入依賴項(xiàng), 因?yàn)樗鼈儾粚儆谌魏文K。為了解決此問(wèn)題, 您可以使用以下構(gòu)造直接從任何模塊設(shè)置一個(gè)守衛(wèi):

app.module.ts
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';

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

當(dāng)使用此方法為守衛(wèi)程序執(zhí)行依賴項(xiàng)注入時(shí),請(qǐng)注意,無(wú)論使用此構(gòu)造的模塊是什么,守衛(wèi)程序?qū)嶋H上是全局的。應(yīng)該在哪里進(jìn)行?選擇定義守衛(wèi)的模塊(上例中的 RolesGuard)。此外,useClass不是處理自定義 providers 注冊(cè)的唯一方法。在這里了解更多。

反射器

守衛(wèi)現(xiàn)在在正常工作,但還不是很智能。我們?nèi)匀粵](méi)有利用最重要的守衛(wèi)的特征,即執(zhí)行上下文。它還不知道角色,或者每個(gè)處理程序允許哪些角色。例如,CatsController 可以為不同的路由提供不同的權(quán)限方案。其中一些可能只對(duì)管理用戶可用,而另一些則可以對(duì)所有人開(kāi)放。我們?nèi)绾我造`活和可重用的方式將角色與路由匹配起來(lái)?

這就是自定義元數(shù)據(jù)發(fā)揮作用的地方。Nest提供了通過(guò) @SetMetadata() 裝飾器將定制元數(shù)據(jù)附加到路由處理程序的能力。這些元數(shù)據(jù)提供了我們所缺少的角色數(shù)據(jù),而守衛(wèi)需要這些數(shù)據(jù)來(lái)做出決策。讓我們看看使用@SetMetadata():

cats.controller.ts
@Post()
@SetMetadata('roles', ['admin'])
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

@SetMetadata() 裝飾器需要從 @nestjs/common 包導(dǎo)入。

通過(guò)上面的構(gòu)建,我們將 roles 元數(shù)據(jù)(roles 是一個(gè)鍵,而 ['admin'] 是一個(gè)特定的值)附加到 create() 方法。 直接使用 @SetMetadata() 并不是一個(gè)好習(xí)慣。 相反,你應(yīng)該創(chuàng)建你自己的裝飾器。

roles.decorator.ts
import { SetMetadata } from '@nestjs/common';

export const Roles = (...roles: string[]) => SetMetadata('roles', roles);

這種方法更簡(jiǎn)潔、更易讀,而且是強(qiáng)類型的?,F(xiàn)在我們有了一個(gè)自定義的 @Roles() 裝飾器,我們可以使用它來(lái)裝飾 create()方法。

cats.controller.ts
@Post()
@Roles('admin')
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

讓我們?cè)俅位氐?nbsp;RolesGuard 。 它只是在所有情況下返回 true,到目前為止允許請(qǐng)求繼續(xù)。我們希望根據(jù)分配給當(dāng)前用戶的角色與正在處理的當(dāng)前路由所需的實(shí)際角色之間的比較來(lái)設(shè)置返回值的條件。 為了訪問(wèn)路由的角色(自定義元數(shù)據(jù)),我們將使用在 @nestjs/core 中提供的 Reflector 幫助類。

roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const roles = this.reflector.get<string[]>('roles', context.getHandler());
    if (!roles) {
      return true;
    }
    const request = context.switchToHttp().getRequest();
    const user = request.user;
    return matchRoles(roles, user.roles);
  }
}

在 node.js 世界中,將授權(quán)用戶附加到 request 對(duì)象是一種常見(jiàn)的做法。 因此,在上面的示例代碼中。我們假設(shè) request.user 包含用戶實(shí)例和允許的角色。 在您的應(yīng)用中,您可能會(huì)在自定義身份驗(yàn)證(或中間件)中建立該關(guān)聯(lián)。

matchRoles() 函數(shù)內(nèi)部的邏輯可以根據(jù)需要簡(jiǎn)單或復(fù)雜。該示例的重點(diǎn)是顯示防護(hù)如何適應(yīng)請(qǐng)求/響應(yīng)周期。

現(xiàn)在,當(dāng)用戶嘗試在沒(méi)有足夠權(quán)限的情況下調(diào)用 /cats POST端點(diǎn)時(shí),Nest 會(huì)自動(dòng)返回以下響應(yīng):

有關(guān)以上下文相關(guān)方式進(jìn)行利用的更多詳細(xì)信息,請(qǐng)參見(jiàn)“ 執(zhí)行”上下文章節(jié)的“ 反射和元數(shù)據(jù)”部分。

當(dāng)特權(quán)不足的用戶請(qǐng)求端點(diǎn)時(shí),Nest自動(dòng)返回以下響應(yīng):

{
  "statusCode": 403,
  "message": "Forbidden resource"
}

實(shí)際上,返回 false 的守衛(wèi)會(huì)拋出一個(gè) HttpException 異常。如果您想要向最終用戶返回不同的錯(cuò)誤響應(yīng),你應(yīng)該拋出一個(gè)異常。

throw new UnauthorizedException();

由守衛(wèi)引發(fā)的任何異常都將由異常層(全局異常過(guò)濾器和應(yīng)用于當(dāng)前上下文的任何異常過(guò)濾器)處理。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)