Asosiy bo'lim8 min read

Guardlar

Guard - @Injectable() dekoratori bilan belgilangan, CanActivate interfeysini amalga oshiradigan sinf.

Guard - @Injectable() dekoratori bilan belgilangan, CanActivate interfeysini amalga oshiradigan sinf.

Guardlar yagona mas'uliyatga ega. Ular run-time paytida mavjud bo'lgan muayyan shartlarga (masalan, permissions, roles, ACLs va hokazo) qarab, berilgan so'rov route handler tomonidan qayta ishlanishi yoki yo'qligini aniqlaydi. Bu ko'pincha authorization deb ataladi. Authorization (va odatda birga ishlaydigan uning yaqin tushunchasi authentication) an'anaviy Express ilovalarida odatda middleware tomonidan bajarilgan. Middleware autentifikatsiya uchun yaxshi tanlov, chunki tokenni validatsiya qilish va request obyektiga xossalarni biriktirish kabi ishlar muayyan route konteksti (va uning metadatasi) bilan kuchli bog'liq emas.

Ammo middleware tabiatan sodda. U next() funksiyasini chaqirgandan keyin qaysi handler bajarilishini bilmaydi. Boshqa tomondan, Guardlar ExecutionContext instansiyasiga kirish huquqiga ega, shuning uchun ular keyin nimaning bajarilishini aniq biladi. Ular exception filterlar, pipe'lar va interceptorlar kabi, request/response siklida aynan to'g'ri nuqtada qayta ishlash mantiqini joylashtirishga va buni deklarativ tarzda qilishga mo'ljallangan. Bu kodingizni DRY va deklarativ tutishga yordam beradi.

Hint

Guardlar barcha middlewarelardan keyin, lekin har qanday interceptor yoki pipe'dan oldin bajariladi.

Authorization guard

Aytilganidek, authorization guardlar uchun ajoyib use-case, chunki muayyan route'lar faqat chaqiruvchi (odatda ma'lum autentifikatsiyadan o'tgan foydalanuvchi) yetarli ruxsatlarga ega bo'lgandagina mavjud bo'lishi kerak. Hozir quradigan AuthGuard autentifikatsiyadan o'tgan foydalanuvchini (va shu sababli token request headerlariga biriktirilganini) nazarda tutadi. U tokenni ajratib oladi va validatsiya qiladi, so'ng ajratilgan ma'lumotlardan foydalanib so'rov davom etishi mumkinligini aniqlaydi.

TypeScript
auth.guard
1import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
2import { Observable } from 'rxjs';
3
4@Injectable()
5export class AuthGuard implements CanActivate {
6  canActivate(
7    context: ExecutionContext,
8  ): boolean | Promise<boolean> | Observable<boolean> {
9    const request = context.switchToHttp().getRequest();
10    return validateRequest(request);
11  }
12}
Hint

Ilovangizda autentifikatsiya mexanizmini qanday amalga oshirish bo'yicha real misol izlayotgan bo'lsangiz, bu bob ga qarang. Xuddi shuningdek, yanada murakkab authorization misoli uchun bu sahifa ga qarang.

validateRequest() funksiyasi ichidagi mantiq ehtiyojga qarab sodda yoki murakkab bo'lishi mumkin. Ushbu misolning asosiy maqsadi - guardlarning request/response sikliga qanday mos kelishini ko'rsatish.

Har bir guard canActivate() funksiyasini amalga oshirishi kerak. Bu funksiya joriy so'rovga ruxsat berilgan yoki berilmaganini ko'rsatuvchi boolean qaytaradi. U javobni sinxron yoki asinxron ( Promise yoki Observable orqali) qaytarishi mumkin. Nest qaytgan qiymatdan keyingi harakatni boshqarishda foydalanadi:

  • agar true qaytarsa, so'rov qayta ishlanadi.
  • agar false qaytarsa, Nest so'rovni rad etadi.

Execution context

canActivate() funksiyasi bitta argument qabul qiladi - ExecutionContext instansiyasi. ExecutionContext ArgumentsHost dan meros oladi. Biz ArgumentsHost ni oldin exception filters bobida ko'rganmiz. Yuqoridagi namunada biz faqat avval ishlatgan ArgumentsHost yordamchi metodlaridan foydalanib Request obyektiga havola oldik. Bu mavzu bo'yicha ko'proq ma'lumot uchun exception filters bobining Arguments host bo'limiga qarang.

ArgumentsHost ni kengaytirish orqali ExecutionContext hozirgi bajarilish jarayoni haqida qo'shimcha tafsilotlarni taqdim etadigan bir nechta yangi yordamchi metodlarni ham qo'shadi. Bu tafsilotlar keng ko'lamdagi controllerlar, metodlar va bajarilish kontekstlarida ishlay oladigan yanada umumiy guardlar qurishda foydali bo'lishi mumkin. ExecutionContext haqida batafsil bu yerda.

