Problem: "JWT işlədirik, çünki stateless-dir"
Komanda single-page front end və Spring Boot API ilə web tətbiqi qurur. Kimsə session-ların miqyaslanmadığını və JWT-nin müasir seçim olduğunu deyir; front end 30 günlük müddətlə JWT-ni localStorage-də saxlayır. Aylar sonra istifadəçi noutbukunun oğurlandığını bildirir. Komanda görür ki, token bitənə qədər həmin istifadəçinin girişini dayandırmağın yolu yoxdur və səhifəyə yeridilmiş istənilən script token-i oxuya bilər.
Heç bir yanaşma ümumilikdə üstün deyil. Onlar vəziyyəti və riski fərqli yerlərə köçürür. Bu yazının məqsədi həmin köçürmələri görünən etməkdir ki, seçimi şüurlu edə biləsiniz.
İki model
Server-side session. Login-dən sonra server session qeydi yaradır (yaddaşda və ya Redis, baza kimi ortaq saxlancda) və brauzerə cookie-də qeyri-şəffaf identifikator göndərir. Hər request cookie daşıyır; server session-u tapır və istifadəçini müəyyən edir. İdentifikator serverin qeydi olmadan heç nə ifadə etmir.
Stateless token. Login-dən sonra server imzalanmış token, çox vaxt JWT verir; onun içində subject, rollar və bitmə vaxtı kimi claim-lər olur. Client onu Authorization: Bearer header-ində göndərir. Server imzanı və bitmə vaxtını yoxlayır və claim-lərə axtarışsız etibar edir.
Session: brauzer --cookie(id)--> server --axtarış--> session saxlancı
Token: client --Bearer(JWT)--> server --imza yoxlaması--> claim-lərJWT imzalanmış token-dir, şifrələnmiş deyil. Onu tutan hər kəs payload-u oxuya bilər, ona görə içində sirr və ya lazımsız şəxsi məlumat olmamalıdır.
Revocation
Həlledici fərq budur.
- Session ilə logout, parol dəyişikliyi və ya "hər yerdən çıxış" qeydləri silmək deməkdir. Effekt dərhal olur.
- Özündə məlumat daşıyan JWT ilə serverdə silinəcək vəziyyət yoxdur. Token
expclaim-i keçənə qədər etibarlı qalır. Daha tez ləğv etmək üçün vəziyyəti yenidən qaytarmalısınız: token identifikatorlarının (jti) rədd siyahısı, istifadəçi üzrə "bu vaxtdan sonrakı token-lər etibarlıdır" damğası və ya qısa müddət ilə ləğv oluna bilən refresh token-lərin birləşməsi.
Bunların hər biri axtarışı geri gətirir, yəni "server vəziyyəti yoxdur" iddiası ləğvə ehtiyacınız olmayanadək doğrudur. Adi kompromis qısa ömürlü access token (dəqiqələr) və serverdə saxlanan, ləğv oluna bilən daha uzunömürlü refresh token-dir.
Refresh token-lər və rotation
Refresh token yeni access token-lərlə dəyişdirilir. Daha uzun yaşadığı üçün daha dəyərli hədəfdir. Yaxşı təcrübə:
- Refresh token-ləri serverdə hash-ləyib saxlayın (parol kimi, amma yüksək entropiyalı təsadüfi dəyərlər olduğu üçün sürətli hash məqbuldur).
- Onları rotate edin: hər istifadə yeni refresh token qaytarır və köhnəni etibarsız edir.
- Təkrar istifadəni aşkar edin: artıq istifadə olunmuş refresh token yenidən gəlirsə, oğurluq güman edin və həmin login üçün bütün token ailəsini ləğv edin.
- Onları cihaz və ya session qeydinə bağlayın ki, istifadəçi session-ları görüb bitirə bilsin.
// Eskiz: refresh endpoint məntiqiRefreshRecord stored = refreshRepo.findByHash(hash(presented)) .orElseThrow(InvalidTokenException::new);if (stored.isUsed()) { // təkrar istifadə aşkarlandı refreshRepo.revokeFamily(stored.familyId()); throw new InvalidTokenException();}stored.markUsed();return tokenService.issue(stored.userId(), stored.familyId()); // yeni cütClient onu harada saxlayır?
Brauzerlər üçün iki əsas seçim var və ikisinin də qiyməti var.
HttpOnly,Securevə uyğunSameSitedəyəri olan cookie: JavaScript onu oxuya bilmir, yeridilmiş script onu birbaşa oğurlaya bilməz. Amma brauzer cookie-ləri avtomatik göndərir və bu, CSRF məruzluğu yaradır.SameSite, CSRF token-ləri (Spring Security session əsaslı qurğularda CSRF qorumasını default aktiv edir) vəOriginyoxlaması ilə müdafiə edin.- Bearer header ilə
localStoragevə ya JavaScript yaddaşı: avtomatik göndərilmir, ona görə klassik CSRF tətbiq olunmur. Amma istənilən XSS onu saxlancdan oxuya bilər. Yalnız yaddaşda saxlamaq davamlılığı azaldır, amma yenilənmədə itir, ona görə tətbiqlər çox vaxt onu refresh cookie ilə cütləşdirir.
Heç bir seçim XSS-in qarşısını almaq ehtiyacını aradan qaldırmır: yeridilmiş script ilə hücumçu token oxunmasa belə səhifənin içindən autentifikasiya olunmuş request-lər göndərə bilər.
Praqmatik default
First-party web tətbiqi üçün, yəni sizin front end-iniz və API-niz bir saytı bölüşəndə, təhlükəsiz cookie ilə server-side session işlədin, ya da token-ləri serverin saxladığı, brauzerin isə yalnız session cookie saxladığı "backend for frontend" patternindən istifadə edin. Dərhal revocation, brauzerdə kiçik hücum səthi və yetkin framework dəstəyi alırsınız (bir neçə instance-a miqyaslanırsınızsa Redis ilə spring-session). CSRF qorumasını aktiv saxlayın.
Mobil tətbiq və ya üçüncü tərəf API client üçün cookie-lər pis uyğun gəlir. Qısa ömürlü access token-lər və rotasiya olunan refresh token-lər işlədin, onları platformanın təhlükəsiz saxlancında (Keychain və ya Keystore) saxlayın və server-side refresh qeydləri vasitəsilə ləğvi mümkün edin. Xarici identity provider ilə inteqrasiya edirsinizsə, öz token formatınızı icad etmək əvəzinə Spring Security-nin resource server dəstəyi ilə OAuth 2.0 / OpenID Connect axınlarından istifadə edin.
Sistem daxilində servis-servis çağırışları üçün dar scope-lu və qısa müddətli imzalanmış token-lər təbiidir, çünki brauzer iştirak etmir.
Resource server-in konfiqurasiyası
@BeanSecurityFilterChain api(HttpSecurity http) throws Exception { return http .csrf(csrf -> csrf.disable()) // yalnız header əsaslı bearer auth üçün məqbuldur .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(a -> a .requestMatchers("/auth/**").permitAll() .anyRequest().authenticated()) .oauth2ResourceServer(o -> o.jwt(Customizer.withDefaults())) .build();}CSRF-in söndürülməsi burada yalnız ona görə əsaslandırılır ki, credential-lar brauzer tərəfindən avtomatik göndərilmir. Eyni API-də cookie də qəbul edirsinizsə, bu əsaslandırma artıq keçərli deyil.
JWT yoxlayarkən həmişə imzanı özünüzün idarə etdiyiniz açarla, bitmə vaxtını, issuer-i və audience-i yoxlayın. Qəbul olunan alqoritmi sabitləyin və heç vaxt none qəbul etməyin. İmza açarlarını rotate edin və yeni açarları tuta bilsinlər deyə key set vasitəsilə dərc edin.
Tez-tez edilən səhvlər
- Revocation yolu olmadan uzunömürlü (gün və ya həftə) access token-lər saxlamaq.
- "Token-in içindədir" deyə payload-a həssas məlumat qoymaq.
- Bütün mühitlərdə eyni imza sirrini işlətmək və ya onu repozitoriyaya commit etmək.
issvəaud-u yoxlamamaq, nəticədə başqa servis üçün nəzərdə tutulmuş token-in qəbul edilməsi.- JWT rol claim-inin həmişə təzə olduğunu güman etmək: rol dəyişəndən sonra köhnə token bitənədək köhnə rolları daşıyır.
- Cookie əsaslı login işlədərkən CSRF-i qlobal söndürmək.
- Yaxşı yoxlanmış kitabxanalar əvəzinə öz token formatını və ya kriptoqrafiyanı yazmaq.
Trade-off-lar
| Cəhət | Server session | Stateless JWT |
|---|---|---|
| Revocation | Dərhal | Əlavə vəziyyət və ya qısa müddət lazımdır |
| Miqyaslanma | Çoxlu instance-da ortaq saxlanc lazımdır | Hər request üçün axtarış yoxdur |
| Brauzerdə saxlama | HttpOnly cookie | Cookie və ya script-ə açıq saxlanc |
| CSRF | İdarə olunmalıdır | Yalnız header bearer üçün yoxdur |
| Servislərarası istifadə | Əlverişsiz | Təbii |
| Əməliyyat sadəliyi | İşlədib izləmək üçün saxlanc | Açar idarəsi və rotation |
Yoxlama siyahısı
- Əvvəlcə client tiplərini müəyyən edin: brauzer, mobil, digər servislər.
- Access token-lər qısa ömürlüdür; refresh token-lər rotate olunur və ləğv oluna bilir.
- Cookie-lər
HttpOnly,Securevə şüurluSameSitedəyəri ilə qurulub. - CSRF qoruması credential daşınma üsuluna uyğundur.
- Issuer, audience, bitmə vaxtı və imza yoxlanılır.
- Logout və parol dəyişikliyi server-side vəziyyəti etibarsız edir.
Sonra nə oxumalı
- API risklərinə daha geniş baxış: API təhlükəsizliyi və OWASP siyahısı.
- Auth xətalarını client-lər üçün ardıcıl edin: validasiya və xəta cavabları.
- Sirləri log-lamadan autentifikasiya hadisələrini izləyin: observability sütunları.
- Qarşıdakı təhlükəsizlik mövzuları üçün senior backend developer roadmap-inə baxın.