JWT Revokovanie Stratégie: Keď Stateless Tokeny Potrebujú Stav
Myslel som si, ze revokacia JWT je jednoducha, kym neprisiel prvy incident. “Účet používateľa kompromitovaný. Revokuj všetky ich tokeny TERAZ.” Ale JWT sú stateless a immutable. Raz vydané, sú platné do expirácie. Ako revokovať to čo sa revokovať nedá?
Testované na: Go 1.21, Redis 7.2, 10,000 requestov/sekundu autentifikačná záťaž
Problém JWT Revokovania
Prečo JWT Nemôžu Byť Revokované
JWT Dizajn:
Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
┌─────────────────────────────────────────────────────────────┐
│ Header: {"alg": "HS256", "typ": "JWT"} │
│ Payload: { │
│ "sub": "user123", │
│ "exp": 1704844800, ← Expiruje za 24 hodín │
│ "iat": 1704758400, │
│ "roles": ["admin"] │
│ } │
│ Signature: HMACSHA256(header + payload, secret) │
└─────────────────────────────────────────────────────────────┘
Verifikácia: Skontroluj podpis + skontroluj exp > now
Žiadny databázový lookup potrebný = stateless = rýchle
Ale: Žiadny spôsob ako invalidovať pred exp
Token uniknutý? Platný ďalších 24 hodín.
Stratégia 1: Krátka Expirácia + Refresh Tokeny
Dizajn
Access Token: 15 minút expirácia
Refresh Token: 7 dní expirácia (uložený server-side)
┌─────────────────────────────────────────────────────────────┐
│ Flow: │
│ │
│ 1. Login → Access Token (15m) + Refresh Token (7d) │
│ 2. API volania používajú Access Token │
│ 3. Access Token expiruje → Použi Refresh Token na nový │
│ 4. Revokácia: Zmaž Refresh Token z databázy │
│ │
│ Max expozícia: 15 minút (kým Access Token expiruje) │
└─────────────────────────────────────────────────────────────┘
Implementácia
// tokens.go
type TokenService struct {
accessTTL time.Duration // 15 minút
refreshTTL time.Duration // 7 dní
db *sql.DB
jwtSecret []byte
}
func (s *TokenService) RevokeAllUserTokens(userID string) error {
// Zmaž všetky refresh tokeny = používateľ sa musí prihlásiť
_, err := s.db.Exec(
`DELETE FROM refresh_tokens WHERE user_id = $1`,
userID,
)
return err
}
Stratégia 2: Token Denylist (Blocklist)
Dizajn
Pri revokácii: Pridaj token do Redis blocklistu
Pri verifikácii: Skontroluj či token je v blockliste
┌─────────────────────────────────────────────────────────────┐
│ Redis Denylist: │
│ │
│ SET revoked:<jti> 1 EX 86400 (TTL = zostávajúce TTL tokenu)│
│ │
│ Verifikácia: │
│ 1. Over JWT podpis │
│ 2. Skontroluj exp > now │
│ 3. GET revoked:<jti> → ak existuje, odmietni │
└─────────────────────────────────────────────────────────────┘
Implementácia
// denylist.go
func (s *DenylistService) VerifyToken(tokenStr string) (*jwt.MapClaims, error) {
token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) {
return s.jwtSecret, nil
})
if err != nil {
return nil, err
}
claims := token.Claims.(jwt.MapClaims)
jti := claims["jti"].(string)
// Skontroluj denylist
exists, err := s.redis.Exists(context.Background(), "revoked:"+jti).Result()
if err != nil {
return nil, err
}
if exists == 1 {
return nil, fmt.Errorf("token revokovaný")
}
return &claims, nil
}
func (s *DenylistService) RevokeToken(tokenStr string) error {
token, _ := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) {
return s.jwtSecret, nil
})
claims := token.Claims.(jwt.MapClaims)
jti := claims["jti"].(string)
exp := int64(claims["exp"].(float64))
// TTL = zostávajúca životnosť tokenu
ttl := time.Until(time.Unix(exp, 0))
if ttl <= 0 {
return nil // Už expirovaný
}
return s.redis.Set(context.Background(), "revoked:"+jti, "1", ttl).Err()
}
Stratégia 3: Token Verzia / Generácia
Dizajn
Ulož "token version" per používateľ
Zahrň verziu do JWT
Pri revokácii: Inkrementuj verziu
┌─────────────────────────────────────────────────────────────┐
│ Databáza: users.token_version = 5 │
│ │
│ JWT: {"sub": "user123", "ver": 5, "exp": ...} │
│ │
│ Verifikácia: │
│ 1. Over JWT podpis │
│ 2. Načítaj user.token_version z DB/cache │
│ 3. Porovnaj JWT.ver == user.token_version │
│ 4. Ak sa nerovnajú → token je revokovaný │
│ │
│ Revokácia: │
│ UPDATE users SET token_version = token_version + 1 │
│ → Všetky existujúce tokeny okamžite neplatné │
└─────────────────────────────────────────────────────────────┘
Implementácia
// version.go
func (s *VersionService) VerifyToken(tokenStr string) (*jwt.MapClaims, error) {
token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) {
return s.jwtSecret, nil
})
if err != nil {
return nil, err
}
claims := token.Claims.(jwt.MapClaims)
userID := claims["sub"].(string)
tokenVersion := int64(claims["ver"].(float64))
// Skontroluj aktuálnu verziu
currentVersion, err := s.getUserVersion(userID)
if err != nil {
return nil, err
}
if tokenVersion != currentVersion {
return nil, fmt.Errorf("token revokovaný (nesúlad verzií)")
}
return &claims, nil
}
func (s *VersionService) RevokeAllUserTokens(userID string) error {
// Inkrementuj verziu → všetky tokeny neplatné
_, err := s.db.Exec(
`UPDATE users SET token_version = token_version + 1 WHERE id = $1`,
userID,
)
if err != nil {
return err
}
// Invaliduj cache
return s.redis.Del(context.Background(), "user_version:"+userID).Err()
}
Porovnanie Performance
Benchmark: 10,000 requestov/sekundu, 100k používateľov
Stratégia │ Verify Latencia│ Úložisko │ Revoke All
──────────────────────┼────────────────┼──────────────┼─────────────
Krátka expirácia │ 0.05ms (no DB) │ Refresh: 1MB │ 1 DELETE
Denylist │ 0.2ms (Redis) │ ~10KB/deň │ N SETs
Token verzia │ 0.3ms (cached) │ 8B/user │ 1 UPDATE
Odporúčanie:
- Nízka bezpečnosť: Krátka expirácia (15 min)
- Vysoká bezpečnosť: Token verzia (okamžitá revokácia)
- Potrebná individuálna revokácia: Denylist
Checklist
## JWT Revokácia Implementácia
### Výber Stratégie
- [ ] Zhodnoť bezpečnostné požiadavky (okno expozície)
- [ ] Zváž individuálnu vs all-token revokáciu
- [ ] Zhodnoť infraštruktúru (Redis dostupný?)
### Implementácia
- [ ] Pridaj jti (JWT ID) pre denylist stratégiu
- [ ] Pridaj ver (verzia) pre version stratégiu
- [ ] Nastav refresh token úložisko pre krátku expiráciu
- [ ] Implementuj cache vrstvu pre version lookupy
### Monitoring
- [ ] Sleduj revokačné eventy
- [ ] Monitoruj veľkosť denylistu (ak používaš)
- [ ] Alert na vysokú verifikačnú latenciu
Záver
JWT revokácia vyžaduje pridanie stavu niekde:
- Krátka expirácia limituje expozíciu ale neodstraňuje ju
- Denylist poskytuje okamžitú revokáciu per token
- Token verzia revokuje všetky tokeny používateľa okamžite
Vyber podľa tvojich bezpečnostných požiadaviek a infraštruktúry.
Súvisiace články
- Adaptive Concurrency Limits - Performance patterns
- Redis Memory Fragmentácia - Redis optimalizácia
Súvisiace články
Redis AOF fsync latency špičky: keď sa durabilita stane tvojím p99
Redis AOF vie spraviť z durability p99 špičky: fsync tlak a BGREWRITEAOF fork CoW. Runbook na dôkaz, bezpečné mitigácie a guardrails.
Cache Stampede Prevencia: Probabilistická Skorá Expirácia (X-Fetch)
100 requestov zasiahne expirovanú cache súčasne. Všetkých 100 sa pýta databázy. Implementujem X-Fetch algoritmus ktorý refreshuje cache pred expiráciou bez zamykania.
Redis Memory Fragmentácia: Keď maxmemory Nestačí
Váš Redis má 4GB maxmemory ale RSS ukazuje 6GB. OOM killer zasiahne. Vysvetlím jemalloc fragmentáciu s reprodukciou a tuningom activedefrag.
Java Profilovanie v Hardened Kubernetes: Keď Security Blokuje Tvoj Debugger
Nemôžeš pripojiť profiler k produkčnej JVM. seccomp blokuje perf_event_open, container dropol CAP_SYS_PTRACE a PodSecurityPolicy bráni privileged mode. Tu je ako profilovať aj tak.
Citujte tento článok
Ak na článok odkazujete, pridajte pôvodnú URL a uveďte autora.