Xavfsizlik10 min read

Avtorizatsiya

Avtorizatsiya foydalanuvchi nima qila olishini aniqlovchi jarayon. Masalan, administrator foydalanuvchi postlarni yaratish, tahrirlash va o'chirish huquqiga ega. Administrator bo'l

Avtorizatsiya foydalanuvchi nima qila olishini aniqlovchi jarayon. Masalan, administrator foydalanuvchi postlarni yaratish, tahrirlash va o'chirish huquqiga ega. Administrator bo'lmagan foydalanuvchi esa postlarni faqat o'qishi mumkin.

Avtorizatsiya autentifikatsiyadan mustaqil va ortogonal. Biroq, avtorizatsiya autentifikatsiya mexanizmini talab qiladi.

Avtorizatsiyani boshqarish uchun turli yondashuv va strategiyalar mavjud. Har bir loyiha uchun yondashuv uning aniq ilova talablariga bog'liq. Bu bob turli talablar uchun moslashtirilishi mumkin bo'lgan avtorizatsiya yondashuvlarini taqdim etadi.

Oddiy RBAC implementatsiyasi

Role-based access control (RBAC) - rollar va imtiyozlar atrofida aniqlangan, siyosatdan mustaqil access-control mexanizmi. Bu bo'limda Nest guards yordamida juda oddiy RBAC mexanizmini qanday implementatsiya qilishni ko'rsatamiz.

Avval tizimdagi rollarni ifodalovchi Role enumni yaratamiz:

TypeScript
role.enum
1export enum Role {
2  User = 'user',
3  Admin = 'admin',
4}
Hint

Murakkabroq tizimlarda rollarni bazada saqlashingiz yoki tashqi autentifikatsiya provayderidan olishingiz mumkin.

Shu bilan, @Roles() dekoratorini yaratamiz. Bu dekorator muayyan resurslarga kirish uchun qaysi rollar talab qilinishini ko'rsatishga imkon beradi.

TypeScript
roles.decorator
1import { SetMetadata } from '@nestjs/common';
2import { Role } from '../enums/role.enum';
3
4export const ROLES_KEY = 'roles';
5export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);

Endi custom @Roles() dekoratorimiz bor, uni istalgan route handlerni bezash uchun ishlata olamiz.

TypeScript
cats.controller
1@Post()
2@Roles(Role.Admin)
3create(@Body() createCatDto: CreateCatDto) {
4  this.catsService.create(createCatDto);
5}

Nihoyat, RolesGuard klassini yaratamiz. U joriy foydalanuvchiga biriktirilgan rollarni joriy so'rov qayta ishlanayotgan route talab qiladigan rollar bilan solishtiradi. Route rollariga (custom metadata) kirish uchun framework tomonidan tayyor taqdim etiladigan va @nestjs/core paketidan eksport qilinadigan Reflector helper klassidan foydalanamiz.

TypeScript
roles.guard
1import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
2import { Reflector } from '@nestjs/core';
3
4@Injectable()
5export class RolesGuard implements CanActivate {
6  constructor(private reflector: Reflector) {}
7
8  canActivate(context: ExecutionContext): boolean {
9    const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
10      context.getHandler(),
11      context.getClass(),
12    ]);
13    if (!requiredRoles) {
14      return true;
15    }
16    const { user } = context.switchToHttp().getRequest();
17    return requiredRoles.some((role) => user.roles?.includes(role));
18  }
19}
Hint

Reflectordan kontekstga bog'liq tarzda foydalanish haqida batafsil ma'lumot uchun Execution context bobidagi Reflection and metadata bo'limiga qarang.

Notice

Bu misol "oddiy" deb nomlangan, chunki biz faqat route handler darajasida rollar mavjudligini tekshiramiz. Haqiqiy ilovalarda bir nechta operatsiyalarni o'z ichiga oladigan endpoint/handlerlar bo'lishi mumkin va ularning har biri ma'lum ruxsatlar to'plamini talab qiladi. Bunday holatda siz biznes mantiqingiz ichida rollarni tekshirish mexanizmini taqdim etishingiz kerak bo'ladi, bu esa markaziy tarzda qaysi amallar qaysi ruxsatlar bilan bog'lanishini saqlashni biroz qiyinlashtiradi.

