IlmHamroh
JavaScript Full-stack/30-qism. NestJS57/65-dars22 daqiqa
Mundarija (30)

NestJS global auth guard: APP_GUARD, @Public va @Roles — "yopiq sukut"

Qisqacha: Global guard — APP_GUARD orqali ulangan va hamma marshrutda ishlaydigan guard. Nest standarti: JwtAuthGuard hammasini yopadi, @Public() dekoratorli yo'llar ochiq qoladi, @Roles("admin") esa RolesGuard orqali 403 beradi. Bu "yopiq sukut" tamoyili: yangi controller qo'shib himoyani unutish mumkin emas. vazifalar-api da xuddi shu g'oya KirishGuard va @Ochiq/@SessiyaKerak/@Rollar nomlari bilan yozilgan. Ko'chishda ikki xato topildi: global versiya sozlamasi auth va admin yo'llarini /v1 siz ham ochib qo'ygan, global guard esa /health ni yopib qo'ygan.

Bu darsda

  • APP_GUARD, @Public(), @Roles() va RolesGuard bilan "yopiq sukut" auth yozasiz.
  • Reflector.getAllAndOverride metod va klass dekoratorlarini qanday birlashtirishini tushunasiz.
  • Guard'lar tartibi nega muhimligini va nega anonim so'rov 415 emas, 401 olishi kerakligini bilasiz.
  • Ko'chishda topilgan ikki xatoni — versiyasiz yo'llar va yopilgan /health — test bilan ushlaysiz.
  • vazifalar-api ga KirishGuard ni qo'shasiz.

Oldin bilishingiz kerak: Access/refresh token va cookie NestJS'da, Global enhancer'lar va bajarilish tartibi, Reflector, metadata va DiscoveryService, RBAC va resurs egaligi, OWASP API Security Top 10.

O'tgan darsda sessiya cookie'si Nest'ga ulandi: kimlik() har so'rovda kim kelganini aniqlaydi. Lekin u hech kimni rad etmaydi. Bu darsda rad etadigan qismni yozamiz — va uni shunday yozamizki, unutib bo'lmasin.

1. Nega bu kerak?

28-qismdagi Express routerini eslang. Har ma'lumot yo'li oldida kirishKerak turardi: v1.use("/vazifalar", kirishKerak, …). Yangi router qo'shilsa, dasturchi kirishKerak ni o'zi yozishi kerak edi. Unutsa — yo'l hammaga ochiq. Hech qanday xato chiqmaydi, testlar o'tadi, chunki ular yangi yo'lni bilmaydi.

Bu "ochiq sukut" (open by default). Xavfsizlikda bu yomon tanlov: xato — jim, oqibati — ma'lumot oqishi. 28-qismdagi xavfsizlik tamoyillari darsida "sukut bo'yicha xavfsiz" (secure by default) degan edik: himoyani yoqish emas, o'chirish aniq harakat talab qilsin.

Nest'da buni bitta qator qiladi: guard'ni APP_GUARD sifatida ulash. Endi har yangi controller avtomatik yopiq. Ochish uchun dasturchi @Public() yozishi kerak — va bu kod review'da ko'zga tashlanadi.

O'xshatish: ofis binosi. "Ochiq sukut" — hamma eshik ochiq, kerakli xonaga qulf osasiz; bitta xonani unutsangiz — ochiq qoladi. "Yopiq sukut" — kirishda turniket, hamma eshik karta bilan ochiladi; ochiq xonalar (oshxona, qabulxona) ro'yxati alohida yozilgan.

2. Nest standarti: JwtAuthGuard + @Public + @Roles

2.1 Dekoratorlar

Nest hujjatidagi naqsh. Dekorator faqat yorliq qo'yadi — metama'lumot (Reflector):

ts
// dekoratorlar.ts — @Public() va @Roles() (Nest hujjatidagi naqsh)
import { SetMetadata } from "@nestjs/common";
import type { Rol } from "../foydalanuvchilar.js";

export const IS_PUBLIC_KEY = "isPublic";
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);

export const ROLES_KEY = "roles";
export const Roles = (...rollar: Rol[]) =>
  SetMetadata(ROLES_KEY, rollar);

2.2 Guard'lar

Birinchisi — Passport'ning AuthGuard("jwt") dan meros, lekin oldin @Public() ni tekshiradi:

ts
@Injectable()
export class JwtAuthGuard extends AuthGuard("jwt") {
  constructor(private readonly reflector: Reflector) {
    super();
  }

  override canActivate(ctx: ExecutionContext) {
    // metod yoki klassda @Public() bormi?
    const ochiq = this.reflector.getAllAndOverride<boolean>(
      IS_PUBLIC_KEY, [ctx.getHandler(), ctx.getClass()]);
    if (ochiq) return true;
    return super.canActivate(ctx); // JwtStrategy ishlaydi
  }
}

Ikkinchisi — rol:

ts
@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private readonly reflector: Reflector) {}

  canActivate(ctx: ExecutionContext): boolean {
    const kerak = this.reflector.getAllAndOverride<Rol[] | undefined>(
      ROLES_KEY, [ctx.getHandler(), ctx.getClass()]);
    if (kerak === undefined) return true; // rol talab qilinmagan
    const req = ctx.switchToHttp().getRequest<Request>();
    const user = req.user as { rol?: Rol } | undefined;
    return user?.rol !== undefined && kerak.includes(user.rol);
  }
}