Role-based authentication

Keling, faqat muayyan roldagi foydalanuvchilarga kirishga ruxsat beradigan yanada funksional guard quramiz. Biz oddiy guard shablonidan boshlaymiz va keyingi bo'limlarda uni rivojlantiramiz. Hozircha u barcha so'rovlarning o'tishiga ruxsat beradi:

TypeScript
roles.guard
1import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
2import { Observable } from 'rxjs';
3
4@Injectable()
5export class RolesGuard implements CanActivate {
6  canActivate(
7    context: ExecutionContext,
8  ): boolean | Promise<boolean> | Observable<boolean> {
9    return true;
10  }
11}

Guardlarni ulash

Pipe va exception filterlar kabi, guardlar controller-scoped, method-scoped yoki global-scoped bo'lishi mumkin. Quyida @UseGuards() dekoratori orqali controller-scoped guardni sozlaymiz. Bu dekorator bitta argumentni yoki vergul bilan ajratilgan argumentlar ro'yxatini qabul qilishi mumkin. Bu sizga bitta deklaratsiya bilan tegishli guardlar to'plamini qo'llash imkonini beradi.

TypeScript
1@Controller('cats')
2@UseGuards(RolesGuard)
3export class CatsController {}
Hint

@UseGuards() dekoratori @nestjs/common paketidan import qilinadi.

Yuqorida biz instansiya emas, RolesGuard sinfini uzatdik, instansiyalash mas'uliyatini freymvorkka qoldirib va dependency injection'ni yoqib. Pipe va exception filterlarda bo'lgani kabi, joyida instansiya ham uzatishimiz mumkin:

TypeScript
1@Controller('cats')
2@UseGuards(new RolesGuard())
3export class CatsController {}

Yuqoridagi konstruktsiya guardni ushbu controller tomonidan e'lon qilingan har bir handlerga biriktiradi. Agar guard faqat bitta metodga tatbiq etilishini istasak, @UseGuards() dekoratorini metod darajasida qo'llaymiz.

Global guardni sozlash uchun Nest ilova instansiyasining useGlobalGuards() metodidan foydalaning:

TypeScript
1const app = await NestFactory.create(AppModule);
2app.useGlobalGuards(new RolesGuard());
Notice

