Guards
Un garde est une classe annotée avec le @Injectable()
décorateur, qui implémente l’interface CanActivate
.
Les gardes ont une responsabilité unique. Ils déterminent si une demande donnée sera traitée par le gestionnaire de route ou non, selon certaines conditions (comme les autorisations, les rôles, les ACL, etc.) présentes à l’exécution. Cela est souvent désigné par le terme autorisation. L’autorisation (et sa cousine, authentification, avec laquelle elle collabore généralement) a traditionnellement été gérée par des middleware dans les applications Express traditionnelles. Les middleware sont un bon choix pour l’authentification, car des éléments comme la validation de jeton et l’attachement de propriétés à l’objet request
ne sont pas fortement connectés à un contexte de route particulier (et à sa métadonnées).
Mais le middleware, par sa nature, est limité. Il ne sait pas quel gestionnaire sera exécuté après l’appel de la fonction next()
. En revanche, les gardes ont accès à l’instance ExecutionContext
, et savent donc exactement ce qui va être exécuté ensuite. Ils sont conçus, tout comme les filtres d’exception, les pipes et les intercepteurs, pour vous permettre d’intercaler une logique de traitement au bon moment dans le cycle de requête/réponse, et de le faire de manière déclarative. Cela aide à garder votre code DRY et déclaratif.
Garde d’autorisation
Comme mentionné, l’autorisation est un excellent cas d’utilisation pour les gardes parce que certaines routes devraient être disponibles uniquement lorsque l’appelant (généralement un utilisateur authentifié spécifique) a les autorisations suffisantes. Le AuthGuard
que nous allons construire suppose un utilisateur authentifié (et qu’en conséquence, un jeton est attaché aux en-têtes de requête). Il va extraire et valider le jeton, et utiliser les informations extraites pour déterminer si la requête peut se poursuivre ou non.
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); }}
La logique à l’intérieur de la fonction validateRequest()
peut être aussi simple ou sophistiquée que nécessaire. Le point principal de cet exemple est de montrer comment les gardes s’intègrent dans le cycle de requête/réponse.
Chaque garde doit implémenter une fonction canActivate()
. Cette fonction doit renvoyer un booléen, indiquant si la requête actuelle est autorisée ou non. Elle peut renvoyer la réponse soit de manière synchrone, soit de manière asynchrone (via une Promise
ou un Observable
). Nest utilise la valeur de retour pour contrôler l’action suivante :
- si elle retourne
true
, la demande sera traitée. - si elle retourne
false
, Nest rejettera la demande.
Contexte d’exécution
La fonction canActivate()
prend un seul argument, l’instance ExecutionContext
. L’ExecutionContext
hérite de ArgumentsHost
. Dans le code d’exemple ci-dessus, nous utilisons simplement les mêmes méthodes d’aide définies sur ArgumentsHost
que nous avons utilisées précédemment, pour obtenir une référence à l’objet Request
. Vous pouvez vous référer à la section Arguments host du chapitre sur les filtres d’exception pour plus d’informations à ce sujet.
En étendant ArgumentsHost
, ExecutionContext
ajoute également plusieurs nouvelles méthodes d’aide qui fournissent des détails supplémentaires sur le processus d’exécution actuel. Ces détails peuvent être utiles pour construire des gardes plus génériques qui peuvent fonctionner sur un large éventail de contrôleurs, de méthodes et de contextes d’exécution. En savoir plus sur ExecutionContext
ici.
Authentification basée sur les rôles
Construisons un garde plus fonctionnel qui permet l’accès uniquement aux utilisateurs ayant un rôle spécifique. Commençons par un modèle de garde de base, et développons-le dans les sections à venir. Pour l’instant, il permet à toutes les demandes de se poursuivre :
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; }}
Liaison des gardes
Comme les pipes et les filtres d’exception, les gardes peuvent être de portée contrôleur, de portée méthode, ou de portée globale. Ci-dessous, nous configurons un garde de portée contrôleur en utilisant le décorateur @UseGuards()
. Ce décorateur peut prendre un seul argument, ou une liste d’arguments séparés par des virgules. Cela vous permet d’appliquer facilement le bon ensemble de gardes avec une seule déclaration.
@Controller('cats')@UseGuards(RolesGuard)export class CatsController {}
Au-dessus, nous avons passé la classe RolesGuard
(au lieu d’une instance), laissant la responsabilité de l’instanciation au cadre et permettant l’injection de dépendances. Comme avec les pipes et les filtres d’exception, nous pouvons également passer une instance sur place.
Pour configurer un garde global, utilisez la méthode useGlobalGuards()
de l’instance de l’application Nest :
const app = await NestFactory.create(AppModule);app.useGlobalGuards(new RolesGuard());
Dans le cas des applications hybrides, la méthode useGlobalGuards()
ne configure pas par défaut les gardes pour les passerelles et les microservices. Consultez les applications hybrides pour des informations sur la façon de modifier ce comportement. Pour les applications microservices “standard” (non hybrides), useGlobalGuards()
monte bien les gardes globalement.
Les gardes globaux sont utilisés à travers toute l’application, pour chaque contrôleur et chaque gestionnaire de route. En termes d’injection de dépendances, les gardes globaux enregistrés depuis l’extérieur de tout module (avec useGlobalGuards()
comme dans l’exemple ci-dessus) ne peuvent pas injecter de dépendances, car cela se fait en dehors du contexte de tout module. Pour résoudre ce problème, vous pouvez configurer un garde directement depuis n’importe quel module en utilisant la construction suivante :
import { Module } from '@nestjs/common';import { APP_GUARD } from '@nestjs/core';
@Module({ providers: [ { provide: APP_GUARD, useClass: RolesGuard, }, ],})export class AppModule {}
Définir des rôles par gestionnaire
Notre RolesGuard
fonctionne, mais il n’est pas encore très intelligent. Nous ne profitons pas encore de la fonctionnalité de garde la plus importante - le contexte d’exécution. Il ne sait pas encore quels rôles sont autorisés pour chaque gestionnaire. Le CatsController
, par exemple, pourrait avoir différents schémas de permission pour différentes routes. Certaines pourraient être disponibles uniquement pour un utilisateur administrateur, et d’autres pourraient être ouvertes à tous. Comment pouvons-nous associer des rôles à des routes d’une manière flexible et réutilisable ?
C’est ici que les métadonnées personnalisées entrent en jeu (en savoir plus ici). Nest offre la possibilité d’attacher des métadonnées personnalisées aux gestionnaires de routes via des décorateurs créés par la méthode statique Reflector#createDecorator
, ou le décorateur intégré @SetMetadata()
.
Par exemple, créons un décorateur @Roles()
utilisant la méthode Reflector#createDecorator
qui attachera les métadonnées au gestionnaire. Reflector
est fourni par défaut par le cadre et exposé depuis le package @nestjs/core
.
import { Reflector } from '@nestjs/core';
export const Roles = Reflector.createDecorator<string[]>();
Le décorateur Roles
ici est une fonction qui prend un seul argument de type string[]
.
Maintenant, pour utiliser ce décorateur, nous annotons simplement le gestionnaire avec :
@Post()@Roles(['admin'])async create(@Body() createCatDto: CreateCatDto) { this.catsService.create(createCatDto);}
Ici, nous avons attaché les métadonnées du décorateur Roles
à la méthode create()
, indiquant que seuls les utilisateurs ayant le rôle admin
devraient être autorisés à accéder à cette route.
Alternativement, au lieu d’utiliser la méthode Reflector#createDecorator
, nous pourrions utiliser le décorateur intégré @SetMetadata()
. En savoir plus ici.
Mettre tout ensemble
Regroupons le tout avec notre RolesGuard
. Actuellement, il retourne simplement true
dans tous les cas, permettant à chaque demande de se poursuivre. Nous voulons rendre la valeur de retour conditionnelle en comparant les rôles assignés à l’utilisateur actuel avec les rôles réels requis par la route actuelle en cours de traitement. Pour accéder aux rôles de la route (métadonnées personnalisées), nous utiliserons à nouveau la classe d’aide Reflector
, comme suit :
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';import { Reflector } from '@nestjs/core';import { Roles } from './roles.decorator';
@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); }}
La logique à l’intérieur de la fonction matchRoles()
peut être aussi simple ou sophistiquée que nécessaire. Le point principal de cet exemple est de montrer comment les gardes s’intègrent dans le cycle de requête/réponse.
Reportez-vous à la section Réflexion et métadonnées du chapitre Contexte d’exécution pour plus de détails sur l’utilisation de Reflector
de manière contextuelle.
Lorsque un utilisateur sans privilèges suffisants demande un point de terminaison, Nest retourne automatiquement la réponse suivante :
{ "statusCode": 403, "message": "Ressource interdite", "error": "Interdit"}
Notez qu’en coulisse, lorsqu’un garde retourne false
, le cadre lance une ForbiddenException
. Si vous voulez retourner une réponse d’erreur différente, vous devriez lancer votre propre exception spécifique. Par exemple :
throw new UnauthorizedException();
Toute exception lancée par un garde sera traitée par la couche d’exceptions (filtres d’exceptions globaux et tous les filtres d’exceptions appliqués au contexte actuel).