Na programação, um guard é um padrão comum de controle de fluxo de programa. Envolve a realização de uma verificação condicional para determinar se a execução do código deve continuar ou sair. O uso de guards pode melhorar a legibilidade e manutenção do seu código.
Os guards no NestJS funcionam de maneira semelhante à programação geral, mas sua implementação está intimamente integrada ao framework NestJS. A responsabilidade dos guards é permitir ou negar o acesso aos endpoints do NestJS. Por exemplo, podemos criar um guard para proteger uma rota que atualiza o perfil de um usuário, para ser acessível apenas por um administrador.
Tabela de conteúdo:
- Guards no ciclo de vida do NestJS
- Criando um AuthGuard
- Usando guards para proteger rotas individuais
- Usando guards para proteger controladores
- Criando um guard de função usando metadados
- Usando role guards com o decorator Roles
- Ignorando o guard de autorização com SkipAuthCheck
- Usando múltiplos guards
Antes de mergulharmos nos detalhes, vamos primeiro entender onde os guards se encaixam no ciclo de vida do framework NestJS.
Guards no ciclo de vida do NestJS
Guards no ciclo de vida do NestJS
No NestJS, o ciclo de vida da solicitação se refere à sequência de eventos que lidam com uma solicitação recebida e a resposta enviada. Uma solicitação recebida flui por vários componentes, como middleware, guards, interceptadores e pipes, antes de chegar ao endpoint e gerar uma resposta. Cada um desses componentes desempenha um papel no tratamento da solicitação e sua resposta. Os guards são executados após qualquer middleware definido e antes que o interceptor seja invocado.
Os guards podem ser aplicados em três níveis diferentes:
- Global
- Controlador
- Escopo da rota (método)
A sequência de execução dos guards segue uma hierarquia, começando pelos guards globais, seguidos pelos guards do controlador e terminando com os guards da rota.
Compreender a sequência de execução permite que os desenvolvedores aproveitem vários eventos do ciclo de vida. Por exemplo, podemos criar um guard global para garantir que apenas usuários com um perfil válido possam acessar o aplicativo. Além disso, criando um guard de função, podemos configurar o controlador do usuário para permitir apenas que o usuário administrador acesse.
Em outras palavras, os guards globais fornecem proteção geral, enquanto os guards do controlador e da rota nos dão controle de acesso mais específico.
Agora, vamos criar um guard.
Criando um AuthGuard
Na documentação oficial, um guard é definido como:
Uma classe anotada com o decorador @Injectable(), que implementa a interface CanActivate.
Para criar um guard, devemos implementar a interface CanActivate. Essa interface requer um método canActivate que é chamado toda vez que uma solicitação é feita para uma rota decorada com o guard. O método canActivate recebe um argumento ExecutionContext e deve retornar um valor booleano que indica se a rota pode ser acessada.
ExecutionContext se refere ao contexto de execução atual da aplicação quando uma solicitação está sendo tratada. Ele inclui informações como:
- A solicitação atual
- Sua resposta
- O manipulador da próxima função.
Usando guards para proteger rotas individuais
Abaixo está um exemplo de um guard simples que permite o acesso a uma rota apenas se a solicitação incluir um cabeçalho Authorization válido:
// auth.guard.ts @Injectable() export class AuthGuard implements CanActivate { constructor(private reflector: Reflector) {} canActivate(context: ExecutionContext): boolean { const request = context.switchToHttp().getRequest(); // usamos uma string hardcoded para validar o usuário para simplificar return request.headers?.authorization === 'valid_token'; } }
O exemplo acima utiliza o método switchToHttp para obter um objeto HttpArgumentsHost adequado para o contexto de aplicação HTTP. Em seguida, o objeto de solicitação é extraído do objeto HttpArgumentsHost e os cabeçalhos da solicitação são examinados para confirmar a presença de um token válido.
Como este é um exemplo fictício, usamos uma string hardcoded para validar o usuário para simplificar. Em um cenário do mundo real, você deve usar informações de um token JWT ou validá-lo em relação a um banco de dados.
Para aplicar o guard, use o decorador @UseGuards do NestJS e passe o guard como um argumento. Abaixo está um exemplo de aplicação do AuthGuard em um ponto de extremidade específico:
import { Controller, Get, UseGuards } from '@nestjs/common'; import { AuthGuard } from './auth.guard'; @Controller() export class AppController { @Get() @UseGuards(AuthGuard) getHello(): string { // Esta rota só será acessível se a solicitação incluir um cabeçalho válido return this.appService.getHello(); } }
Usando guards para proteger controladores
Para proteger controladores, também podemos usar guards, em vez de apenas rotas individuais.
import { Controller, UseGuards } from '@nestjs/common'; import { AuthGuard } from './auth.guard'; @Controller('users') @UseGuards(AuthGuard) export class AppController { ... }
Para aplicar um guard no nível da aplicação, podemos usar o método useGlobalGuards da instância da aplicação NestJS:
const app = await NestFactory.create(AppModule); app.useGlobalGuards(new RolesGuard());
Para testar o novo AuthGuard, execute o seguinte comando para iniciar o servidor NestJS:
npm run start
Se enviarmos uma solicitação com um cabeçalho de autorização incorreto a resposta será o código de status HTTP 403:
Com um cabeçalho correto, a solicitação é bem-sucedida:
Criando um guard de função usando metadados
Controle de acesso baseado em funções (RBAC) é um mecanismo de controle de acesso comumente usado definido em torno de funções. Vamos ver como implementar um RBAC básico no NestJS usando guards.
Primeiramente, precisamos definir o enum de função representando as funções neste exemplo:
// role.enum.ts export enum Role { Admin = 'Admin', Reader = 'Reader', Writer = 'Writer' }
Em seguida, crie um decorator Roles para definir as funções permitidas para um controlador ou método:
// roles.ts import {SetMetadata} from '@nestjs/common'; import { Role } from './clients/entities/role.enum'; export const Roles = (...roles: Role[]) => SetMetadata('roles', roles);
No NestJS, o decorator @SetMetadata é fornecido para anexar metadados a uma classe ou método. Ele armazena os metadados como um par chave-valor. No código acima, a chave é "roles", e o valor é passado a partir do argumento "roles". Os metadados salvos podem ser usados pelo guard de função posteriormente.
// role.guard.ts @Injectable() export class RolesGuard implements CanActivate { constructor(private reflector: Reflector) {} canActivate(context: ExecutionContext): boolean { // recupera as roles const roles = this.reflector.getAllAndOverride<string[]>('roles', [context.getHandler(), context.getClass()]); if (!roles) { return false; } const request = context.switchToHttp().getRequest(); const userRoles = request.headers?.role?.split(','); return this.validateRoles(roles, userRoles); } validateRoles(roles: string[], userRoles: string[]) { return roles.some(role => userRoles.includes(role)); } }
Agora, podemos criar o RoleGuard:
No guard de função, o método getAllAndOverride é usado para recuperar as funções. O método leva dois argumentos:
- Chave: A chave dos metadados
- Alvos: o conjunto de objetos decorados a partir dos quais os metadados são recuperados
O código acima obtém os metadados para a chave "roles" a partir do contexto da classe decorada e do handler da rota decorada. Se diferentes funções estiverem associadas à classe e ao handler, os metadados do handler substituem os metadados da classe no resultado retornado.
Resumidamente, o método recupera as funções do usuário dos headers da requisição e chama o método validateRoles para comparar a função do usuário com as funções requeridas. Se a função do usuário estiver presente, o método retorna true para conceder ao usuário acesso ao recurso.
Usando role guards com o decorator Roles
O role guard deve ser usado em conjunto com o decorador Roles. Neste exemplo, atribuímos a função Writer ao endpoint createClient e a função Reader ao endpoint getClients.
// client.controller.ts @Post() @Roles(Role.Writer) @UseGuards(RolesGuard) create(@Body() createClientDto: CreateClientDto) { return this.clientsService.create(createClientDto); } @Get() @Roles(Role.Reader) @UseGuards(AuthGuard, RolesGuard) getClients() { return this.clientsService.findAll(); }
Vamos testar o role guard usando postman
Nós enviamos uma solicitação POST e uma solicitação GET para o endpoint de clientes. Em ambas as solicitações, definimos o cabeçalho da solicitação com a função de leitor. A solicitação GET retorna com o código de status HTTP 200 e a solicitação POST é rejeitada com o código de status 401. Isso mostra que o guarda de função está funcionando corretamente.
Ignorando o guard de autorização com SkipAuthCheck
Às vezes, podemos querer ignorar o guard de autorização. Por exemplo, podemos aplicar um AuthGuard a um controlador, mas um dos pontos finais dentro do controlador é destinado a ser público. Isso pode ser alcançado adicionando metadados específicos ao ponto final.
Para começar, vamos criar um decorador AuthMetaData, que pode ser usado para definir os metadados com a chave 'auth' para um ponto final específico:
import { SetMetadata } from "@nestjs/common"; export const AuthMetaData = (...metadata: string[]) => SetMetadata('auth', metadata);
Mais tarde, esses metadados podem ser lidos por um guard. Então, podemos usá-los para adicionar os metadados 'skipAuthCheck' a um ponto final:
@Get('hello') @AuthMetaData('skipAuthCheck') getPublicHello(): string { …}
No AuthGuard, adicionamos este bloco de código para verificar se os metadados skipAuthCheck existem:
const authMetaData = this.reflector.getAllAndOverride<string[]>('auth', [ context.getHandler(), context.getClass(), ]); if (authMetaData?.includes('skipAuthCheck')) { return true; }
A adição do bloco de código acima permitirá que o guard de autorização ignore a verificação se 'skipAuthCheck' estiver contido nos metadados com a chave 'auth'.
O guard SkipAuthCheck adiciona flexibilidade ao gerenciar o acesso a pontos finais específicos.
Usando múltiplos guards
O NestJS permite que vários guards sejam aplicados a um único alvo no nível do controlador ou rota.
Podemos usar o decorador @UseGuards para aplicar vários guards, e os guards serão executados na ordem em que são vinculados. Se algum dos guards retornar falso, a solicitação será negada.
No exemplo abaixo, usamos o decorador @UseGuards para combinar tanto o AuthGuard quanto o RoleGuard no ClientsController:
@Controller('clients') @UseGuards(AuthGuard, RolesGuard) export class ClientsController { // Este controlador será protegido pelo AuthGuard e pelo RoleGuard }
Nesse cenário, o acesso ao controlador é concedido somente se tanto o AuthGuard quanto o RolesGuard retornarem true.
Resumo
Os Guards são um recurso poderoso do NestJS que podem ser usados para controlar o acesso às rotas com base em certas condições. Eles podem ser usados em diferentes escopos e também podem ser aplicados em combinação para criar um controle de acesso refinado. Compreender como usar os guards pode ajudar a criar aplicativos NestJS mais seguros e bem organizados.