getAllAndOverride(kalit, [handler, klass]) — avval metoddan qidiradi, topilmasa klassdan. Ya'ni metod klassni bosib o'tadi. Klassda @Roles("admin"), bitta metodda @Roles("foydalanuvchi") bo'lsa — o'sha metodga foydalanuvchi ham kiradi. Ikkalasini qo'shish kerak bo'lsa — getAllAndMerge.

RolesGuard false qaytarsa, Nest o'zi 403 ForbiddenException tashlaydi.

Guard'lar so'rov yo'lining qayerida turadi? So'rov hayot sikli darsidagi jadvalni auth ko'zi bilan qayta o'qiymiz:

Qadam Auth uchun nima qiladi Rad etsa
middleware cookie va sarlavhani o'qiydi, "kim"ni aniqlaydi (kanonda kimlik()) odatda rad etmaydi
guard qaror: o'tkazish, 401 yoki 403 xato tashlaydi → filtr
interceptor (oldin) log, kesh — kim ekani endi aniq —
pipe tana va parametrlar tekshiruvi 422
handler ish: "faqat o'z vazifasi" — bazada 404
interceptor (keyin), filtr javobni o'raydi; xatoni Problem Details'ga aylantiradi —

Guard'ning joyi qulay: u handler'dan oldin, lekin "kim" allaqachon ma'lum bo'lgandan keyin turadi. Pipe hali ishlamagan — anonim so'rovning tanasi umuman tekshirilmaydi.

2.3 Ulash va tartib

ts
providers: [
  JwtStrategy,
  // tartib muhim: avval kimligi (401), keyin roli (403)
  { provide: APP_GUARD, useClass: JwtAuthGuard },
  { provide: APP_GUARD, useClass: RolesGuard },
],

Bitta modul ichidagi bir nechta APP_GUARD ro'yxatdagi tartibda ishlaydi; turli modullardagi APP_GUARD lar orasidagi tartib esa kafolatlanmagan (global enhancer'lar). Shuning uchun tartibi muhim guard'lar bitta modulda, bitta ro'yxatda yoziladi. RolesGuard req.user ni o'qiydi — uni JwtAuthGuard qo'yadi. Tartib teskari bo'lsa, RolesGuard hali bo'sh req.user ni ko'radi.

APP_GUARD ni istalgan modulda yozsa bo'ladi — u baribir global. Lekin odatda AppModule da turadi, chunki "hamma marshrut yopiq" degan qaror butun ilovaga tegishli va bitta joyda ko'rinib tursin.

2.4 Biz ishga tushirdik

Uch controller: /health (@Public()), /vazifalar (hech qanday dekoratorsiz) va /admin/foydalanuvchilar (@Roles("admin")). Uch xil mijoz (NestJS 12.2.0, @nestjs/passport 12.0.0):

text
anonim GET /health                  → 200 {"holat":"ok"}
anonim GET /vazifalar               → 401 {"message":"Unauthorized","statusCode":401}
anonim GET /admin/foydalanuvchilar  → 401 {"message":"Unauthorized","statusCode":401}
aziz   GET /health                  → 200 {"holat":"ok"}
aziz   GET /vazifalar               → 200 {"vazifalar":[]}
aziz   GET /admin/foydalanuvchilar  → 403 {"message":"Forbidden resource","error":"Forbidden","statusCode":403}
admin  GET /health                  → 200 {"holat":"ok"}
admin  GET /vazifalar               → 200 {"vazifalar":[]}
admin  GET /admin/foydalanuvchilar  → 200 {"soni":3}

Jadvalni ustun bo'yicha o'qing. /vazifalar controller'ida hech qanday auth kodi yo'q — lekin anonimga 401. Bu "yopiq sukut". Anonim admin yo'liga 401 oldi, 403 emas: avval "kimsan?" (401), keyin "ruxsating bormi?" (403). Tartib shu.

Tekshirib ko'ring: /admin yo'lida anonimga 403 qaytsa, nima yomon?

Javob

403 — "sizni taniymiz, lekin ruxsat yo'q" degani. Anonimga bunday javob mantiqan noto'g'ri va frontend'ni chalg'itadi: u "kirish" sahifasiga yo'naltirmaydi. RFC 9110 ham farqlaydi: 401 — autentifikatsiya kerak (WWW-Authenticate bilan), 403 — autentifikatsiya yordam bermaydi. Shuning uchun kim ekanini tekshiradigan guard birinchi turadi.

2.5 Permission va ownership guard

Rol — eng qo'pol ruxsat. Real loyihalarda yana ikki tur uchraydi:

  • Permission guard — rol emas, aniq ruxsat: @Ruxsat("vazifalar:yozish"). Rolga ruxsatlar ro'yxati biriktiriladi, guard ro'yxatni tekshiradi. vazifalar-api da bu API kalitlarida bor: kalitda vazifalar:oqish yoki vazifalar:yozish.
  • Ownership guard — "bu vazifa sizniki mi?". Uni guard'da qilish qiyin: guard'da hali resurs yo'q, faqat :id. Ko'pincha tekshiruv xizmatda yoki bazada (RLS) qilinadi — keyingi darsning mavzusi.

