Mundarija (30)
- Bu darsda
- 1. Nega bu kerak?
- 2. Nest standarti: JwtAuthGuard + @Public + @Roles
- 2.1 Dekoratorlar
- 2.2 Guard'lar
- 2.3 Ulash va tartib
- 2.4 Biz ishga tushirdik
- 2.5 Permission va ownership guard
- 2.6 Guard'ni sinash
- 3. Kanon: KirishGuard va uchta dekorator
- 3.1 Nima farq qiladi
- 3.2 KirishGuard qanday qaror qiladi
- 3.3 Guard'lar zanjiri
- 4. Ko'chishda topilgan ikki xato
- 4.1 Versiyasiz ochilib qolgan yo'llar
- 4.2 Yopilib qolgan /health
- 4.3 Inventar testi qanday ishlaydi
- 5. Hujumchi nigohi
- 6. Ko'p uchraydigan xatolar
- 7. Mashqlar
- 1-mashq (oson): bo'shliqlarni to'ldiring
- 2-mashq (o'rta): inventar
- 3-mashq (qiyin): metod klassdan kuchli
- 8. Loyiha qadami: KirishGuard
- 8.1 qoriqchi.ts
- 8.2 Boshqa o'zgarishlar
- 8.3 Tekshiruv: to'rt kutilgan xato
- 9. Real ishda
- Xulosa
- Manbalar
NestJS global auth guard: APP_GUARD, @Public va @Roles — "yopiq sukut"
Qisqacha: Global guard —
APP_GUARDorqali ulangan va hamma marshrutda ishlaydigan guard. Nest standarti:JwtAuthGuardhammasini yopadi,@Public()dekoratorli yo'llar ochiq qoladi,@Roles("admin")esaRolesGuardorqali 403 beradi. Bu "yopiq sukut" tamoyili: yangi controller qo'shib himoyani unutish mumkin emas.vazifalar-apida xuddi shu g'oyaKirishGuardva@Ochiq/@SessiyaKerak/@Rollarnomlari bilan yozilgan. Ko'chishda ikki xato topildi: global versiya sozlamasi auth va admin yo'llarini/v1siz ham ochib qo'ygan, global guard esa/healthni yopib qo'ygan.
Bu darsda
APP_GUARD,@Public(),@Roles()vaRolesGuardbilan "yopiq sukut" auth yozasiz.Reflector.getAllAndOverridemetod 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-apigaKirishGuardni 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):
// 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:
@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:
@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
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):
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:
/adminyo'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-apida bu API kalitlarida bor: kalitdavazifalar:oqishyokivazifalar: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:
- 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. - Yo'l ochiqmi? Talab yo'q va
@Ochiq()bor — o'tkazadi. Login, ro'yxat, health shu yerda o'tadi. - 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). - 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.
- 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 uchunvazifalar: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):
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:
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:
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:
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:
/v1/vazifalar /vazifalar /v1/auth /auth /v1/admin /admin
/v1/vazifalar /vazifalar /v1/auth /v1/adminIkkinchi qator — tuzatish: global sukut faqat "1", versiyasiz yo'l esa aniq belgilangan controller'larda.
Tuzatishni qaytarib, 28-qismning owasp-api testini yurgizdik:
× 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):
GET /health/live → 200
GET /auth/men → 404
GET /v1/auth/men → 401
GET /vazifalar → 401
GET /v1/vazifalar → 401Tekshirib ko'ring: Versiyasiz
/auth/menham 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:
× 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:
+// 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:
/** 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
- Unutilgan yo'l. Yangi controller himoyasiz. Global guard bilan bu imkonsiz — faqat
@Ochiq()ochadi, inventar testi esa uni ko'rsatadi. - Yashirin nusxa.
/auth/menva/v1/auth/men— ikki eshik, himoya bittasida. Versiyalash sukutini tekshiring. - Klass darajasidagi
@Ochiq().AuthControllerbutunlay@Ochiq()(login, ro'yxat). Uning ichidagi/menesa sessiya talab qiladi.KirishGuardda metoddagi@SessiyaKerak()klassdagi@Ochiq()dan kuchli — aks holda profil hammaga ochiq bo'lardi. - Guard tartibi orqali razvedka. 415 yoki 422 anonimga ko'rinsa, hujumchi yo'llar va tana formatini o'rganadi. Avval 401.
- 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/coredan; 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
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:
{ 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
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:
login, anonim: 200
men, anonim: 401
men, aziz: 200
admin, aziz: 403Ikkinchi 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):
// 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_NEUTRALimporti olib tashlanadi.src/vazifalar/vazifalar.controller.tsvateglar.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-qismadmin.tsrouterining 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):
@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):
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?", "
getAllAndOverridevagetAllAndMergefarqi?", "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_NEUTRALsukutda auth/admin/eksport'ni versiyasiz ochgan; global guard/healthni yopgan. Ikkalasini inventar testi ushlaydi. - Loyiha:
KirishGuardqo'shildi;tipto'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
- NestJS hujjati — Authentication (global guard,
@Public()): https://docs.nestjs.com/security/authentication#enable-authentication-globally - NestJS hujjati — Authorization (
@Roles,RolesGuard): https://docs.nestjs.com/security/authorization - NestJS hujjati — Versioning (
VERSION_NEUTRAL,defaultVersion): https://docs.nestjs.com/techniques/versioning - OWASP API Security Top 10 (2023) — API9 Improper Inventory Management: https://owasp.org/API-Security/editions/2023/en/0xa9-improper-inventory-management/
- RFC 9110 — 401 va 403: https://www.rfc-editor.org/rfc/rfc9110#name-403-forbidden
- Kanon (IlmHamroh, 2026-11-01):
KirishGuard,test/nest-auth.test.ts, guard tartibi. Demo (JwtAuthGuard+RolesGuard), tuzatishlar qaytarilgan nusxadagi test chiqishlari, jonli server va oraliqtsckesimi — biz ishga tushirdik (2026-11-02, NestJS 12.2.0, Node 24.21.0, PostgreSQL 17.6).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!