Merhaba arkadaşlar, bugün başımı çok ağrıtan bir sorunu ve onun basit ama etkili çözümünü anlatacağım. Bildiğiniz gibi JWT (JSON Web Token) stateless yapısıyla harika, ama bir kere verildi mi, süresi dolana kadar geçerliliğini koruyor. Peki bir kullanıcı çıkış yaptığında veya şifresini değiştirdiğinde o token'ı nasıl geçersiz kılacağız? İşte benim kullandığım en temiz çözüm: bir token blacklist (kara liste) mekanizması.
Karşılaştığım Sorun
Müşterim, kullanıcıların birden fazla cihazda oturum açabildiği bir uygulama istiyordu. Ancak güvenlik gereği, kullanıcı "tüm cihazlardan çıkış yap" butonuna bastığında veya şifresini sıfırladığında, o kullanıcıya ait tüm mevcut JWT token'larının anında geçersiz olmasını talep etti. Klasik JWT'de token'ın kendisi bu bilgiyi taşımaz. Süresi 7 günse, 7 gün boyunca geçerli kalır. Bu hatayı ilk gördüğümde kafayı yemiştim diyebilirim. Hemen bir çözüm arayışına girdim.
Çözüm: Basit Bir Redis Tabanlı Blacklist
Stateless yapıyı bozmadan, state'i minimumda tutacak bir yöntem düşündüm. Çözüm, geçersiz kılınan token'ların jti (JWT ID) değerlerini, kısa süreliğine bir veritabanında saklamak oldu. Performans için Redis mükemmel bir seçim. Token oluşturulurken benzersiz bir `jti` ekliyorum. Kullanıcı çıkış yaptığında, o kullanıcıya ait tüm jti'leri (veya sadece o token'ın jti'sini) Redis'e, token'ın orijinal süresi kadar TTL (yaşam süresi) ile kaydediyorum. Her istekte, gelen token'ın jti'sinin bu listede olup olmadığını kontrol ediyorum.
Node.js (Express) ile Kod Örnekleri
İşte token oluştururken jti eklediğim kısım:
Ve işte çıkış yapma (logout) endpoint'im. Burada, token'ın kalan süresi boyunca jti'yi Redis'te saklıyorum:
Son olarak, tüm istekleri kontrol eden bir middleware yazdım:
Sonuç ve Performans
Bu yöntemle, JWT'nin stateless avantajından büyük ölçüde vazgeçmeden, kritik bir güvenlik açığını kapatmış oldum. Redis sayesinde okuma işlemi inanılmaz hızlı (O(1)). Sistemde sadece geçersiz kılınmış ve süresi dolmamış token'lar tutulduğu için bellek kullanımı da minimal seviyede kalıyor.
Siz backend geliştiricileri, JWT token yönetiminde benzer bir sorunla karşılaştınız mı? Token'ları geçersiz kılmak için farklı ne gibi yöntemler kullandınız? Belki session tabanlı bir yaklaşım mı tercih ediyorsunuz? Yorumlarda deneyimlerinizi paylaşın, tartışalım!
Müşterim, kullanıcıların birden fazla cihazda oturum açabildiği bir uygulama istiyordu. Ancak güvenlik gereği, kullanıcı "tüm cihazlardan çıkış yap" butonuna bastığında veya şifresini sıfırladığında, o kullanıcıya ait tüm mevcut JWT token'larının anında geçersiz olmasını talep etti. Klasik JWT'de token'ın kendisi bu bilgiyi taşımaz. Süresi 7 günse, 7 gün boyunca geçerli kalır. Bu hatayı ilk gördüğümde kafayı yemiştim diyebilirim. Hemen bir çözüm arayışına girdim.
Stateless yapıyı bozmadan, state'i minimumda tutacak bir yöntem düşündüm. Çözüm, geçersiz kılınan token'ların jti (JWT ID) değerlerini, kısa süreliğine bir veritabanında saklamak oldu. Performans için Redis mükemmel bir seçim. Token oluşturulurken benzersiz bir `jti` ekliyorum. Kullanıcı çıkış yaptığında, o kullanıcıya ait tüm jti'leri (veya sadece o token'ın jti'sini) Redis'e, token'ın orijinal süresi kadar TTL (yaşam süresi) ile kaydediyorum. Her istekte, gelen token'ın jti'sinin bu listede olup olmadığını kontrol ediyorum.
İşte token oluştururken jti eklediğim kısım:
JavaScript:
const jwt = require('jsonwebtoken');
const { v4: uuidv4 } = require('uuid');
function generateToken(userId) {
const jti = uuidv4(); // Benzersiz bir ID
const token = jwt.sign(
{
sub: userId,
jti: jti, // Token'ın kimliği
iat: Math.floor(Date.now() / 1000)
},
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);
return { token, jti };
}
Ve işte çıkış yapma (logout) endpoint'im. Burada, token'ın kalan süresi boyunca jti'yi Redis'te saklıyorum:
JavaScript:
const redisClient = require('../config/redis');
async function logout(req, res) {
try {
const token = req.headers.authorization.split(' ')[1];
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// JTI'yi Redis'e ekle. Key'in TTL'si token'ın kalan süresi kadar olsun.
const tokenExpiry = decoded.exp;
const nowInSeconds = Math.floor(Date.now() / 1000);
const ttl = tokenExpiry - nowInSeconds;
if (ttl > 0) {
await redisClient.setEx(`blacklist:${decoded.jti}`, ttl, 'revoked');
}
res.json({ message: 'Çıkış başarılı. Token geçersiz kılındı.' });
} catch (error) {
res.status(400).json({ error: 'Geçersiz token' });
}
}
Son olarak, tüm istekleri kontrol eden bir middleware yazdım:
JavaScript:
async function checkBlacklist(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader) return next();
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// Redis'te bu jti blacklist'te var mı?
const inBlacklist = await redisClient.get(`blacklist:${decoded.jti}`);
if (inBlacklist) {
return res.status(401).json({ error: 'Token geçersiz kılındı.' });
}
req.user = decoded;
next();
} catch (err) {
// JWT verify hatası (süre dolması, imza hatası vb.)
next();
}
}
Bu yöntemle, JWT'nin stateless avantajından büyük ölçüde vazgeçmeden, kritik bir güvenlik açığını kapatmış oldum. Redis sayesinde okuma işlemi inanılmaz hızlı (O(1)). Sistemde sadece geçersiz kılınmış ve süresi dolmamış token'lar tutulduğu için bellek kullanımı da minimal seviyede kalıyor.
Siz backend geliştiricileri, JWT token yönetiminde benzer bir sorunla karşılaştınız mı? Token'ları geçersiz kılmak için farklı ne gibi yöntemler kullandınız? Belki session tabanlı bir yaklaşım mı tercih ediyorsunuz? Yorumlarda deneyimlerinizi paylaşın, tartışalım!