Bu misolda request.user foydalanuvchi instansiyasi va ruxsat etilgan rollarni (roles xossasi ostida) o'z ichiga oladi deb taxmin qildik. Ilovangizda bu bog'lanishni odatda custom autentifikatsiya guard ichida yaratasiz - batafsil ma'lumot uchun authentication bobiga qarang.

Ushbu misol ishlashi uchun User klassingiz quyidagicha bo'lishi kerak:

TypeScript
1class User {
2  // ...other properties
3  roles: Role[];
4}

Oxirida RolesGuard ni ro'yxatdan o'tkazing, masalan, controller darajasida yoki global tarzda:

TypeScript
1providers: [
2  {
3    provide: APP_GUARD,
4    useClass: RolesGuard,
5  },
6],

Imtiyozlari yetarli bo'lmagan foydalanuvchi endpointga murojaat qilganda, Nest avtomatik ravishda quyidagi javobni qaytaradi:

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

Agar boshqacha xato javobi qaytarishni xohlasangiz, boolean qiymat qaytarish o'rniga o'zingizning aniq istisnoingizni tashlang.

Claims-based avtorizatsiya

Shaxs (identity) yaratilganda, unga ishonchli tomon tomonidan bir yoki bir nechta claim berilishi mumkin. Claim - bu subyekt nima qila olishini ifodalovchi name-value juftligi, subyektning kimligi emas.

Nestda claims-based avtorizatsiyani implementatsiya qilish uchun yuqorida RBAC bo'limida ko'rsatgan qadamlarni bir muhim farq bilan takrorlaysiz: aniq rollarni tekshirish o'rniga permissions ni solishtirasiz. Har bir foydalanuvchida ruxsatlar to'plami bo'ladi. Shuningdek, har bir resurs/endpoint kirish uchun qanday ruxsatlar kerakligini belgilaydi (masalan, maxsus @RequirePermissions() dekoratori orqali).

TypeScript
cats.controller
1@Post()
2@RequirePermissions(Permission.CREATE_CAT)
3create(@Body() createCatDto: CreateCatDto) {
4  this.catsService.create(createCatDto);
5}
Hint

