Merhaba arkadaşlar, bugün başımı çok ağrıtan bir sorunu ve bulduğum temiz çözümü anlatacağım. JWT tabanlı kimlik doğrulama sistemlerinde, refresh token'ların yönetimi her zaman bir baş ağrısıdır. Özellikle bir kullanıcı çıkış yaptığında veya bir token'ı iptal etmek istediğimizde, genellikle bir "blacklist" (kara liste) tablosu oluşturup veritabanında sorgu yapmamız gerekir. Bu, performans ve ölçeklenebilirlik açısından pek de hoş olmayan bir yöntem. Ben de "Acaba veritabanına dokunmadan, stateless bir şekilde bu işi çözebilir miyiz?" diye düşündüm ve işte karşıma çıkan yöntem.
Karşılaştığım Sorun
Projemde Node.js (Express) ve JWT kullanıyordum. Klasik akış şuydu: Access token süresi kısaydı (15 dk), refresh token süresi uzundu (7 gün). Kullanıcı çıkış yaptığında, o refresh token'ı veritabanındaki bir `blacklisted_tokens` tablosuna kaydediyorduk. Her token refresh isteğinde, gelen refresh token'ın bu listede olup olmadığını kontrol ediyorduk. Kullanıcı sayısı arttıkça bu tablo şişmeye ve her refresh işleminde ekstra bir database sorgusu yapmaya başladık. Bu durumdan mustarip olan var mı?
Aklıma Gelen Çözüm: Token Versiyonlama (Token Versioning)
Araştırmalarım sonucu "refresh token rotation" ve "token versioning" kavramlarına denk geldim. Temel fikir şu: Her kullanıcının veritabanında bir `tokenVersion` (ya da `jwtSecretVersion`) alanı tutmak. Refresh token'ı imzalarken (signing) içine bu version bilgisini de gömüyoruz. Kullanıcı çıkış yaptığında veya tüm cihazlardan çıkış yapmasını istediğimizde, basitçe veritabanındaki bu `tokenVersion` değerini bir artırıyoruz.
Böylece, eski version numarası ile imzalanmış tüm refresh token'lar bir sonraki kontrol anında geçersiz hale geliyor. Sihir gibi! Veritabanında sadece bir alanı güncelliyoruz, kocaman bir blacklist tablosuyla uğraşmıyoruz.
Nasıl Uyguladım? (Kod Zamanı!)
İlk olarak, kullanıcı şemasına basit bir version alanı ekledim.
Sonra, refresh token oluştururken bu version bilgisini token'ın payload'ına ekliyorum.
En can alıcı kısım, refresh token'ı doğrularken (verify) yapılan kontrol. Gelen token'ın içindeki version, veritabanındaki kullanıcının güncel `tokenVersion` değeri ile eşleşmeli.
Ve işte zafer anı! Kullanıcı çıkış yaptığında yapmam gereken tek şey:
Sonuç ve Düşüncelerim
Bu yöntemle birlikte:
- Blacklist tablosu ve ona ait ekstra sorgulardan kurtuldum.
- Çıkış işlemi inanılmaz hızlandı (sadece bir alanı güncelle).
- Sistem tamamen stateless kalmaya yaklaştı, sadece kritik bir noktada basit bir db güncellemesi yapılıyor.
- Güvenlik açısından da kullanıcı "tüm cihazlardan çıkış" yapmak istediğinde bu yöntem harika çalışıyor.
Tabii ki her çözümün bir trade-off'u var. Bu yöntemde, bir refresh token iptal edildiğinde, o kullanıcıya ait tüm refresh token'lar (diğer cihazlardaki veya tarayıcılardaki) de geçersiz hale geliyor. Yani kullanıcı sadece bir cihazdan değil, her yerden çıkış yapmış oluyor. Projenizin gereksinimlerine göre bu bir sorun teşkil edebilir veya etmeyebilir.
Siz daha önce JWT token yönetiminde benzer sorunlar yaşadınız mı? Blacklist dışında farklı, temiz çözümler deneyen oldu mu? Yorumlarda fikir alışverişi yapalım!
Projemde Node.js (Express) ve JWT kullanıyordum. Klasik akış şuydu: Access token süresi kısaydı (15 dk), refresh token süresi uzundu (7 gün). Kullanıcı çıkış yaptığında, o refresh token'ı veritabanındaki bir `blacklisted_tokens` tablosuna kaydediyorduk. Her token refresh isteğinde, gelen refresh token'ın bu listede olup olmadığını kontrol ediyorduk. Kullanıcı sayısı arttıkça bu tablo şişmeye ve her refresh işleminde ekstra bir database sorgusu yapmaya başladık. Bu durumdan mustarip olan var mı?
Araştırmalarım sonucu "refresh token rotation" ve "token versioning" kavramlarına denk geldim. Temel fikir şu: Her kullanıcının veritabanında bir `tokenVersion` (ya da `jwtSecretVersion`) alanı tutmak. Refresh token'ı imzalarken (signing) içine bu version bilgisini de gömüyoruz. Kullanıcı çıkış yaptığında veya tüm cihazlardan çıkış yapmasını istediğimizde, basitçe veritabanındaki bu `tokenVersion` değerini bir artırıyoruz.
Böylece, eski version numarası ile imzalanmış tüm refresh token'lar bir sonraki kontrol anında geçersiz hale geliyor. Sihir gibi! Veritabanında sadece bir alanı güncelliyoruz, kocaman bir blacklist tablosuyla uğraşmıyoruz.
İlk olarak, kullanıcı şemasına basit bir version alanı ekledim.
JavaScript:
// Kullanıcı Modeli (Mongoose Örneği)
const userSchema = new mongoose.Schema({
email: String,
passwordHash: String,
tokenVersion: { type: Number, default: 0 } // İşte kritik alan!
});
Sonra, refresh token oluştururken bu version bilgisini token'ın payload'ına ekliyorum.
JavaScript:
// Refresh Token Oluşturma
const generateRefreshToken = (user) => {
const payload = {
userId: user._id,
version: user.tokenVersion // Version bilgisi payload'da
};
return jwt.sign(payload, process.env.REFRESH_TOKEN_SECRET, {
expiresIn: '7d'
});
};
En can alıcı kısım, refresh token'ı doğrularken (verify) yapılan kontrol. Gelen token'ın içindeki version, veritabanındaki kullanıcının güncel `tokenVersion` değeri ile eşleşmeli.
JavaScript:
// Refresh Token Doğrulama ve Yeni Access Token Üretme
const refreshTokenHandler = async (req, res) => {
const { refreshToken } = req.body;
if (!refreshToken) return res.sendStatus(401);
try {
// 1. Token'ı verify et
const decoded = jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET);
// 2. Kullanıcıyı bul
const user = await User.findById(decoded.userId);
if (!user) return res.sendStatus(403); // Forbidden
// 3. TOKEN VERSION KONTROLÜ (Blacklist'e gerek kalmadan iptal mekanizması)
if (decoded.version !== user.tokenVersion) {
return res.sendStatus(403); // Token geçersiz! Version uyuşmuyor.
}
// 4. Versionlar uyuşuyorsa, yeni access token üret
const newAccessToken = generateAccessToken(user);
res.json({ accessToken: newAccessToken });
} catch (error) {
// Token süresi dolmuş veya geçersiz
return res.sendStatus(403);
}
};
Ve işte zafer anı! Kullanıcı çıkış yaptığında yapmam gereken tek şey:
JavaScript:
// Kullanıcı Çıkış (Logout) veya Tüm Cihazlardan Çıkış
const logout = async (req, res) => {
try {
// Sadece tokenVersion'ı bir artır. Eski token'lar kendiliğinden geçersiz olacak.
await User.findByIdAndUpdate(req.userId, { $inc: { tokenVersion: 1 } });
res.sendStatus(200);
} catch (error) {
res.sendStatus(500);
}
};
Bu yöntemle birlikte:
- Blacklist tablosu ve ona ait ekstra sorgulardan kurtuldum.
- Çıkış işlemi inanılmaz hızlandı (sadece bir alanı güncelle).
- Sistem tamamen stateless kalmaya yaklaştı, sadece kritik bir noktada basit bir db güncellemesi yapılıyor.
- Güvenlik açısından da kullanıcı "tüm cihazlardan çıkış" yapmak istediğinde bu yöntem harika çalışıyor.
Tabii ki her çözümün bir trade-off'u var. Bu yöntemde, bir refresh token iptal edildiğinde, o kullanıcıya ait tüm refresh token'lar (diğer cihazlardaki veya tarayıcılardaki) de geçersiz hale geliyor. Yani kullanıcı sadece bir cihazdan değil, her yerden çıkış yapmış oluyor. Projenizin gereksinimlerine göre bu bir sorun teşkil edebilir veya etmeyebilir.
Siz daha önce JWT token yönetiminde benzer sorunlar yaşadınız mı? Blacklist dışında farklı, temiz çözümler deneyen oldu mu? Yorumlarda fikir alışverişi yapalım!