2.6 Guard'ni sinash

Guard — oddiy klass, uni HTTP serversiz ham sinash mumkin. Nest testlarida Reflector haqiqiy, ExecutionContext esa kichik soxta obyekt bo'ladi: getHandler(), getClass() va switchToHttp().getRequest() ni qaytaradi. Shunda bir necha qatorda "metodda @Public() — o'tdi", "rol yo'q — false" holatlarini tekshirasiz (NestJS unit testlari).

Lekin bu yetarli emas. Unit test guard to'g'ri ekanini ko'rsatadi, lekin guard hamma joyda ulanganini ko'rsatmaydi. Global guard xatolari ulanishda bo'ladi: tartib, versiyalash, unutilgan @Public(). Ularni faqat butun ilovani ko'taradigan test ushlaydi — buni «Ko'chishda topilgan ikki xato» bo'limi yaqqol ko'rsatadi.

3. Kanon: KirishGuard va uchta dekorator

3.1 Nima farq qiladi

vazifalar-api dagi guard g'oyasi aynan shu, lekin uch farq bor:

Nest standarti Kanon Nega
@Public() @Ochiq() o'zbekcha nom
— @SessiyaKerak() profil, API kalitlarni boshqarish — faqat brauzer sessiyasi bilan, kalit bilan emas
@Roles("admin") @Rollar("admin") rol bazadan, har so'rovda (o'zgarsa darhol kuchga kiradi)
AuthGuard("jwt") ichida kimlik() middleware'da kim — guard'dan oldin (idempotentlik ham egasini bilsin)

Oxirgi qator — o'tgan darsdagi idempotentlik xatosining sababi va yechimi. "Kim" oldinroq, Express middleware'ida aniqlanadi. Guard faqat qaror qiladi: o'tkazish, 401 yoki 403.

3.2 KirishGuard qanday qaror qiladi

To'liq kod darsning "Loyiha qadami" bo'limida. Avval uning mantig'ini so'z bilan, yuqoridan pastga o'qiymiz. Har so'rovda guard beshta savol beradi va birinchi aniq javobda to'xtaydi:

  1. Rol yoki sessiya talab qilinganmi? Metodda yoki klassda @Rollar(...) yoki @SessiyaKerak() bo'lsa — ha. Bu savol birinchi, chunki u keyingi @Ochiq() ni bekor qila oladi.
  2. Yo'l ochiqmi? Talab yo'q va @Ochiq() bor — o'tkazadi. Login, ro'yxat, health shu yerda o'tadi.
  3. Auth umuman yoqilganmi? Xotira yoki fayl ombori bilan (DEV rejim) auth yo'q — hamma so'rov o'tadi, egasi esa config'dagi EGASI_ID. Bu 25–27-qismlardagi xulq: kursning oldingi darslari bazasiz ham ishlayveradi. Production'da config bu rejimni rad etadi (config validatsiyasi).
  4. Sessiya shartmi? Ha bo'lsa — faqat brauzer sessiyasi qabul qilinadi. API kaliti bilan kelgan so'rov — 401: kalit o'zini yoki boshqa kalitni yarata olmasin. Rol mos kelmasa — 403.
  5. Oddiy ma'lumot yo'lmi? Sessiya ham, API kaliti ham bo'ladi. Kalit bilan kelganda uning ruxsati tekshiriladi: o'qish (GET, HEAD) uchun vazifalar:oqish, qolgani uchun vazifalar:yozish. Yetmasa — 403. Hech biri yo'q — 401.

Beshinchi qadamni o'tgan darsda ko'rgansiz: faqat o'qish ruxsatli kalit bilan POST — 403. Aynan shu 403 idempotentlik omborida saqlanib qolgan edi.