Gibrid ilovalar holatida useGlobalGuards() metodi odatda gatewaylar va mikroservislar uchun guardlarni sozlamaydi (bu xatti-harakatni qanday o'zgartirish haqida ma'lumot uchun Hybrid application ga qarang). "Standart" (gibrid bo'lmagan) mikroservis ilovalari uchun useGlobalGuards() guardlarni global tarzda o'rnatadi.

Global guardlar butun ilova bo'ylab, har bir controller va har bir route handler uchun ishlatiladi. Dependency injection nuqtai nazaridan shuni yodda tuting: har qanday moduldan tashqarida (useGlobalGuards() orqali, yuqoridagi misoldagidek) ro'yxatdan o'tkazilgan global guardlar bog'liqliklarni in'eksiya qila olmaydi, chunki bu modul kontekstidan tashqarida bajariladi. Bu muammoni hal qilish uchun quyidagi konstruktsiya yordamida guardni bevosita istalgan moduldan sozlashingiz mumkin:

TypeScript
app.module
1import { Module } from '@nestjs/common';
2import { APP_GUARD } from '@nestjs/core';
3
4@Module({
5  providers: [
6    {
7      provide: APP_GUARD,
8      useClass: RolesGuard,
9    },
10  ],
11})
12export class AppModule {}
Hint

Guard uchun dependency injection'ni shu yondashuv orqali bajarayotganda, bu konstruktsiya qaysi modulda qo'llanmasin, guard aslida global ekanini yodda tuting. Buni qayerda qilish kerak? Guard (RolesGuard yuqoridagi misolda) aniqlangan modulni tanlang. Shuningdek, useClass maxsus provayder ro'yxatga olishning yagona usuli emas. Batafsil bu yerda.

Har bir handler uchun rollarni belgilash

Bizning RolesGuard ishlayapti, ammo u hali unchalik aqlli emas. Biz guardning eng muhim xususiyatidan - execution context dan hali foydalanmayapmiz. U hali rollar yoki har bir handler uchun qaysi rollar ruxsat etilganini bilmaydi. Masalan, CatsController turli route'lar uchun turli ruxsat sxemalariga ega bo'lishi mumkin. Ba'zilari faqat admin foydalanuvchi uchun mavjud bo'lishi mumkin, boshqalari esa hamma uchun ochiq. Rollarni marshrutlarga qanday qilib moslashuvchan va qayta foydalaniladigan tarzda bog'laymiz?

Bu yerda custom metadata yordamga keladi (batafsil bu yerda). Nest custom metadata ni route handlerlarga Reflector.createDecorator statik metodi orqali yaratilgan dekoratorlar yoki o'rnatilgan @SetMetadata() dekoratori orqali biriktirish imkonini beradi.

Masalan, handlerga metadata biriktiradigan @Roles() dekoratorini Reflector.createDecorator metodi yordamida yaratamiz. Reflector freymvork tomonidan tayyor holatda taqdim etiladi va @nestjs/core paketidan eksport qilinadi.

TypeScript
roles.decorator
1import { Reflector } from '@nestjs/core';
2
3export const Roles = Reflector.createDecorator<string[]>();

Bu yerdagi Roles dekoratori string[] tipidagi bitta argument qabul qiladigan funksiya.

Endi bu dekoratordan foydalanish uchun handlerni shunchaki unga annotatsiya qilamiz:

TypeScript
cats.controller
1@Post()
2@Roles(['admin'])
3async create(@Body() createCatDto: CreateCatDto) {
4  this.catsService.create(createCatDto);
5}

Bu yerda Roles dekoratori metadata'sini create() metodiga biriktirdik, ya'ni faqat admin roliga ega foydalanuvchilar ushbu route'ga kirishi kerakligini ko'rsatdik.

Muqobil ravishda, Reflector.createDecorator metodidan foydalanish o'rniga o'rnatilgan @SetMetadata() dekoratoridan foydalanishimiz mumkin. Batafsil bu yerda.

Hammasini birlashtiramiz

Endi orqaga qaytib, buni RolesGuard bilan birlashtiramiz. Hozir u barcha holatlarda true qaytaradi va har qanday so'rovning o'tishiga ruxsat beradi. Biz qaytish qiymatini joriy foydalanuvchiga berilgan rollar bilan joriy marshrut talab qilayotgan rollarni solishtirishga asoslab, shartli qilishni istaymiz. Marshrutning roli(lar)i (custom metadata) ga kirish uchun yana Reflector yordamchi sinfidan foydalanamiz, quyidagicha:

TypeScript
roles.guard
1import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
2import { Reflector } from '@nestjs/core';
3import { Roles } from './roles.decorator';
4
5@Injectable()
6export class RolesGuard implements CanActivate {
7  constructor(private reflector: Reflector) {}
8
9  canActivate(context: ExecutionContext): boolean {
10    const roles = this.reflector.get(Roles, context.getHandler());
11    if (!roles) {
12      return true;
13    }
14    const request = context.switchToHttp().getRequest();
15    const user = request.user;
16    return matchRoles(roles, user.roles);
17  }
18}
Hint

node.js dunyosida avtorizatsiyadan o'tgan foydalanuvchini request obyektiga biriktirish odatiy amaliyotdir. Shuning uchun, yuqoridagi namunaviy kodda biz request.user foydalanuvchi instansiyasini va ruxsat etilgan rollarni o'z ichiga oladi deb faraz qilamiz. Sizning ilovangizda, ehtimol, bu bog'lanishni maxsus authentication guard (yoki middleware) ichida qilasiz. Ushbu mavzu bo'yicha ko'proq ma'lumot uchun bu bob ga qarang.

Warning

matchRoles() funksiyasi ichidagi mantiq ehtiyojga qarab sodda yoki murakkab bo'lishi mumkin. Ushbu misolning asosiy maqsadi - guardlarning request/response sikliga qanday mos kelishini ko'rsatish.

Reflector ni kontekstga sezgir tarzda ishlatish bo'yicha batafsil ma'lumot uchun Execution context bobining Reflection and metadata bo'limiga qarang.

Huquqlari yetarli bo'lmagan foydalanuvchi endpoint so'raganda, Nest avtomatik ravishda quyidagi javobni qaytaradi:

TypeScript
1{
2  "statusCode": 403,
3  "message": "Forbidden resource",
4  "error": "Forbidden"
5}

E'tibor bering, ichkarida guard false qaytarganda, freymvork ForbiddenException tashlaydi. Agar siz boshqa xato javobini qaytarmoqchi bo'lsangiz, o'zingizga xos istisno tashlashingiz kerak. Masalan:

TypeScript
1throw new UnauthorizedException();

Guard tomonidan tashlangan har qanday istisno exceptions layer (global exceptions filter va joriy kontekstga qo'llangan istisno filterlari) tomonidan qayta ishlanadi.

Hint

Authorizationni qanday amalga oshirish bo'yicha real misol izlayotgan bo'lsangiz, bu bob ga qarang.