Passport (autentifikatsiya)
Passport - bu eng mashhur Node.js authentication kutubxonasi bo'lib, community orasida yaxshi tanilgan va ko'plab production ilovalarda muvaffaqiyatli ishlatiladi. Ushbu kutubxonan
Passport - bu eng mashhur Node.js authentication kutubxonasi bo'lib, community orasida yaxshi tanilgan va ko'plab production ilovalarda muvaffaqiyatli ishlatiladi. Ushbu kutubxonani @nestjs/passport moduli yordamida Nest ilovasi bilan integratsiya qilish oson. Yuqori darajada Passport quyidagi qadamlarni bajaradi:
- Foydalanuvchini uning "credentials" ma'lumotlarini tekshirish orqali autentifikatsiya qiladi (masalan, username/password, JSON Web Token (JWT) yoki Identity Provider'dan olingan identity token)
- Authenticated holatni boshqaradi (masalan, JWT kabi portable token berish yoki Express session yaratish orqali)
- Autentifikatsiyadan o'tgan foydalanuvchi haqidagi ma'lumotni keyinchalik route handler'larda ishlatish uchun
Requestobyektiga biriktiradi
Passport turli authentication mexanizmlarini amalga oshiradigan boy strategy ekotizimiga ega. Konsepsiya jihatidan sodda bo'lsa-da, tanlash mumkin bo'lgan Passport strategiyalari juda ko'p va xilma-xildir. Passport bu turli qadamlarni yagona standart pattern ichiga joylaydi, @nestjs/passport esa ushbu pattern'ni Nest'ga xos construct'lar bilan o'rab, standartlashtiradi.
Ushbu bobda ushbu kuchli va moslashuvchan modullar yordamida RESTful API server uchun to'liq end-to-end authentication yechimini implement qilamiz. Bu yerda tushuntirilgan g'oyalar yordamida authentication sxemangizni moslashtirish uchun istalgan Passport strategy'ni implement qilishingiz mumkin. Ushbu bobdagi qadamlarni kuzatib, to'liq ishlaydigan namunani qurishingiz mumkin.
Authentication talablari
Talablarni aniq belgilab olaylik. Ushbu use case'da client'lar avval username va password bilan autentifikatsiya qiladi. Autentifikatsiyadan so'ng server JWT beradi va u keyingi so'rovlarda autentifikatsiyani isbotlash uchun authorization header'dagi bearer token sifatida yuboriladi. Shuningdek, faqat yaroqli JWT bo'lgan so'rovlar kira oladigan protected route ham yaratamiz.
Avval birinchi talabdan boshlaymiz: foydalanuvchini autentifikatsiya qilish. So'ngra uni JWT berish bilan kengaytiramiz. Oxirida so'rovda yaroqli JWT borligini tekshiradigan protected route yaratamiz.
Avval kerakli paketlarni o'rnatishimiz kerak. Passport username/password asosidagi authentication mexanizmini implement qiladigan passport-local strategy'sini taqdim etadi va bu use case'ning ushbu qismi uchun mos keladi.
1$ npm install --save @nestjs/passport passport passport-local
2$ npm install --save-dev @types/passport-localQaysi Passport strategy'ni tanlamang, har doim @nestjs/passport va passport paketlari kerak bo'ladi. Keyin siz qurayotgan authentication strategy'ni implement qiladigan strategy'ga xos paketni (passport-jwt yoki passport-local kabi) ham o'rnatishingiz kerak. Bundan tashqari, yuqoridagi @types/passport-local misolida bo'lgani kabi, istalgan Passport strategy uchun type definition'larni ham o'rnatishingiz mumkin. Bu TypeScript kod yozishda yordam beradi.
Passport strategiyalarini implement qilish
Endi authentication funksiyasini implement qilishga tayyormiz. Avval har qanday Passport strategy uchun ishlatiladigan umumiy jarayonni ko'rib chiqamiz. Passport'ni o'zicha kichik freymvork deb tasavvur qilish foydali. Uning jozibasi shundaki, authentication jarayonini siz implement qilayotgan strategy'ga qarab sozlanadigan bir nechta asosiy qadamga abstraksiyalaydi. U freymvorkka o'xshaydi, chunki siz uni customization parametrlar (oddiy JSON obyektlari) va Passport kerakli vaqtda chaqiradigan callback funksiyalar ko'rinishidagi custom kod bilan sozlaysiz. @nestjs/passport moduli ushbu freymvorkni Nest uslubidagi paketga o'raydi va uni Nest ilovasiga integratsiya qilishni osonlashtiradi. Quyida @nestjs/passport dan foydalanamiz, ammo avval vanilla Passport qanday ishlashini ko'raylik.
Vanilla Passport'da strategy'ni sozlash uchun ikkita narsa kerak bo'ladi:
- O'sha strategy'ga xos opsiyalar to'plami. Masalan, JWT strategy'sida tokenlarni imzolash uchun secret berishingiz mumkin.
- "verify callback", ya'ni Passport'ga user store bilan qanday ishlashni aytadigan joy (foydalanuvchi account'lari shu yerda boshqariladi). Bu yerda foydalanuvchi mavjudligini (va/yoki yangi foydalanuvchi yaratishni) hamda credential'lari yaroqliligini tekshirasiz. Passport kutubxonasi validatsiya muvaffaqiyatli bo'lsa to'liq user obyektini, muvaffaqiyatsiz bo'lsa
nullqaytishini kutadi (muvaffaqiyatsizlik degani foydalanuvchi topilmagan yokipassport-localholatida parol mos kelmagan bo'lishi mumkin).
@nestjs/passport bilan Passport strategy PassportStrategy class'ini extend qilish orqali sozlanadi. Strategy opsiyalari (yuqoridagi 1-band) subclass ichidagi super() metodiga options obyektini uzatish orqali beriladi. Verify callback (yuqoridagi 2-band) esa subclass ichida validate() metodini implement qilish orqali taqdim etiladi.
Boshlash uchun AuthModule va uning ichida AuthService generatsiya qilamiz:
1$ nest g module auth
2$ nest g service authAuthService ni implement qilayotganda user operatsiyalarini UsersService ichiga kapsullash foydali bo'ladi, shuning uchun hozir shu modul va service'ni ham yarataylik:
1$ nest g module users
2$ nest g service usersYaratilgan fayllarning standart tarkibini quyidagidek almashtiring. Namuna ilovamizda UsersService xotirada saqlanadigan hard-coded foydalanuvchilar ro'yxatini va username orqali topish metodini saqlaydi. Real ilovada esa aynan shu yerda user model va persistence layer quriladi, o'zingiz tanlagan kutubxona (masalan, TypeORM, Sequelize, Mongoose va boshqalar) bilan.
1import { Injectable } from '@nestjs/common';
2
3// This should be a real class/interface representing a user entity
4export type User = any;
5
6@Injectable()
7export class UsersService {
8 private readonly users = [
9 {
10 userId: 1,
11 username: 'john',
12 password: 'changeme',
13 },
14 {
15 userId: 2,
16 username: 'maria',
17 password: 'guess',
18 },
19 ];
20
21 async findOne(username: string): Promise<User | undefined> {
22 return this.users.find(user => user.username === username);
23 }
24}UsersModule ichida kerakli yagona o'zgarish - UsersService ni @Module dekoratoridagi exports massiviga qo'shish. Shunda u modul tashqarisida ham ko'rinadi (tez orada uni AuthService ichida ishlatamiz).
1import { Module } from '@nestjs/common';
2import { UsersService } from './users.service';
3
4@Module({
5 providers: [UsersService],
6 exports: [UsersService],
7})
8export class UsersModule {}AuthService ning vazifasi foydalanuvchini olish va parolni tekshirishdan iborat. Shu maqsadda validateUser() metodini yaratamiz. Quyidagi kodda qaytarishdan oldin user obyektidan password xossasini olib tashlash uchun qulay ES6 spread operator'dan foydalanamiz. Bir ozdan keyin validateUser() metodini Passport local strategy ichidan chaqiramiz.
1import { Injectable } from '@nestjs/common';
2import { UsersService } from '../users/users.service';
3
4@Injectable()
5export class AuthService {
6 constructor(private usersService: UsersService) {}
7
8 async validateUser(username: string, pass: string): Promise<any> {
9 const user = await this.usersService.findOne(username);
10 if (user && user.password === pass) {
11 const { password, ...result } = user;
12 return result;
13 }
14 return null;
15 }
16}Albatta, real ilovada parolni plain text ko'rinishida saqlamaysiz. Buning o'rniga bcrypt kabi, salted one-way hash algoritmiga ega kutubxonadan foydalanasiz. Bu yondashuvda faqat hash qilingan parollar saqlanadi va keyin saqlangan parol kelayotgan parolning hash qilingan versiyasi bilan solishtiriladi. Shunday qilib parol hech qachon plain text ko'rinishida saqlanmaydi yoki oshkor qilinmaydi. Namuna ilovani sodda saqlash uchun biz bu qoidani buzib, plain text ishlatyapmiz. Buni real ilovangizda qilmang!
Endi AuthModule ni yangilab, UsersModule ni import qilamiz.
1import { Module } from '@nestjs/common';
2import { AuthService } from './auth.service';
3import { UsersModule } from '../users/users.module';
4
5@Module({
6 imports: [UsersModule],
7 providers: [AuthService],
8})
9export class AuthModule {}Passport local'ni implement qilish
Endi Passport local authentication strategy ni implement qilishimiz mumkin. auth papkasi ichida local.strategy.ts faylini yarating va quyidagi kodni qo'shing:
1import { Strategy } from 'passport-local';
2import { PassportStrategy } from '@nestjs/passport';
3import { Injectable, UnauthorizedException } from '@nestjs/common';
4import { AuthService } from './auth.service';
5
6@Injectable()
7export class LocalStrategy extends PassportStrategy(Strategy) {
8 constructor(private authService: AuthService) {
9 super();
10 }
11
12 async validate(username: string, password: string): Promise<any> {
13 const user = await this.authService.validateUser(username, password);
14 if (!user) {
15 throw new UnauthorizedException();
16 }
17 return user;
18 }
19}Biz oldin Passport strategiyalari uchun tasvirlangan umumiy recipe'ga amal qildik. passport-local holatida qo'shimcha konfiguratsiya opsiyalari yo'q, shu sabab constructor oddiygina super() ni options obyektisiz chaqiradi.
Passport strategy xatti-harakatini moslashtirish uchun super() chaqiruviga options obyektini uzatish mumkin. Bu misolda passport-local strategy standart holatda request body ichida username va password nomli xossalarni kutadi. Boshqa property nomlarini berish uchun options obyektini uzating, masalan: super({{ '{' }} usernameField: 'email' {{ '}' }}). Batafsil ma'lumot uchun Passport documentation ni ko'ring.
Biz validate() metodini ham implement qildik. Har bir strategy uchun Passport verify funksiyasini (@nestjs/passport ichida validate() orqali implement qilinadi) strategy'ga xos parametrlar to'plami bilan chaqiradi. Local strategy uchun Passport quyidagi signature'ga ega validate() metodini kutadi: validate(username: string, password:string): any.
Validatsiyaning asosiy qismi AuthService ichida (UsersService yordamida) bajariladi, shuning uchun bu metod ancha sodda. Har qanday Passport strategy uchun validate() metodi o'xshash pattern'ga amal qiladi, farq faqat credential'lar qanday ifodalanishida bo'ladi. Agar foydalanuvchi topilsa va credential'lar to'g'ri bo'lsa, user obyekt qaytariladi va Passport o'z vazifalarini tugatadi (masalan, Request obyektida user xossasini yaratadi), keyin request pipeline davom etadi. Agar foydalanuvchi topilmasa, exception tashlaymiz va uni exceptions layer ga topshiramiz.
Odatda har bir strategy uchun validate() metodidagi asosiy farq - foydalanuvchi mavjudligi va yaroqliligi qanday tekshirilishidadir. Masalan, JWT strategy'da talabga qarab decoded token ichidagi userId bizning user database'dagi yozuvga mos keladimi yoki revoked token'lar ro'yxatida bormi - shuni tekshirishingiz mumkin. Shuning uchun subclass yozish va strategy'ga xos validatsiyani implement qilish pattern'i izchil, chiroyli va kengaytiriladigan yondashuv hisoblanadi.
Endi AuthModule ni Passport'ning hozirgina yaratgan imkoniyatlaridan foydalanadigan qilib sozlashimiz kerak. auth.module.ts ni quyidagicha yangilang:
1import { Module } from '@nestjs/common';
2import { AuthService } from './auth.service';
3import { UsersModule } from '../users/users.module';
4import { PassportModule } from '@nestjs/passport';
5import { LocalStrategy } from './local.strategy';
6
7@Module({
8 imports: [UsersModule, PassportModule],
9 providers: [AuthService, LocalStrategy],
10})
11export class AuthModule {}Built-in Passport Guard'lari
Guards bo'limida Guard'larning asosiy vazifasi request route handler tomonidan qayta ishlanishi yoki yo'qligini aniqlash ekani tushuntirilgan. Bu yerda ham shu to'g'ri bo'lib qoladi va biz bu standart imkoniyatdan tez orada foydalanamiz. Ammo @nestjs/passport modulidan foydalanganda boshida biroz chalkashtirishi mumkin bo'lgan kichik bir farq paydo bo'ladi, shuning uchun avval shuni muhokama qilamiz. Authentication nuqtai nazaridan ilovangiz ikki holatdan birida bo'lishi mumkin:
- foydalanuvchi/client hali log in qilmagan (
authenticatedemas) - foydalanuvchi/client log in qilgan (
authenticated)
Birinchi holatda (foydalanuvchi log in qilmaganida) biz ikki xil vazifani bajarishimiz kerak:
-
Authentication qilinmagan foydalanuvchi kira oladigan route'larni cheklash, ya'ni himoyalangan route'larga kirishni rad etish. Buni odatdagi usulda, protected route'larga Guard qo'yish orqali qilamiz. Kutilganidek, bu Guard ichida valid JWT mavjudligini tekshiramiz, shuning uchun JWT muvaffaqiyatli berila boshlagach bu Guard'ga qaytamiz.
-
Oldin authentication qilinmagan foydalanuvchi log in qilishga uringanda authentication bosqichini boshlash. Aynan shu bosqichda biz valid foydalanuvchiga JWT beramiz. Bir oz o'ylab ko'rsak, authentication'ni boshlash uchun
username/passwordcredential'lariniPOSTqilishimiz kerak bo'ladi, shu sababPOST /auth/loginroute'ini yaratamiz. Bu esa savol tug'diradi: shu route ichida passport-local strategy qanday ishga tushiriladi?
Javob oddiy: Guard'ning boshqa, biroz farqli turi orqali. @nestjs/passport moduli bu ish uchun built-in Guard beradi. Bu Guard Passport strategy'ni ishga tushiradi va yuqorida aytilgan bosqichlarni boshlaydi: credential'larni olish, verify funksiyani ishga tushirish, user property yaratish va hokazo.
Yuqoridagi ikkinchi holat esa (foydalanuvchi log in qilgan bo'lsa) himoyalangan route'larga kirishni ruxsat etish uchun biz allaqachon ko'rib chiqqan standart Guard turiga tayanadi.
Login route'i
Endi strategy tayyor bo'lgani uchun oddiy /auth/login route'ini implement qilamiz va passport-local flow'ni boshlash uchun built-in Guard'ni qo'llaymiz.
app.controller.ts faylini ochib, tarkibini quyidagiga almashtiring:
1import { Controller, Request, Post, UseGuards } from '@nestjs/common';
2import { AuthGuard } from '@nestjs/passport';
3
4@Controller()
5export class AppController {
6 @UseGuards(AuthGuard('local'))
7 @Post('auth/login')
8 async login(@Request() req) {
9 return req.user;
10 }
11}@UseGuards(AuthGuard('local')) bilan biz passport-local strategy'ni kengaytirganimizda @nestjs/passport biz uchun avtomatik yaratgan AuthGuard'dan foydalanayapmiz. Keling, buni ajratib ko'ramiz. Bizning Passport local strategy'mizning standart nomi 'local'. Shu nomni @UseGuards() dekoratorida ko'rsatib, uni passport-local package taqdim etgan kod bilan bog'laymiz. Bu, ilovamizda bir nechta Passport strategy bo'lganda qaysi biri ishga tushirilishini aniq ajratish uchun kerak bo'ladi, chunki har biri o'ziga xos AuthGuard yaratishi mumkin. Hozircha bitta strategy bor, lekin birozdan keyin ikkinchisini ham qo'shamiz, shuning uchun bunday ajratish zarur.
Route'ni test qilish uchun hozircha /auth/login route'i shunchaki user'ni qaytarsin. Bu Passport'ning yana bir foydali imkoniyatini ko'rsatadi: Passport validate() metodidan qaytgan qiymat asosida avtomatik ravishda user obyektini yaratadi va uni Request obyektiga req.user sifatida biriktiradi. Keyinroq biz buni JWT yaratib qaytaradigan kod bilan almashtiramiz.
Bu API route'lari bo'lgani uchun ularni keng tarqalgan cURL kutubxonasi orqali test qilamiz. UsersService ichida hard-code qilingan istalgan user obyektidan foydalanishingiz mumkin.
1$ # POST to /auth/login
2$ curl -X POST http://localhost:3000/auth/login -d '{"username": "john", "password": "changeme"}' -H "Content-Type: application/json"
3$ # result -> {"userId":1,"username":"john"}Bu ishlasa ham, strategy nomini AuthGuard() ichiga to'g'ridan-to'g'ri berish codebase ichida magic string'lar paydo bo'lishiga olib keladi. Shuning o'rniga quyidagidek o'z class'ingizni yaratishni tavsiya qilamiz:
1import { Injectable } from '@nestjs/common';
2import { AuthGuard } from '@nestjs/passport';
3
4@Injectable()
5export class LocalAuthGuard extends AuthGuard('local') {}Endi /auth/login route handler'ini yangilab, LocalAuthGuard dan foydalanishimiz mumkin:
1@UseGuards(LocalAuthGuard)
2@Post('auth/login')
3async login(@Request() req) {
4 return req.user;
5}Logout route'i
Log out qilish uchun req.logout() ni chaqirib foydalanuvchi session'ini tozalaydigan qo'shimcha route yaratishimiz mumkin. Bu session-based authentication'da odatiy yondashuv, lekin JWT uchun qo'llanmaydi.
1@UseGuards(LocalAuthGuard)
2@Post('auth/logout')
3async logout(@Request() req) {
4 return req.logout();
5}JWT funksionalligi
Endi auth tizimimizning JWT qismiga o'tishga tayyormiz. Talablarni yana bir bor ko'rib chiqib, aniqlashtiraylik:
- Foydalanuvchilar
username/passwordbilan authentication qila olishi va keyinchalik protected API endpoint'larga murojaat qilish uchun JWT olishi kerak. Bunga ancha yaqinlashdik. Uni yakunlash uchun JWT beradigan kodni yozishimiz kerak. - Valid JWT bearer token mavjud bo'lsa ishlaydigan protected API route'larini yaratish kerak
JWT talablarini qo'llab-quvvatlash uchun yana bir nechta package o'rnatishimiz kerak bo'ladi:
1$ npm install --save @nestjs/jwt passport-jwt
2$ npm install --save-dev @types/passport-jwt@nestjs/jwt package'i (bu yerda ko'proq ma'lumot bor) JWT bilan ishlashni osonlashtiradigan utility package hisoblanadi. passport-jwt esa JWT strategy'ni implement qiladigan Passport package'i, @types/passport-jwt esa TypeScript type definition'larini beradi.
Endi POST /auth/login request'i qanday qayta ishlanishini yaqinroq ko'rib chiqamiz. Biz route'ni passport-local strategy taqdim etgan built-in AuthGuard bilan dekoratsiya qildik. Bu shuni anglatadiki:
- Route handler faqat foydalanuvchi validatsiyadan o'tgandagina chaqiriladi
reqparametri ichidauserproperty bo'ladi (Passport uni passport-local authentication flow davomida to'ldiradi)
Shuni hisobga olib, endi nihoyat haqiqiy JWT yaratib, uni shu route'dan qaytarishimiz mumkin. Service'lar modullarga toza ajratilgan bo'lishi uchun JWT yaratishni authService ichida qilamiz. auth papkasidagi auth.service.ts faylini ochib, login() metodini qo'shing va JwtService ni quyidagidek import qiling:
1import { Injectable } from '@nestjs/common';
2import { UsersService } from '../users/users.service';
3import { JwtService } from '@nestjs/jwt';
4
5@Injectable()
6export class AuthService {
7 constructor(
8 private usersService: UsersService,
9 private jwtService: JwtService
10 ) {}
11
12 async validateUser(username: string, pass: string): Promise<any> {
13 const user = await this.usersService.findOne(username);
14 if (user && user.password === pass) {
15 const { password, ...result } = user;
16 return result;
17 }
18 return null;
19 }
20
21 async login(user: any) {
22 const payload = { username: user.username, sub: user.userId };
23 return {
24 access_token: this.jwtService.sign(payload),
25 };
26 }
27}Biz @nestjs/jwt kutubxonasidan foydalanayapmiz, u user obyektining ma'lum property'lari asosida JWT yaratish uchun sign() funksiyasini beradi. So'ng uni faqat bitta access_token property'ga ega oddiy obyekt ko'rinishida qaytaramiz. Eslatma: userId qiymatini JWT standartlariga mos bo'lishi uchun sub nomli property ichida saqlayapmiz. JwtService provider'ini AuthService ichiga inject qilishni unutmang.
Endi AuthModule ni yangilab, yangi dependency'larni import qilishimiz va JwtModule ni sozlashimiz kerak.
Avval auth papkasi ichida constants.ts faylini yaratib, quyidagi kodni qo'shing:
1export const jwtConstants = {
2 secret: 'DO NOT USE THIS VALUE. INSTEAD, CREATE A COMPLEX SECRET AND KEEP IT SAFE OUTSIDE OF THE SOURCE CODE.',
3};Biz bundan JWT'ni sign qilish va verify qilish bosqichlari o'rtasida bir xil key'ni ulashish uchun foydalanamiz.
Bu key'ni ommaga oshkor qilmang. Biz bu yerda kod nima qilayotganini aniq ko'rsatish uchun shunday yozdik, lekin production tizimda bu key'ni albatta himoya qilishingiz kerak. Masalan, secrets vault, environment variable yoki configuration service orqali.
Endi auth papkasidagi auth.module.ts faylini ochib, quyidagicha yangilang:
1import { Module } from '@nestjs/common';
2import { AuthService } from './auth.service';
3import { LocalStrategy } from './local.strategy';
4import { UsersModule } from '../users/users.module';
5import { PassportModule } from '@nestjs/passport';
6import { JwtModule } from '@nestjs/jwt';
7import { jwtConstants } from './constants';
8
9@Module({
10 imports: [
11 UsersModule,
12 PassportModule,
13 JwtModule.register({
14 secret: jwtConstants.secret,
15 signOptions: { expiresIn: '60s' },
16 }),
17 ],
18 providers: [AuthService, LocalStrategy],
19 exports: [AuthService],
20})
21export class AuthModule {}Biz JwtModule ni register() orqali configuration obyekt uzatib sozlaymiz. Nest JwtModule haqida ko'proq ma'lumotni bu yerda, mavjud konfiguratsiya opsiyalari haqida esa bu yerda ko'rishingiz mumkin.
Endi /auth/login route'ini JWT qaytaradigan qilib yangilashimiz mumkin.
1import { Controller, Request, Post, UseGuards } from '@nestjs/common';
2import { LocalAuthGuard } from './auth/local-auth.guard';
3import { AuthService } from './auth/auth.service';
4
5@Controller()
6export class AppController {
7 constructor(private authService: AuthService) {}
8
9 @UseGuards(LocalAuthGuard)
10 @Post('auth/login')
11 async login(@Request() req) {
12 return this.authService.login(req.user);
13 }
14}Keling, route'larni yana cURL bilan test qilib ko'ramiz. UsersService ichida hard-code qilingan istalgan user obyektidan foydalanishingiz mumkin.
1$ # POST to /auth/login
2$ curl -X POST http://localhost:3000/auth/login -d '{"username": "john", "password": "changeme"}' -H "Content-Type: application/json"
3$ # result -> {"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}
4$ # Note: above JWT truncatedPassport JWT'ni implement qilish
Endi oxirgi talabni hal qilamiz: endpoint'larni request ichida valid JWT bo'lishini talab qilgan holda himoyalash. Bu yerda ham Passport yordam beradi. U JSON Web Token orqali RESTful endpoint'larni himoyalash uchun passport-jwt strategy'sini taqdim etadi. Avval auth papkasida jwt.strategy.ts nomli fayl yarating va quyidagi kodni qo'shing:
1import { ExtractJwt, Strategy } from 'passport-jwt';
2import { PassportStrategy } from '@nestjs/passport';
3import { Injectable } from '@nestjs/common';
4import { jwtConstants } from './constants';
5
6@Injectable()
7export class JwtStrategy extends PassportStrategy(Strategy) {
8 constructor() {
9 super({
10 jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
11 ignoreExpiration: false,
12 secretOrKey: jwtConstants.secret,
13 });
14 }
15
16 async validate(payload: any) {
17 return { userId: payload.sub, username: payload.username };
18 }
19}Bizning JwtStrategy bilan oldin barcha Passport strategy'lar uchun ko'rsatib o'tilgan bir xil recipe'ga amal qildik. Bu strategy'ga boshlang'ich konfiguratsiya kerak, shuning uchun super() chaqiruviga options obyekt uzatamiz. Mavjud opsiyalar haqida ko'proq ma'lumotni bu yerda o'qishingiz mumkin. Bizning holatda opsiyalar quyidagilar:
jwtFromRequest: JWTRequestdan qanday usul bilan olinishi kerakligini belgilaydi. Biz API request'laridaAuthorizationheader orqali bearer token uzatishning standart yondashuvidan foydalanamiz. Boshqa variantlar bu yerda keltirilgan.ignoreExpiration: aniq ko'rsatish uchun standartfalseqiymatini tanlaymiz. Bu JWT muddati tugagan-tugamaganini tekshirish mas'uliyatini Passport moduliga topshiradi. Ya'ni route'ga muddati o'tgan JWT yuborilsa, request rad etiladi va401 Unauthorizedjavobi qaytadi. Passport buni biz uchun avtomatik bajaradi.secretOrKey: token'ni sign qilish uchun symmetric secret'ni to'g'ridan-to'g'ri uzatayapmiz. PEM formatdagi public key kabi boshqa variantlar production ilovalar uchun ko'proq mos kelishi mumkin (bu yerda ko'proq ma'lumot bor). Har holda, yuqorida ogohlantirilganidek, bu secret'ni oshkor qilmang.
validate() metodi haqida alohida to'xtalish kerak. jwt-strategy uchun Passport avval JWT signature'sini tekshiradi va JSON'ni decode qiladi. So'ng decoded JSON'ni yagona parametr sifatida uzatib, bizning validate() metodimizni chaqiradi. JWT sign qilish qanday ishlashini hisobga olsak, bizga avval o'zimiz sign qilib, valid foydalanuvchiga bergan token kelayotganiga ishonch hosil qilamiz.
Shu sabab validate() callback'iga javobimiz juda oddiy: userId va username property'lariga ega obyektni qaytaramiz. Yana bir bor eslatamiz, Passport validate() metodidan qaytgan qiymat asosida user obyektini yaratadi va uni Request obyektiga property sifatida biriktiradi.
Bundan tashqari, siz array ham qaytarishingiz mumkin: birinchi qiymat user obyektini yaratish uchun, ikkinchi qiymat esa authInfo obyektini yaratish uchun ishlatiladi.
Bu yondashuv jarayonga boshqa business logic qo'shish uchun ham joy qoldirishini aytib o'tish kerak. Masalan, validate() metodi ichida database query qilib, foydalanuvchi haqida qo'shimcha ma'lumot olib, Request ichida boyitilganroq user obyektiga ega bo'lishimiz mumkin. Yoki shu joyda token'ni qo'shimcha validatsiya qilish, masalan userId ni revoked token'lar ro'yxatidan tekshirish orqali token revocation'ni amalga oshirish mumkin. Bu sample kodda implement qilgan model tez ishlovchi, "stateless JWT" modeli bo'lib, unda har bir API chaqiruvi valid JWT mavjudligi asosida darhol autorizatsiya qilinadi va request pipeline ichida so'rov yuboruvchiga oid kichik ma'lumot to'plami (userId va username) mavjud bo'ladi.
Yangi JwtStrategy ni AuthModule ichida provider sifatida qo'shing:
1import { Module } from '@nestjs/common';
2import { AuthService } from './auth.service';
3import { LocalStrategy } from './local.strategy';
4import { JwtStrategy } from './jwt.strategy';
5import { UsersModule } from '../users/users.module';
6import { PassportModule } from '@nestjs/passport';
7import { JwtModule } from '@nestjs/jwt';
8import { jwtConstants } from './constants';
9
10@Module({
11 imports: [
12 UsersModule,
13 PassportModule,
14 JwtModule.register({
15 secret: jwtConstants.secret,
16 signOptions: { expiresIn: '60s' },
17 }),
18 ],
19 providers: [AuthService, LocalStrategy, JwtStrategy],
20 exports: [AuthService],
21})
22export class AuthModule {}JWT sign qilinganda ishlatilgan shu bir xil secret'ni import qilish orqali Passport bajaradigan verify bosqichi va AuthService ichidagi sign bosqichi bir xil secret'dan foydalanishini ta'minlaymiz.
Oxirida built-in AuthGuard ni kengaytiradigan JwtAuthGuard class'ini yaratamiz:
1import { Injectable } from '@nestjs/common';
2import { AuthGuard } from '@nestjs/passport';
3
4@Injectable()
5export class JwtAuthGuard extends AuthGuard('jwt') {}Protected route va JWT strategy guard'larini implement qilish
Endi protected route va unga bog'liq Guard'ni implement qilishimiz mumkin.
app.controller.ts faylini ochib, quyidagicha yangilang:
1import { Controller, Get, Request, Post, UseGuards } from '@nestjs/common';
2import { JwtAuthGuard } from './auth/jwt-auth.guard';
3import { LocalAuthGuard } from './auth/local-auth.guard';
4import { AuthService } from './auth/auth.service';
5
6@Controller()
7export class AppController {
8 constructor(private authService: AuthService) {}
9
10 @UseGuards(LocalAuthGuard)
11 @Post('auth/login')
12 async login(@Request() req) {
13 return this.authService.login(req.user);
14 }
15
16 @UseGuards(JwtAuthGuard)
17 @Get('profile')
18 getProfile(@Request() req) {
19 return req.user;
20 }
21}Yana bir bor, passport-jwt modulini sozlaganimizda @nestjs/passport biz uchun avtomatik yaratgan AuthGuard ni qo'llayapmiz. Bu Guard standart nomi jwt orqali chaqiriladi. GET /profile route'iga request kelganda, Guard avtomatik ravishda biz sozlagan passport-jwt strategy'ni ishga tushiradi, JWT'ni validatsiya qiladi va user property'ni Request obyektiga biriktiradi.
Ilova ishlayotganiga ishonch hosil qiling va route'larni cURL orqali test qiling.
1$ # GET /profile
2$ curl http://localhost:3000/profile
3$ # result -> {"statusCode":401,"message":"Unauthorized"}
4
5$ # POST /auth/login
6$ curl -X POST http://localhost:3000/auth/login -d '{"username": "john", "password": "changeme"}' -H "Content-Type: application/json"
7$ # result -> {"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2Vybm... }
8
9$ # GET /profile using access_token returned from previous step as bearer code
10$ curl http://localhost:3000/profile -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2Vybm..."
11$ # result -> {"userId":1,"username":"john"}E'tibor bering, AuthModule ichida JWT muddati 60 seconds qilib sozlangan. Bu ehtimol juda qisqa muddat, token expiration va refresh tafsilotlari esa ushbu maqola doirasidan tashqarida. Lekin biz buni JWT va passport-jwt strategy'ning muhim xususiyatini ko'rsatish uchun tanladik. Agar authentication'dan keyin 60 soniya kutib, so'ng GET /profile request'ini yuborsangiz, 401 Unauthorized javobini olasiz. Sababi Passport JWT ichidagi expiration vaqtini avtomatik tekshiradi va bu ishni ilova kodida qo'lda yozishingizga hojat qoldirmaydi.
Shu bilan JWT authentication implementatsiyasini yakunladik. Endi JavaScript client'lar (masalan Angular/React/Vue) va boshqa JavaScript ilovalar bizning API server bilan authentication qilib, xavfsiz aloqa qila oladi.
Guard'larni kengaytirish
Ko'p holatlarda tayyor AuthGuard class'idan foydalanish yetarli bo'ladi. Ammo ayrim use case'larda standart error handling yoki authentication logic'ni kengaytirish kerak bo'lishi mumkin. Buning uchun built-in class'ni kengaytirib, sub-class ichida kerakli metodlarni override qilishingiz mumkin.
1import {
2 ExecutionContext,
3 Injectable,
4 UnauthorizedException,
5} from '@nestjs/common';
6import { AuthGuard } from '@nestjs/passport';
7
8@Injectable()
9export class JwtAuthGuard extends AuthGuard('jwt') {
10 canActivate(context: ExecutionContext) {
11 // Add your custom authentication logic here
12 // for example, call super.logIn(request) to establish a session.
13 return super.canActivate(context);
14 }
15
16 handleRequest(err, user, info) {
17 // You can throw an exception based on either "info" or "err" arguments
18 if (err || !user) {
19 throw err || new UnauthorizedException();
20 }
21 return user;
22 }
23}Standart error handling va authentication logic'ni kengaytirishdan tashqari, authentication'ni strategy'lar zanjiri orqali ham o'tkazish mumkin. Muvaffaqiyatli bo'lgan, redirect qilgan yoki error qaytargan birinchi strategy zanjirni to'xtatadi. Authentication xatolari esa har bir strategy bo'ylab ketma-ket yuradi va hammasi muvaffaqiyatsiz tugasa, yakuniy xato qaytadi.
1export class JwtAuthGuard extends AuthGuard(['strategy_jwt_1', 'strategy_jwt_2', '...']) { ... }Authentication'ni global yoqish
Agar endpoint'laringizning katta qismi standart holatda protected bo'lishi kerak bo'lsa, authentication guard'ni global guard sifatida ro'yxatdan o'tkazishingiz mumkin. Shunda har bir controller ustiga @UseGuards() dekoratori yozish o'rniga, qaysi route'lar public ekanini belgilab qo'yishning o'zi kifoya qiladi.
Avval JwtAuthGuard ni quyidagi ko'rinishda global guard sifatida ro'yxatdan o'tkazing (istalgan modul ichida):
1providers: [
2 {
3 provide: APP_GUARD,
4 useClass: JwtAuthGuard,
5 },
6],Shundan keyin Nest avtomatik ravishda JwtAuthGuard ni barcha endpoint'larga bog'laydi.
Endi route'larni public deb e'lon qilish mexanizmini berishimiz kerak. Buning uchun SetMetadata decorator factory funksiyasi yordamida custom decorator yaratamiz.
1import { SetMetadata } from '@nestjs/common';
2
3export const IS_PUBLIC_KEY = 'isPublic';
4export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);Yuqoridagi faylda biz ikkita constant export qildik. Birinchisi IS_PUBLIC_KEY nomli metadata key, ikkinchisi esa Public deb atayotgan yangi dekoratorning o'zi. Xohlasangiz uni SkipAuth yoki AllowAnon deb ham nomlashingiz mumkin, loyihangizga qaysi biri mos tushsa o'shani ishlating.
Endi custom @Public() dekoratorimiz bor ekan, uni istalgan metodni bezash uchun quyidagicha ishlatishimiz mumkin:
1@Public()
2@Get()
3findAll() {
4 return [];
5}Oxirida JwtAuthGuard "isPublic" metadata'sini topsa true qaytarishi kerak. Buning uchun Reflector class'idan foydalanamiz (bu yerda batafsilroq o'qishingiz mumkin).
1@Injectable()
2export class JwtAuthGuard extends AuthGuard('jwt') {
3 constructor(private reflector: Reflector) {
4 super();
5 }
6
7 canActivate(context: ExecutionContext) {
8 const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
9 context.getHandler(),
10 context.getClass(),
11 ]);
12 if (isPublic) {
13 return true;
14 }
15 return super.canActivate(context);
16 }
17}Request scope'li strategy'lar
Passport API strategy'larni kutubxonaning global instance'iga ro'yxatdan o'tkazish g'oyasiga asoslangan. Shu sabab strategy'lar request'ga bog'liq options'ga ega bo'lish yoki har bir request uchun dinamik yaratilish uchun mo'ljallanmagan (request-scoped provider'lar haqida ko'proq o'qing). Agar strategy'ni request-scoped qilib sozlasangiz, Nest uni umuman instantiate qilmaydi, chunki u aniq bir route'ga bog'lanmagan bo'ladi. Qaysi "request-scoped" strategy qaysi request uchun ishlashi kerakligini fizik jihatdan aniqlashning iloji yo'q.
Shunga qaramay, strategy ichida request-scoped provider'larni dinamik resolve qilish usullari bor. Buning uchun module reference imkoniyatidan foydalanamiz.
Avval local.strategy.ts faylini ochib, odatdagi usulda ModuleRef ni inject qiling:
1constructor(private moduleRef: ModuleRef) {
2 super({
3 passReqToCallback: true,
4 });
5}ModuleRef class'i @nestjs/core package'idan import qilinadi.
Yuqorida ko'rsatilganidek, passReqToCallback konfiguratsiya property'sini true qilishni unutmang.
Keyingi bosqichda yangi context identifier yaratish o'rniga request instance'ning o'zi orqali joriy context identifier olinadi (request context haqida ko'proq bu yerda o'qishingiz mumkin).
Endi LocalStrategy class'ining validate() metodi ichida ContextIdFactory class'idagi getByRequest() metodidan foydalanib request obyektiga asoslangan context id yarating va uni resolve() chaqiruviga uzating:
1async validate(
2 request: Request,
3 username: string,
4 password: string,
5) {
6 const contextId = ContextIdFactory.getByRequest(request);
7 // "AuthService" is a request-scoped provider
8 const authService = await this.moduleRef.resolve(AuthService, contextId);
9 ...
10}Yuqoridagi misolda resolve() metodi AuthService provider'ining request-scoped instance'ini asynchronous tarzda qaytaradi (AuthService request-scoped provider sifatida belgilangan deb faraz qilyapmiz).
Passport'ni moslashtirish
Passport'ning istalgan standart customization opsiyalari xuddi shu usulda, register() metodi orqali uzatilishi mumkin. Mavjud opsiyalar implement qilinayotgan strategy'ga bog'liq bo'ladi. Masalan:
1PassportModule.register({ session: true });Strategy'larni constructor ichida options obyekt berib ham sozlashingiz mumkin. Masalan, local strategy uchun quyidagini uzatishingiz mumkin:
1constructor(private authService: AuthService) {
2 super({
3 usernameField: 'email',
4 passwordField: 'password',
5 });
6}Property nomlari uchun rasmiy Passport Website ni ko'rib chiqing.
Nomlangan strategy'lar
Strategy implement qilayotganda PassportStrategy funksiyasiga ikkinchi argument sifatida nom berishingiz mumkin. Agar buni qilmasangiz, har bir strategy standart nomga ega bo'ladi (masalan, jwt-strategy uchun 'jwt'):
1export class JwtStrategy extends PassportStrategy(Strategy, 'myjwt')Keyin uni @UseGuards(AuthGuard('myjwt')) kabi dekorator orqali ishlatasiz.
GraphQL
GraphQL bilan AuthGuard ishlatish uchun built-in AuthGuard class'ini kengaytirib, getRequest() metodini override qiling.
1@Injectable()
2export class GqlAuthGuard extends AuthGuard('jwt') {
3 getRequest(context: ExecutionContext) {
4 const ctx = GqlExecutionContext.create(context);
5 return ctx.getContext().req;
6 }
7}GraphQL resolver ichida joriy authenticated user'ni olish uchun @CurrentUser() dekoratorini yaratishingiz mumkin:
1import { createParamDecorator, ExecutionContext } from '@nestjs/common';
2import { GqlExecutionContext } from '@nestjs/graphql';
3
4export const CurrentUser = createParamDecorator(
5 (data: unknown, context: ExecutionContext) => {
6 const ctx = GqlExecutionContext.create(context);
7 return ctx.getContext().req.user;
8 },
9);Yuqoridagi dekoratordan resolver ichida foydalanish uchun uni query yoki mutation parametriga qo'shishni unutmang:
1@Query(() => User)
2@UseGuards(GqlAuthGuard)
3whoAmI(@CurrentUser() user: User) {
4 return this.usersService.findById(user.id);
5}passport-local strategy uchun GraphQL context argument'larini request body'ga ham qo'shishingiz kerak, shunda Passport ularni validatsiya uchun o'qiy oladi. Aks holda Unauthorized xatosini olasiz.
1@Injectable()
2export class GqlLocalAuthGuard extends AuthGuard('local') {
3 getRequest(context: ExecutionContext) {
4 const gqlExecutionContext = GqlExecutionContext.create(context);
5 const gqlContext = gqlExecutionContext.getContext();
6 const gqlArgs = gqlExecutionContext.getArgs();
7
8 gqlContext.req.body = { ...gqlContext.req.body, ...gqlArgs };
9 return gqlContext.req;
10 }
11}