Bu guard'da bazaga so'rov yo'q. Sessiyani kimlik() allaqachon o'qigan, rol ham sessiya bilan birga kelgan (28-qismdagi sessiyalar va foydalanuvchilar jadvallari bitta so'rovda). Guard — toza qaror funksiyasi. Shuning uchun uni 3-mashqda paketsiz model qilib yozish oson.

3.3 Guard'lar zanjiri

vazifalar-api da to'rtta global guard bor, shu tartibda:

flowchart LR
  A["KirishGuard 401/403"] --> B["CheklovGuard 429"]
  B --> C["CaptchaGuard 400"]
  C --> D["JsonTanaGuard 415"]
  D --> E["Handler"]

Nega aynan shunday? Paketsiz modelda ko'ramiz (brauzerda ishlaydi):

js
const guardlar = {
  kirish: (r) => (r.sessiya ? null : 401),
  cheklov: (r) => (r.soni > 600 ? 429 : null),
  json: (r) => (r.turi === "application/json" ? null : 415),
};

function zanjir(tartib, r) {
  for (const nom of tartib) {
    const status = guardlar[nom](r);
    if (status !== null) return `${status} (${nom})`;
  }
  return "201 (handler)";
}

const anonim = { sessiya: false, soni: 1, turi: "text/plain" };
const aziz = { sessiya: true, soni: 1, turi: "text/plain" };
console.log(zanjir(["json", "kirish", "cheklov"], anonim));
console.log(zanjir(["kirish", "cheklov", "json"], anonim));
console.log(zanjir(["kirish", "cheklov", "json"], aziz));

Konsolda:

text
415 (json)
401 (kirish)
415 (json)

Birinchi qator — noto'g'ri tartib. Anonim mehmon 415 oldi va bu bilan "bu yo'l bor va JSON kutadi" degan ma'lumotni bilib oldi. To'g'ri tartibda anonim faqat 401 ko'radi — kirmaguncha hech narsa oshkor bo'lmaydi. Kirgan Aziz esa haqiqiy xatoni (415) ko'radi. Kanon testi aynan shuni tekshiradi: "anonim + text/plain → 401 (415 emas); kirgan — 415".

4. Ko'chishda topilgan ikki xato

28-qism testlari bu ikki xatoni ko'rmasdi — ular Express ilovasi uchun yozilgan. Ikkalasi faqat Nest'da paydo bo'ldi. Kanon ularni yangi test fayli test/nest-auth.test.ts bilan ushladi va tuzatdi. Biz holatning nusxasida tuzatishni orqaga qaytarib, testlarni yurgizdik (Vitest 5.0.5, PostgreSQL 17.6) — chiqishlar shu yerdan.

4.1 Versiyasiz ochilib qolgan yo'llar

25-qismda eski versiyasiz yo'llar (/vazifalar) Deprecation sarlavhasi bilan qoldirilgan edi. Nest'ga ko'chishda buni bitta global sozlama qildi:

ts
app.enableVersioning({
  type: VersioningType.URI,
  defaultVersion: ["1", VERSION_NEUTRAL],
});

VERSION_NEUTRAL — "versiyasiz ham". Auth'gacha ziyonsiz edi: faqat vazifalar va teglar bor edi. Auth controller'lari qo'shilgach, ular ham /auth/men, /admin/foydalanuvchilar, /eksportlar/… sifatida ochildi. 28-qismda biz aynan shu xatoni — yashirin yo'llarni — OWASP API9 (noto'g'ri inventar boshqaruvi) deb topib, tuzatgandik. Nest'da u global sozlama orqali qaytdi.

Paketsiz model:

js
const kontrollerlar = [
  { yol: "vazifalar", versiya: undefined },
  { yol: "auth", versiya: undefined },
  { yol: "admin", versiya: undefined },
];

function yollar(sukut) {
  const natija = [];
  for (const k of kontrollerlar) {
    for (const v of k.versiya ?? sukut) {
      natija.push(v === "NEUTRAL" ? `/${k.yol}` : `/v${v}/${k.yol}`);
    }
  }
  return natija.join(" ");
}

console.log(yollar(["1", "NEUTRAL"]));
kontrollerlar[0].versiya = ["1", "NEUTRAL"]; // faqat vazifalar
console.log(yollar(["1"]));

Konsolda:

text
/v1/vazifalar /vazifalar /v1/auth /auth /v1/admin /admin
/v1/vazifalar /vazifalar /v1/auth /v1/admin

Ikkinchi qator — tuzatish: global sukut faqat "1", versiyasiz yo'l esa aniq belgilangan controller'larda.

Tuzatishni qaytarib, 28-qismning owasp-api testini yurgizdik:

text
× API9 inventar: versiyasiz yo'llardan faqat /vazifalar va /teglar (Deprecation bilan); /eksportlar, /auth, /admin — 404 585ms
Error: expected 404 "Not Found", got 200 "OK"
 ❯ test/owasp-api.test.ts:93:55
     92|     for (const yol of ["/eksportlar/x", "/auth/men", "/admin/foydalanu…
     93|       await request(app).get(yol).set("Cookie", aziz).expect(404);

Tarjimasi: "404 kutilgan edi, 200 keldi". /auth/men /v1 siz javob berdi. Ma'lumot oqmadi (sessiya baribir kerak), lekin hujum yuzasi ikki baravar: har himoya (tezlik cheklovi, no-store, CSRF) faqat /v1/auth uchun yozilgan bo'lsa, versiyasiz nusxa undan chetda qoladi. no-store aynan ["/v1/auth", "/v1/admin"] ga ulangan — versiyasiz /auth/men javobi keshlanardi.

Tuzatishdan keyin jonli server (biz ishga tushirdik, SAQLASH=postgres):

text
GET /health/live → 200
GET /auth/men → 404
GET /v1/auth/men → 401
GET /vazifalar → 401
GET /v1/vazifalar → 401

Tekshirib ko'ring: Versiyasiz /auth/men ham sessiya so'raydi va begona ma'lumot bermaydi. Unda bu nega "xato"?

Javob

Chunki himoya faqat guard'dan iborat emas. no-store sarlavhasi, kelajakdagi tezlik cheklovi qoidalari, proxy'dagi yo'l bo'yicha qoidalar, loglardagi filtrlar — hammasi /v1/auth yo'liga yozilgan. Versiyasiz nusxa shularning hammasidan chetda qoladi. Ikkinchi sabab — inventar: hujjatda (Swagger) yo'q yo'l hech kim kuzatmaydigan yo'l. OWASP API9 aynan shu xavfni nomlaydi: siz bilmagan eshik — siz qo'riqlamaydigan eshik.

4.2 Yopilib qolgan /health

Ikkinchi xato — teskarisi. "Yopiq sukut" o'z ishini qildi va hamma narsani yopdi, /health/live ni ham. Load balancer yoki orkestrator health yo'lini kirishsiz so'raydi. 401 olsa — "server o'lik" deb hisoblaydi va uni aylanishdan chiqaradi (Health check'lar).

Tuzatishni qaytarib, nest-auth testlarini yurgizdik. Inventar testi chiqishida - belgili qatorlarga qarang — ular javobning kalitidir:

text
× yopiq sukut: har marshrut (ALL'dan tashqari) anonim so'rovga 401; 401 bermaganlari — aynan OCHIQ ro'yxati 681ms
AssertionError: expected [ …(15) ] to deeply equal [ 'GET /health/live', …(16) ]
- Expected
+ Received
@@ -1,8 +1,6 @@
  [
-   "GET /health/live",
-   "GET /health/ready",
    "GET /v1/auth/oidc/:provayder",

Tarjimasi: "15 elementli massiv GET /health/live bilan boshlanadigan 17 elementli massivga teng bo'lishi kutilgan edi". - bilan belgilangan qatorlar — kutilgan, lekin yo'q. Ya'ni ikki health yo'li endi 401 beradi. Sarlavhalar testi ham yiqildi: GET /health/live — expected 200 "OK", got 401 "Unauthorized".

Tuzatish — bitta dekorator:

diff
+// health — kirishsiz (load balancer, k8s probe): aniq @Ochiq()
 @Controller({ path: "health", version: VERSION_NEUTRAL })
+@Ochiq()

4.3 Inventar testi qanday ishlaydi

Bu testning g'oyasi ikkala xatodan muhimroq. U DiscoveryService bilan hamma controller va metodlarni topadi (DiscoveryService), har biriga anonim so'rov yuboradi va 401 bermaganlarini yig'adi. Keyin ro'yxatni qo'lda yozilgan OCHIQ ro'yxati bilan solishtiradi:

ts
/** Kirishsiz ochiq marshrutlar — ATAYLAB ro'yxat: yangi @Ochiq()
 * qo'shilsa, shu test yiqiladi (ko'rib chiqib qo'shiladi) */
const OCHIQ = [
  "GET /health/live", "GET /health/ready",
  "POST /v1/auth/royxat", "POST /v1/auth/kirish",
  "POST /v1/auth/chiqish", "GET /v1/auth/oidc/:provayder",
  // … jami 17 ta
];

Nega 28-qismning yuzdan ortiq xavfsizlik testi bu ikki xatoni ko'rmadi? Chunki har test aniq yo'lni tekshiradi: "/v1/auth/men sessiyasiz — 401". Bu test Nest'da ham o'tdi. Xato esa ro'yxatda yo'q yo'lda edi: /auth/men ni hech kim so'ramagan, /health/live ni auth testlari umuman bilmagan. Aniq yo'llarni tekshiradigan test faqat siz o'ylagan holatni ushlaydi. Inventar testi esa savolni teskari beradi: "ilovada qanday yo'llar bor va qaysilari ochiq?" — va shu bilan siz o'ylamagan holatni ham topadi.

Natija ikki tomonlama himoya: kimdir yo'lni tasodifan ochsa — test yiqiladi; kimdir health'ni tasodifan yopsa — test yana yiqiladi. Ochiq yo'llar ro'yxati — kod review'da ko'riladigan bitta joy. Kanon testida 40 dan ortiq marshrut tekshiriladi (expect(jami).toBeGreaterThan(40)).

5. Hujumchi nigohi

  1. Unutilgan yo'l. Yangi controller himoyasiz. Global guard bilan bu imkonsiz — faqat @Ochiq() ochadi, inventar testi esa uni ko'rsatadi.
  2. Yashirin nusxa. /auth/men va /v1/auth/men — ikki eshik, himoya bittasida. Versiyalash sukutini tekshiring.
  3. Klass darajasidagi @Ochiq(). AuthController butunlay @Ochiq() (login, ro'yxat). Uning ichidagi /men esa sessiya talab qiladi. KirishGuard da metoddagi @SessiyaKerak() klassdagi @Ochiq() dan kuchli — aks holda profil hammaga ochiq bo'lardi.
  4. Guard tartibi orqali razvedka. 415 yoki 422 anonimga ko'rinsa, hujumchi yo'llar va tana formatini o'rganadi. Avval 401.
  5. Rol tokenda. Standart naqshda rol JWT ichida — 15 daqiqa eskirgan bo'lishi mumkin. Admin rolidan mahrum qilingan odam hali admin. Kanonda rol har so'rovda bazadan.

6. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
Hamma yo'l 401, login ham @Public() login'ga qo'yilmagan login, ro'yxat, health — @Public()
/health 401, orkestrator serverni o'chiradi global guard health'ni yopgan @Ochiq() / @Public()
Anonimga 403 RolesGuard birinchi APP_GUARD tartibi: auth, keyin rol
/auth/men /v1 siz ham javob beradi defaultVersion da VERSION_NEUTRAL sukut "1", neytral — aniq controller'da
Metodda @Roles ishlamaydi reflector.get(…, ctx.getClass()) getAllAndOverride([handler, klass])
Guard ishlamaydi providers da emas, @UseGuards ham yo'q { provide: APP_GUARD, useClass }

7. Mashqlar

Mashqlar kurs/mashqlar/30/57-global-guard/ da.

1-mashq (oson): bo'shliqlarni to'ldiring

  • Guard'ni global qiladigan token — { provide: , useClass: … }
  • Metod dekoratori klassnikini bosib o'tadigan Reflector metodi —
  • Kanonda @Public() ning nomi — @()
  • Kirmagan foydalanuvchi admin yo'lida oladigan status —
  • Versiyasiz yo'lni bildiruvchi Nest konstantasi —
Yechim
  • APP_GUARD — @nestjs/core dan; bitta modulda bir nechtasi bo'lsa, ro'yxat tartibida ishlaydi (modullar orasida tartib kafolatlanmagan).
  • getAllAndOverride — avval metod, topilmasa klass; getAllAndMerge — ikkalasini qo'shadi.
  • Ochiq — @Ochiq(); yana @SessiyaKerak() va @Rollar(...).
  • 401 — avval kimlik, keyin rol.
  • VERSION_NEUTRAL — kanonda faqat vazifalar va teglar controller'larida.

2-mashq (o'rta): inventar

Marshrutlar ro'yxati berilgan: har birida yol, ochiq (dekorator bor-yo'qligi) bor. inventar(marshrutlar, ruxsat) funksiyasini yozing: ochiq marshrutlardan ruxsat ro'yxatida yo'qlarini (ortiqcha) va ruxsat ro'yxatida bor, lekin endi ochiq bo'lmaganlarini (yopilgan) qaytarsin. Ishora: filter va Set.

Yechim
js
function inventar(marshrutlar, ruxsat) {
  const ochiq = new Set(
    marshrutlar.filter((m) => m.ochiq).map((m) => m.yol));
  const kutilgan = new Set(ruxsat);
  return {
    ortiqcha: [...ochiq].filter((y) => !kutilgan.has(y)),
    yopilgan: [...kutilgan].filter((y) => !ochiq.has(y)),
  };
}

const RUXSAT = ["GET /health/live", "POST /v1/auth/kirish"];
const marshrutlar = [
  { yol: "GET /health/live", ochiq: false }, // @Ochiq unutildi
  { yol: "POST /v1/auth/kirish", ochiq: true },
  { yol: "GET /v1/hisobot", ochiq: true }, // tasodifan ochilgan
  { yol: "GET /v1/vazifalar", ochiq: false },
];
console.log(inventar(marshrutlar, RUXSAT));

Konsolda:

text
{ ortiqcha: [ 'GET /v1/hisobot' ], yopilgan: [ 'GET /health/live' ] }

Bitta funksiya darsdagi ikki xatoning ikkalasini ushlaydi: tasodifan ochilgan yo'l va tasodifan yopilgan health. Kanon testi ham shunday — faqat ochiq ni dekoratordan emas, haqiqiy anonim so'rovdan oladi (401 bermadi — demak ochiq). Bu ishonchliroq: dekorator to'g'ri, lekin guard xato bo'lsa ham ushlanadi.

3-mashq (qiyin): metod klassdan kuchli

KirishGuard ning qaror mantig'ini toza funksiya qiling: qaror(meta, req). meta — { klass: {...}, metod: {...} }, ichida ochiq, sessiya, rollar bo'lishi mumkin. Qoida: rollar yoki sessiya (metod, bo'lmasa klass) bo'lsa — sessiya kerak; aks holda ochiq bo'lsa — 200; sessiya yo'q — 401; rol mos emas — 403; qolgani — 200. getAllAndOverride ni metod[k] ?? klass[k] bilan modellashtiring. Ishora: ??.

Yechim
js
function qaror(meta, req) {
  const ol = (k) => meta.metod[k] ?? meta.klass[k];
  const rollar = ol("rollar");
  const sessiyaKerak = rollar !== undefined || ol("sessiya") === true;
  if (!sessiyaKerak && ol("ochiq") === true) return 200;
  if (req.sessiya === undefined) return 401;
  if (rollar !== undefined && !rollar.includes(req.sessiya.rol)) {
    return 403;
  }
  return 200;
}

const auth = { klass: { ochiq: true } };
const login = { ...auth, metod: {} };
const men = { ...auth, metod: { sessiya: true } };
const admin = { klass: { rollar: ["admin"] }, metod: {} };
const aziz = { sessiya: { rol: "foydalanuvchi" } };
console.log("login, anonim:", qaror(login, {}));
console.log("men, anonim:", qaror(men, {}));
console.log("men, aziz:", qaror(men, aziz));
console.log("admin, aziz:", qaror(admin, aziz));

Konsolda:

text
login, anonim: 200
men, anonim: 401
men, aziz: 200
admin, aziz: 403

Ikkinchi qator — eng muhimi. AuthController klassi @Ochiq(), lekin /men metodida @SessiyaKerak(). Agar ochiq ni birinchi tekshirsak, profil anonimga ochilardi. Kanon kodidagi izoh ham shu: "metod darajasidagi talab klass darajasidagi @Ochiq dan kuchli".

8. Loyiha qadami: KirishGuard

8.1 qoriqchi.ts

Yangi fayl src/auth/qoriqchi.ts. Kanon faylidan, telefon uchun bo'lingan (izohlar qisqartirildi, kod ma'nosi aynan):

ts
// qoriqchi.ts — GLOBAL auth guard. Sukut — HAMMA marshrut
// himoyalangan ("yopiq sukut"): yangi controller qo'shib, himoyani
// unutish mumkin emas. Ochiqlari aniq belgilanadi: @Ochiq().
// Qo'shimcha talablar: @SessiyaKerak() (API kaliti emas, aynan
// brauzer sessiyasi), @Rollar("admin") (RBAC, rol bazadan).
import { Inject, Injectable, SetMetadata } from "@nestjs/common";
import type { CanActivate, ExecutionContext } from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import type { Request } from "express";
import { IlovaXatosi } from "../umumiy/xatolar.ts";
import { KirilmaganXatosi } from "./middleware.ts";
import type { Ruxsat } from "./api-kalit.ts";
import { AUTH } from "../tokenlar.ts";
import type { AuthBogliqliklari } from "./xizmat.ts";
import type { NestQoida } from "./cheklov.guard.ts";

export type Rol = "foydalanuvchi" | "admin";

const OCHIQ = "auth:ochiq";
const SESSIYA = "auth:sessiya";
const ROLLAR = "auth:rollar";

/** Kirishsiz ochiq (login, OIDC qaytishi, health). Klass/metodga */
export const Ochiq = (): MethodDecorator & ClassDecorator =>
  SetMetadata(OCHIQ, true);
/** Faqat brauzer sessiyasi (API kaliti bilan EMAS) */
export const SessiyaKerak = (): MethodDecorator & ClassDecorator =>
  SetMetadata(SESSIYA, true);
/** Rol talabi: 401 — kim ekani noma'lum, 403 — ruxsati yo'q */
export const Rollar =
  (...rollar: Rol[]): MethodDecorator & ClassDecorator =>
    SetMetadata(ROLLAR, rollar);

export class RuxsatYoqXatosi extends IlovaXatosi {
  constructor() {
    super(403, "Bu amal uchun ruxsatingiz yo'q",
      { kod: "ruxsat-yoq", sarlavha: "Ruxsat yo'q" });
  }
}

const OQISH = new Set(["GET", "HEAD"]);

@Injectable()
export class KirishGuard implements CanActivate {
  constructor(
    private readonly reflector: Reflector,
    @Inject(AUTH) private readonly auth: AuthBogliqliklari | null,
  ) {}

  canActivate(kontekst: ExecutionContext): boolean {
    const req = kontekst.switchToHttp().getRequest<Request>();
    const nishon = [kontekst.getHandler(), kontekst.getClass()];
    const rollar = this.reflector
      .getAllAndOverride<Rol[] | undefined>(ROLLAR, nishon);
    const sessiyaKerak = rollar !== undefined ||
      this.reflector.getAllAndOverride<boolean | undefined>(
        SESSIYA, nishon) === true;
    // metoddagi SessiyaKerak/Rollar klassdagi @Ochiq dan kuchli
    if (!sessiyaKerak && this.reflector.getAllAndOverride<
      boolean | undefined>(OCHIQ, nishon) === true) {
      return true;
    }
    // DEV rejim (auth yo'q, xotira/fayl ombori): egasi — EGASI_ID
    if (this.auth === null) return true;
    if (sessiyaKerak) {
      if (req.sessiya === undefined || req.apiKalit !== undefined) {
        throw new KirilmaganXatosi();
      }
      if (rollar !== undefined && !rollar.includes(req.sessiya.rol)) {
        throw new RuxsatYoqXatosi();
      }
      return true;
    }
    // Ma'lumot yo'llari: sessiya YOKI API kaliti (ruxsati bilan:
    // GET/HEAD — vazifalar:oqish, qolgani — vazifalar:yozish)
    if (req.apiKalit !== undefined) {
      const kerak: Ruxsat = OQISH.has(req.method)
        ? "vazifalar:oqish" : "vazifalar:yozish";
      if (!req.apiKalit.ruxsatlar.includes(kerak)) {
        throw new IlovaXatosi(403, `Kalitda "${kerak}" ruxsati yo'q`,
          { kod: "ruxsat-yoq", sarlavha: "Ruxsat yo'q" });
      }
      return true;
    }
    if (req.sessiya === undefined) throw new KirilmaganXatosi();
    return true;
  }
}

/** Ma'lumot yo'llari: foydalanuvchi (yoki API kaliti egasi) bo'yicha
 * daqiqasiga 600 so'rov. DEV rejimda (auth yo'q) cheklov yo'q */
export const API_CHEKLOVI: NestQoida = {
  nom: "api", limit: 600, oynaMs: 60_000,
  kalit: (req) =>
    (req.egasi === undefined ? undefined : String(req.egasi)),
};

Bir detalga e'tibor bering: KirishGuard hech qachon false qaytarmaydi. Rad etish kerak bo'lsa, u o'z xatosini tashlaydi — KirilmaganXatosi (401) yoki RuxsatYoqXatosi (403). Nega? false qaytsa, Nest o'zi ForbiddenException yaratadi: har doim 403 va matni "Forbidden resource". Bunda ikki narsa yo'qoladi. Birinchisi — 401 bilan 403 farqi: anonim ham 403 olardi. Ikkinchisi — javob shakli: vazifalar-api ning xato filtri IlovaXatosi ni Problem Details'ga aylantiradi, kod maydoni bilan (ruxsat-yoq). Frontend shu kod ga qarab qaror qiladi. Demo'dagi RolesGuard uchun false yetarli edi, kanonga esa aniq xato kerak.

API_CHEKLOVI — tezlik cheklovi qoidasi. Uning turi NestQoida @nestjs/throttler darsida yoziladigan cheklov.guard.ts dan keladi.

8.2 Boshqa o'zgarishlar

  • src/ilova.ts — enableVersioning({ type: VersioningType.URI, defaultVersion: "1" }); VERSION_NEUTRAL importi olib tashlanadi.
  • src/vazifalar/vazifalar.controller.ts va teglar.controller.ts — @Controller({ path: "vazifalar", version: ["1", VERSION_NEUTRAL] }) (teglar uchun ham shunday). Eski yo'llar faqat shu ikkitasida qoladi.
  • src/salomatlik/salomatlik.module.ts — @Ochiq().
  • src/auth/admin.controller.ts — yangi: 28-qism admin.ts routerining Nest shakli. Klass darajasida @Rollar("admin"), uchta yo'l: ro'yxat, bloklash, blokdan chiqarish. Bloklashda foydalanuvchining hamma sessiyasi darhol bekor qilinadi — o'tgan darsdagi "15 daqiqa muammosi" bu yerda yo'q.

admin.controller.ts boshi (kanon faylidan):

ts
@ApiTags("Admin")
@Controller("admin")
@Rollar("admin")
export class AdminController {
  constructor(private readonly auth: AuthService) {}

8.3 Tekshiruv: to'rt kutilgan xato

Shu kesimni biz kanon fayllaridan yig'ib tekshirdik (npx tsc --noEmit):

text
src/auth/qoriqchi.ts(14,32): error TS2307: Cannot find module './cheklov.guard.ts' or its corresponding type declarations.
src/auth/qoriqchi.ts(67,90): error TS7006: Parameter 'req' implicitly has an 'any' type.
src/ilova.ts(23,15): error TS2305: Module '"./vazifalar/ombor.ts"' has no exported member 'OmborFabrikasi'.
src/ilova.ts(59,91): error TS2353: Object literal may only specify known properties, and 'auth' does not exist in type 'IlovaBogliqliklari'.

Tarjimasi: birinchi ikkitasi yangi — cheklov.guard.ts hali yo'q (TS2307), shuning uchun NestQoida turi noma'lum va kalit: (req) => … dagi req yashirin any (TS7006). Ikkinchi xato birinchisining oqibati. Qolgan ikkitasi o'tgan darsdan. Qator raqamlari kanonning asl faylidagi qatorlar — sizning bo'lingan nusxangizda boshqacha bo'ladi, xato kodi va matni esa bir xil.

Faqat shu to'rt xato bo'lsa — to'g'ri. Hali commit qilmang.

9. Real ishda

  • Har jiddiy Nest loyihasida global auth guard bor. Kod review'dagi eng muhim savol: "bu yo'lga @Public() nega kerak?".
  • Inventar testlari — katta jamoalarda xavfsizlik jamoasi talab qiladi: "ochiq yo'llar ro'yxati" — alohida fayl, uni o'zgartirish xavfsizlik ko'rigidan o'tadi.
  • Health va metrikalar — doim ochiq (yoki alohida port), lekin ma'lumot bermaydi. Bu NestJS, Spring, ASP.NET — hammasida bir xil muammo.
  • Intervyu: "Global guard'da login'ni qanday ochiq qoldirasiz?", "getAllAndOverride va getAllAndMerge farqi?", "401 va 403 farqi?".

Xulosa

  • Global guard (APP_GUARD) — "yopiq sukut": har yangi yo'l himoyalangan, ochish — aniq @Public()/@Ochiq().
  • getAllAndOverride([handler, klass]) — metod klassni bosib o'tadi; kanonda metoddagi @SessiyaKerak() klassdagi @Ochiq() dan kuchli.
  • Guard tartibi: kimlik (401/403) → tezlik (429) → CAPTCHA → 415; anonim hech narsani bilib olmasin.
  • Topilgan xatolar: VERSION_NEUTRAL sukutda auth/admin/eksport'ni versiyasiz ochgan; global guard /health ni yopgan. Ikkalasini inventar testi ushlaydi.
  • Loyiha: KirishGuard qo'shildi; tip to'rt kutilgan xato beradi — keyingi darslarda yo'qoladi.

Keyingi dars: CASL bilan policy-based avtorizatsiya — rol yetmaganda: "faqat o'z vazifasini". CASL'ni o'rganamiz, keyin vazifalar-api egalikni nega Postgres RLS'ga topshirganini ko'ramiz.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
NestJS global auth guard: APPGUARD, @Public va @Roles — "yopiq sukut" — IlmHamroh