Yuqoridagi misolda Permission (RBAC bo'limida ko'rsatgan Role ga o'xshash) tizimingizda mavjud barcha ruxsatlarni o'z ichiga oladigan TypeScript enum.

CASL integratsiyasi

CASL - bu berilgan klient qaysi resurslarga kira olishini cheklaydigan isomorphic avtorizatsiya kutubxonasi. U bosqichma-bosqich joriy etishga moslashtirilgan va oddiy claim based modeldan to'liq funksional subject va attribute based avtorizatsiyagacha oson masshtablanadi.

Boshlash uchun avval @casl/ability paketini o'rnating:

Terminal
1$ npm i @casl/ability
Hint

Bu misolda biz CASLni tanladik, lekin xohishingiz va loyiha ehtiyojlaringizga qarab accesscontrol yoki acl kabi boshqa kutubxonadan ham foydalanishingiz mumkin.

O'rnatish tugagach, CASL mexanikasini ko'rsatish uchun ikki entity klassini aniqlaymiz: User va Article.

TypeScript
1class User {
2  id: number;
3  isAdmin: boolean;
4}

User klassi ikki xossadan iborat: noyob foydalanuvchi identifikatori bo'lgan id va foydalanuvchi administrator imtiyozlariga ega ekanini bildiradigan isAdmin.

TypeScript
1class Article {
2  id: number;
3  isPublished: boolean;
4  authorId: number;
5}

Article klassi uchta xossaga ega: id - noyob maqola identifikatori, isPublished - maqola chop etilgan yoki yo'qligini bildiradi, va authorId - maqolani yozgan foydalanuvchi ID si.

Endi ushbu misol uchun talablarimizni ko'rib chiqamiz va aniqlashtiramiz:

  • Adminlar barcha entitylarni boshqara oladi (create/read/update/delete)
  • Foydalanuvchilar hamma narsaga faqat o'qish huquqiga ega
  • Foydalanuvchilar o'z maqolalarini yangilay oladi (article.authorId === userId)
  • Allaqachon chop etilgan maqolalar o'chirilmaydi (article.isPublished === true)

Shu asosda, foydalanuvchilar entitylar bilan bajarishi mumkin bo'lgan barcha harakatlarni ifodalovchi Action enumini yaratamiz:

TypeScript
1export enum Action {
2  Manage = 'manage',
3  Create = 'create',
4  Read = 'read',
5  Update = 'update',
6  Delete = 'delete',
7}
Notice

manage CASLdagi maxsus keyword bo'lib, "har qanday amal" degan ma'noni anglatadi.

CASL kutubxonasini kapsullash uchun, CaslModule va CaslAbilityFactory ni generatsiya qilamiz.

Terminal
1$ nest g module casl
2$ nest g class casl/casl-ability.factory

Shu bilan, CaslAbilityFactory ichida createForUser() metodini aniqlaymiz. Bu metod berilgan foydalanuvchi uchun Ability obyektini yaratadi:

TypeScript
1type Subjects = InferSubjects<typeof Article | typeof User> | 'all';
2
3export type AppAbility = MongoAbility<[Action, Subjects]>;
4
5@Injectable()
6export class CaslAbilityFactory {
7  createForUser(user: User) {
8    const { can, cannot, build } = new AbilityBuilder(createMongoAbility);
9
10    if (user.isAdmin) {
11      can(Action.Manage, 'all'); // read-write access to everything
12    } else {
13      can(Action.Read, 'all'); // read-only access to everything
14    }
15
16    can(Action.Update, Article, { authorId: user.id });
17    cannot(Action.Delete, Article, { isPublished: true });
18
19    return build({
20      // Read https://casl.js.org/v6/en/guide/subject-type-detection#use-classes-as-subject-types for details
21      detectSubjectType: (item) =>
22        item.constructor as ExtractSubjectType<Subjects>,
23    });
24  }
25}
Notice

all CASLdagi maxsus keyword bo'lib, "istalgan subject"ni anglatadi.

Hint

CASL v6 dan boshlab, MongoAbility MongoDBga o'xshash sintaksisdagi shartlarga asoslangan ruxsatlarni yaxshiroq qo'llab-quvvatlash uchun legacy Ability ni almashtirib, default ability klassi bo'lib xizmat qiladi. Nomiga qaramay, u MongoDBga bog'lanmagan - u obyektlarni Mongo uslubidagi shartlar bilan solishtirish orqali har qanday ma'lumot bilan ishlaydi.

Hint

MongoAbility, AbilityBuilder, AbilityClass, va ExtractSubjectType klasslari @casl/ability paketidan eksport qilinadi.

Hint

detectSubjectType opsiyasi CASLga obyektning subject tipini qanday olishni tushunishga yordam beradi. Batafsil ma'lumot uchun CASL documentation ga qarang.

Yuqoridagi misolda biz AbilityBuilder klassi yordamida MongoAbility instansiyasini yaratdik. Taxmin qilganingizdek, can va cannot bir xil argumentlarni qabul qiladi, ammo ma'nosi turlicha: can ruxsat beradi, cannot taqiqlaydi. Ikkalasi ham 4 tagacha argument qabul qilishi mumkin. Bu funksiyalar haqida ko'proq ma'lumot olish uchun rasmiy CASL hujjatlariga qarang.

Oxirida CaslModule modul ta'rifida CaslAbilityFactory ni providers va exports massivlariga qo'shishni unutmang:

TypeScript
1import { Module } from '@nestjs/common';
2import { CaslAbilityFactory } from './casl-ability.factory';
3
4@Module({
5  providers: [CaslAbilityFactory],
6  exports: [CaslAbilityFactory],
7})
8export class CaslModule {}

Shu bilan, CaslModule host kontekstda import qilingan bo'lsa, CaslAbilityFactory ni istalgan klassga standart konstruktor injection orqali inject qilishimiz mumkin:

TypeScript
1constructor(private caslAbilityFactory: CaslAbilityFactory) {}

So'ng uni klassda quyidagicha ishlatamiz.

TypeScript
1const ability = this.caslAbilityFactory.createForUser(user);
2if (ability.can(Action.Read, 'all')) {
3  // "user" has read access to everything
4}
Hint

MongoAbility klassi haqida batafsil rasmiy CASL hujjatlarida o'qing.

Masalan, admin bo'lmagan foydalanuvchini olamiz. Bu holatda u maqolalarni o'qiy oladi, ammo yangilarini yaratish yoki mavjudlarini o'chirish taqiqlangan bo'lishi kerak.

TypeScript
1const user = new User();
2user.isAdmin = false;
3
4const ability = this.caslAbilityFactory.createForUser(user);
5ability.can(Action.Read, Article); // true
6ability.can(Action.Delete, Article); // false
7ability.can(Action.Create, Article); // false
Hint

MongoAbility va AbilityBuilder klasslari ikkalasi ham can va cannot metodlarini taqdim etsa-da, ularning maqsadi va qabul qiladigan argumentlari biroz farq qiladi.

Shuningdek, talablarimizga ko'ra, foydalanuvchi o'z maqolalarini yangilay olishi kerak:

TypeScript
1const user = new User();
2user.id = 1;
3
4const article = new Article();
5article.authorId = user.id;
6
7const ability = this.caslAbilityFactory.createForUser(user);
8ability.can(Action.Update, article); // true
9
10article.authorId = 2;
11ability.can(Action.Update, article); // false

Ko'rib turganingizdek, MongoAbility instansiyasi ruxsatlarni ancha o'qilishi oson tarzda tekshirishga imkon beradi. Xuddi shuningdek, AbilityBuilder ruxsatlarni (va turli shartlarni) o'xshash tarzda belgilash imkonini beradi. Ko'proq misollar uchun rasmiy hujjatlarga qarang.

Ilg'or: PoliciesGuard ni implementatsiya qilish

Bu bo'limda biz bir oz murakkabroq guardni qurishni ko'rsatamiz; u metod darajasida sozlanishi mumkin bo'lgan avtorizatsiya siyosatlari bo'yicha foydalanuvchining mosligini tekshiradi (xohlasangiz, uni klass darajasidagi siyosatlarga ham moslashtirishingiz mumkin). Bu misolda CASL paketidan faqat illyustratsiya sifatida foydalanamiz, ammo bu kutubxonadan foydalanish majburiy emas. Shuningdek, oldingi bo'limda yaratgan CaslAbilityFactory providerdan foydalanamiz.

Avval talablarimizni aniqlaymiz. Maqsad - har bir route handler uchun siyosat tekshiruvlarini ko'rsatishga imkon beradigan mexanizm taqdim etish. Biz obyektlarni ham, funksiyalarni ham qo'llab-quvvatlaymiz (oddiy tekshiruvlar va funksional uslubni afzal ko'rganlar uchun).

Keling, siyosat handlerlari uchun interfeyslarni aniqlaymiz:

TypeScript
1import { AppAbility } from '../casl/casl-ability.factory';
2
3interface IPolicyHandler {
4  handle(ability: AppAbility): boolean;
5}
6
7type PolicyHandlerCallback = (ability: AppAbility) => boolean;
8
9export type PolicyHandler = IPolicyHandler | PolicyHandlerCallback;

Yuqorida aytilganidek, siyosat handlerini aniqlashning ikki yo'lini taqdim etdik: obyekt (ya'ni IPolicyHandler interfeysini implementatsiya qiladigan klass instansiyasi) va funksiya (ya'ni PolicyHandlerCallback tipiga mos keladigan).

Shu bilan, @CheckPolicies() dekoratorini yaratamiz. Bu dekorator muayyan resurslarga kirish uchun qaysi siyosatlar bajarilishi kerakligini ko'rsatishga imkon beradi.

TypeScript
1export const CHECK_POLICIES_KEY = 'check_policy';
2export const CheckPolicies = (...handlers: PolicyHandler[]) =>
3  SetMetadata(CHECK_POLICIES_KEY, handlers);

Endi route handlerga bog'langan barcha siyosat handlerlarini ajratib olib, bajaradigan PoliciesGuard yaratamiz.

TypeScript
1@Injectable()
2export class PoliciesGuard implements CanActivate {
3  constructor(
4    private reflector: Reflector,
5    private caslAbilityFactory: CaslAbilityFactory,
6  ) {}
7
8  async canActivate(context: ExecutionContext): Promise<boolean> {
9    const policyHandlers =
10      this.reflector.get<PolicyHandler[]>(
11        CHECK_POLICIES_KEY,
12        context.getHandler(),
13      ) || [];
14
15    const { user } = context.switchToHttp().getRequest();
16    const ability = this.caslAbilityFactory.createForUser(user);
17
18    return policyHandlers.every((handler) =>
19      this.execPolicyHandler(handler, ability),
20    );
21  }
22
23  private execPolicyHandler(handler: PolicyHandler, ability: AppAbility) {
24    if (typeof handler === 'function') {
25      return handler(ability);
26    }
27    return handler.handle(ability);
28  }
29}
Hint

Bu misolda request.user foydalanuvchi instansiyasini o'z ichiga oladi deb taxmin qildik. Ilovangizda bu bog'lanishni odatda custom autentifikatsiya guard ichida yaratasiz - batafsil ma'lumot uchun authentication bobiga qarang.

Keling, bu misolni qisqacha tahlil qilaylik. policyHandlers - bu @CheckPolicies() dekoratori orqali metodga biriktirilgan handlerlar massivi. Keyin CaslAbilityFactory#create metodidan foydalanib Ability obyektini yaratamiz, bu obyekt foydalanuvchi aniq amallarni bajarish uchun yetarli ruxsatga ega yoki yo'qligini tekshirishga imkon beradi. Biz bu obyektni siyosat handleriga uzatamiz; u funksiya yoki IPolicyHandler interfeysini implementatsiya qiladigan klass instansiyasi bo'lishi mumkin va handle() metodi boolean qiymat qaytaradi. Oxirida barcha handlerlar true qaytarganini tekshirish uchun Array#every metodidan foydalanamiz.

Nihoyat, bu guardni sinash uchun uni istalgan route handlerga bog'lang va inline siyosat handlerini (funksional yondashuv) ro'yxatdan o'tkazing, quyidagicha:

TypeScript
1@Get()
2@UseGuards(PoliciesGuard)
3@CheckPolicies((ability: AppAbility) => ability.can(Action.Read, Article))
4findAll() {
5  return this.articlesService.findAll();
6}

Muqobil ravishda, IPolicyHandler interfeysini implementatsiya qiladigan klassni aniqlashimiz mumkin:

TypeScript
1export class ReadArticlePolicyHandler implements IPolicyHandler {
2  handle(ability: AppAbility) {
3    return ability.can(Action.Read, Article);
4  }
5}

Va undan quyidagicha foydalanamiz:

TypeScript
1@Get()
2@UseGuards(PoliciesGuard)
3@CheckPolicies(new ReadArticlePolicyHandler())
4findAll() {
5  return this.articlesService.findAll();
6}
Notice

Siyosat handlerini new keywordi bilan joyida instansiyalashimiz kerak bo'lgani uchun, ReadArticlePolicyHandler klassi Dependency Injectiondan foydalana olmaydi. Buni ModuleRef#get metodi yordamida hal qilish mumkin (batafsil bu yerda). Aslida, @CheckPolicies() dekoratori orqali funksiya va instansiyalarni ro'yxatdan o'tkazish o'rniga Type<IPolicyHandler> uzatishga ruxsat berishingiz kerak. So'ng guard ichida type reference yordamida instansiyani olishingiz mumkin: moduleRef.get(YOUR_HANDLER_TYPE) yoki hatto ModuleRef#create metodi orqali dinamik instansiyalashingiz mumkin.