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ər

JWT 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 exp claim-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ə:

  1. 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).
  2. Onları rotate edin: hər istifadə yeni refresh token qaytarır və köhnəni etibarsız edir.
  3. 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.
  4. 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üt

Client onu harada saxlayır?

Brauzerlər üçün iki əsas seçim var və ikisinin də qiyməti var.

  • HttpOnly, Secure və uyğun SameSite də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ə Origin yoxlaması ilə müdafiə edin.
  • Bearer header ilə localStorage və 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

  1. Revocation yolu olmadan uzunömürlü (gün və ya həftə) access token-lər saxlamaq.
  2. "Token-in içindədir" deyə payload-a həssas məlumat qoymaq.
  3. Bütün mühitlərdə eyni imza sirrini işlətmək və ya onu repozitoriyaya commit etmək.
  4. iss və aud-u yoxlamamaq, nəticədə başqa servis üçün nəzərdə tutulmuş token-in qəbul edilməsi.
  5. 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.
  6. Cookie əsaslı login işlədərkən CSRF-i qlobal söndürmək.
  7. Yaxşı yoxlanmış kitabxanalar əvəzinə öz token formatını və ya kriptoqrafiyanı yazmaq.

Trade-off-lar

CəhətServer sessionStateless JWT
RevocationDərhalƏlavə vəziyyət və ya qısa müddət lazımdır
MiqyaslanmaÇoxlu instance-da ortaq saxlanc lazımdırHər request üçün axtarış yoxdur
Brauzerdə saxlamaHttpOnly cookieCookie və ya script-ə açıq saxlanc
CSRFİdarə olunmalıdırYalnız header bearer üçün yoxdur
Servislərarası istifadəƏlverişsizTəbii
Əməliyyat sadəliyiİşlədib izləmək üçün saxlancAç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, Secure və şüurlu SameSite də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ı