守衛(wèi)是一個使用 @Injectable() 裝飾器的類。 守衛(wèi)應(yīng)該實現(xiàn) CanActivate 接口。
守衛(wèi)有一個單獨的責(zé)任。它們根據(jù)運行時出現(xiàn)的某些條件(例如權(quán)限,角色,訪問控制列表等)來確定給定的請求是否由路由處理程序處理。 這通常稱為授權(quán)。在傳統(tǒng)的 Express 應(yīng)用程序中,通常由中間件處理授權(quán)。中間件是身份驗證的良好選擇。到目前為止,訪問限制邏輯大多在中間件內(nèi)。這樣很好,因為諸如 token 驗證或?qū)?nbsp;request 對象附加屬性與特定路由沒有強關(guān)聯(lián)。
中間件不知道調(diào)用 next() 函數(shù)后會執(zhí)行哪個處理程序。另一方面,守衛(wèi)可以訪問 ExecutionContext 實例,因此確切地知道接下來要執(zhí)行什么。它們的設(shè)計與異常過濾器、管道和攔截器非常相似,目的是讓您在請求/響應(yīng)周期的正確位置插入處理邏輯,并以聲明的方式進(jìn)行插入。這有助于保持代碼的簡潔和聲明性。
守衛(wèi)在每個中間件之后執(zhí)行,但在任何攔截器或管道之前執(zhí)行。
正如前面提到的,授權(quán)是保護的一個很好的用例,因為只有當(dāng)調(diào)用者(通常是經(jīng)過身份驗證的特定用戶)具有足夠的權(quán)限時,特定的路由才可用。我們現(xiàn)在要構(gòu)建的 AuthGuard 假設(shè)用戶是經(jīng)過身份驗證的(因此,請求頭附加了一個token)。它將提取和驗證token,并使用提取的信息來確定請求是否可以繼續(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ù)需要變得簡單或復(fù)雜。本例的主要目的是說明保護如何適應(yīng)請求/響應(yīng)周期。
每個守衛(wèi)必須實現(xiàn)一個canActivate()函數(shù)。此函數(shù)應(yīng)該返回一個布爾值,指示是否允許當(dāng)前請求。它可以同步或異步地返回響應(yīng)(通過 Promise 或 Observable)。Nest使用返回值來控制下一個行為:
canActivate() 函數(shù)接收單個參數(shù) ExecutionContext 實例。ExecutionContext 繼承自 ArgumentsHost 。ArgumentsHost 是傳遞給原始處理程序的參數(shù)的包裝器,在上面的示例中,我們只是使用了之前在 ArgumentsHost上定義的幫助器方法來獲得對請求對象的引用。有關(guān)此主題的更多信息。你可以在這里了解到更多(在異常過濾器章節(jié))。
ExecutionContext 提供了更多功能,它擴展了 ArgumentsHost,但是也提供了有關(guān)當(dāng)前執(zhí)行過程的更多詳細(xì)信息。
export interface ExecutionContext extends ArgumentsHost {
getClass<T = any>(): Type<T>;
getHandler(): Function;
}
getHandler()方法返回對將要調(diào)用的處理程序的引用。getClass()方法返回這個特定處理程序所屬的 Controller 類的類型。例如,如果當(dāng)前處理的請求是 POST 請求,目標(biāo)是 CatsController上的 create() 方法,那么 getHandler() 將返回對 create() 方法的引用,而 getClass()將返回一個CatsControllertype(而不是實例)。
一個更詳細(xì)的例子是一個 RolesGuard 。這個守衛(wèi)只允許具有特定角色的用戶訪問。我們將從一個基本模板開始,并在接下來的部分中構(gòu)建它。目前,它允許所有請求繼續(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)可以是控制范圍的、方法范圍的或全局范圍的。下面,我們使用 @UseGuards()裝飾器設(shè)置了一個控制范圍的守衛(wèi)。這個裝飾器可以使用單個參數(shù),也可以使用逗號分隔的參數(shù)列表。也就是說,你可以傳遞幾個守衛(wèi)并用逗號分隔它們。
@Controller('cats')
@UseGuards(RolesGuard)
export class CatsController {}
@UseGuards() 裝飾器需要從 @nestjs/common 包導(dǎo)入。
上例,我們已經(jīng)傳遞了 RolesGuard 類型而不是實例, 讓框架進(jìn)行實例化,并啟用了依賴項注入。與管道和異常過濾器一樣,我們也可以傳遞一個實例:
@Controller('cats')
@UseGuards(new RolesGuard())
export class CatsController {}
上面的構(gòu)造將守衛(wèi)附加到此控制器聲明的每個處理程序。如果我們決定只限制其中一個, 我們只需要在方法級別設(shè)置守衛(wèi)。為了綁定全局守衛(wèi), 我們使用 Nest 應(yīng)用程序?qū)嵗?nbsp;useGlobalGuards() 方法:
為了設(shè)置一個全局守衛(wèi),使用Nest應(yīng)用程序?qū)嵗?nbsp;useGlobalGuards() 方法:
const app = await NestFactory.create(AppModule);
app.useGlobalGuards(new RolesGuard());
對于混合應(yīng)用程序,useGlobalGuards() 方法不會為網(wǎng)關(guān)和微服務(wù)設(shè)置守衛(wèi)。對于“標(biāo)準(zhǔn)”(非混合)微服務(wù)應(yīng)用程序,useGlobalGuards()在全局安裝守衛(wèi)。
全局守衛(wèi)用于整個應(yīng)用程序, 每個控制器和每個路由處理程序。在依賴注入方面, 從任何模塊外部注冊的全局守衛(wèi) (如上面的示例中所示) 不能插入依賴項, 因為它們不屬于任何模塊。為了解決此問題, 您可以使用以下構(gòu)造直接從任何模塊設(shè)置一個守衛(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í)行依賴項注入時,請注意,無論使用此構(gòu)造的模塊是什么,守衛(wèi)程序?qū)嶋H上是全局的。應(yīng)該在哪里進(jìn)行?選擇定義守衛(wèi)的模塊(上例中的 RolesGuard)。此外,useClass不是處理自定義 providers 注冊的唯一方法。在這里了解更多。
守衛(wèi)現(xiàn)在在正常工作,但還不是很智能。我們?nèi)匀粵]有利用最重要的守衛(wèi)的特征,即執(zhí)行上下文。它還不知道角色,或者每個處理程序允許哪些角色。例如,CatsController 可以為不同的路由提供不同的權(quán)限方案。其中一些可能只對管理用戶可用,而另一些則可以對所有人開放。我們?nèi)绾我造`活和可重用的方式將角色與路由匹配起來?
這就是自定義元數(shù)據(jù)發(fā)揮作用的地方。Nest提供了通過 @SetMetadata() 裝飾器將定制元數(shù)據(jù)附加到路由處理程序的能力。這些元數(shù)據(jù)提供了我們所缺少的角色數(shù)據(jù),而守衛(wèi)需要這些數(shù)據(jù)來做出決策。讓我們看看使用@SetMetadata():
cats.controller.ts
@Post()
@SetMetadata('roles', ['admin'])
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
@SetMetadata() 裝飾器需要從 @nestjs/common 包導(dǎo)入。
通過上面的構(gòu)建,我們將 roles 元數(shù)據(jù)(roles 是一個鍵,而 ['admin'] 是一個特定的值)附加到 create() 方法。 直接使用 @SetMetadata() 并不是一個好習(xí)慣。 相反,你應(yīng)該創(chuàng)建你自己的裝飾器。
roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
這種方法更簡潔、更易讀,而且是強類型的?,F(xiàn)在我們有了一個自定義的 @Roles() 裝飾器,我們可以使用它來裝飾 create()方法。
cats.controller.ts
@Post()
@Roles('admin')
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
讓我們再次回到 RolesGuard 。 它只是在所有情況下返回 true,到目前為止允許請求繼續(xù)。我們希望根據(jù)分配給當(dāng)前用戶的角色與正在處理的當(dāng)前路由所需的實際角色之間的比較來設(shè)置返回值的條件。 為了訪問路由的角色(自定義元數(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 對象是一種常見的做法。 因此,在上面的示例代碼中。我們假設(shè) request.user 包含用戶實例和允許的角色。 在您的應(yīng)用中,您可能會在自定義身份驗證(或中間件)中建立該關(guān)聯(lián)。
matchRoles() 函數(shù)內(nèi)部的邏輯可以根據(jù)需要簡單或復(fù)雜。該示例的重點是顯示防護如何適應(yīng)請求/響應(yīng)周期。
現(xiàn)在,當(dāng)用戶嘗試在沒有足夠權(quán)限的情況下調(diào)用 /cats POST端點時,Nest 會自動返回以下響應(yīng):
有關(guān)以上下文相關(guān)方式進(jìn)行利用的更多詳細(xì)信息,請參見“ 執(zhí)行”上下文章節(jié)的“ 反射和元數(shù)據(jù)”部分。
當(dāng)特權(quán)不足的用戶請求端點時,Nest自動返回以下響應(yīng):
{
"statusCode": 403,
"message": "Forbidden resource"
}
實際上,返回 false 的守衛(wèi)會拋出一個 HttpException 異常。如果您想要向最終用戶返回不同的錯誤響應(yīng),你應(yīng)該拋出一個異常。
throw new UnauthorizedException();
由守衛(wèi)引發(fā)的任何異常都將由異常層(全局異常過濾器和應(yīng)用于當(dāng)前上下文的任何異常過濾器)處理。
更多建議: