Developer TurkeyBlog

Node.js ve Express ile JWT ve Rol Bazlı Yetkilendirme

Ben Enes Şahin, full stack web geliştiriciyim. 2022'den beri Node.js ve Express ile geliştirdiğim bir e-ticaret backend'inde JWT ile kimlik doğrulama ve rol bazlı yetkilendirme kullanıyorum. Bu yazıda bu yaklaşımın temel parçalarını, sık yapılan hatalarla birlikte örnek kodla anlatıyorum.

JWT nasıl çalışır, kısaca

JSON Web Token, sunucunun imzaladığı ve içine kullanıcı hakkında bilgi (id, rol gibi) gömdüğü bir metin parçasıdır. Kullanıcı giriş yaptığında sunucu bu token'ı üretir, istemci de sonraki her istekte Authorization başlığında geri gönderir. Sunucu token'ın imzasını doğrular ve içindeki bilgiye güvenir; her istekte veritabanına gidip oturum kontrol etmesine gerek kalmaz.

Örnekte iki token kullanıyorum:

  • Access token: kısa ömürlü (örneğin 15 dakika), her API isteğinde gönderilir.
  • Refresh token: uzun ömürlü, sadece yeni bir access token almak için kullanılır ve httpOnly cookie'de saklanır.

Access token'ı localStorage'da değil, bellekte (in-memory) tutmayı öneriyorum. localStorage JavaScript'ten okunabildiği için bir XSS açığı doğrudan token çalınmasına dönüşür; sayfa yenilendiğinde de zaten refresh token'la yeni bir access token alınıyor.

Token üretimi

src/lib/tokens.ts
import jwt from "jsonwebtoken";
 
type Payload = { userId: string; role: "admin" | "customer" };
 
export function signAccessToken(payload: Payload) {
  return jwt.sign(payload, process.env.JWT_ACCESS_SECRET!, { expiresIn: "15m" });
}
 
export function signRefreshToken(payload: Payload) {
  return jwt.sign(payload, process.env.JWT_REFRESH_SECRET!, { expiresIn: "7d" });
}

Access ve refresh token için ayrı gizli anahtar (secret) kullanmak iyi bir pratik. Biri sızarsa diğerini etkilemeden değiştirebilirsin, iki token da aynı anahtara bağlı olmaz.

Kimlik doğrulama ara katmanı

İstek geldiğinde önce token'ın geçerli olup olmadığına bakan, sonra kullanıcıyı req nesnesine ekleyen bir ara katman yazıyorum:

src/middleware/authenticate.ts
import type { Request, Response, NextFunction } from "express";
import jwt from "jsonwebtoken";
 
export type AuthedRequest = Request & { user?: { userId: string; role: string } };
 
export function authenticate(req: AuthedRequest, res: Response, next: NextFunction) {
  const header = req.headers.authorization;
  const token = header?.startsWith("Bearer ") ? header.slice(7) : null;
  if (!token) return res.status(401).json({ message: "Oturum bulunamadı" });
 
  try {
    req.user = jwt.verify(token, process.env.JWT_ACCESS_SECRET!) as AuthedRequest["user"];
    next();
  } catch {
    res.status(401).json({ message: "Token geçersiz veya süresi dolmuş" });
  }
}

Rol bazlı yetkilendirme

Kimlik doğrulama "bu kullanıcı kim" sorusuna cevap veriyor, yetkilendirme ise "bu kullanıcı bunu yapabilir mi" sorusuna. İkisini ayrı ara katmanlarda tutmak işe yarıyor, çünkü aynı endpoint'e farklı rollerin farklı erişimi olabiliyor:

src/middleware/requireRole.ts
import type { Response, NextFunction } from "express";
import type { AuthedRequest } from "./authenticate";
 
export function requireRole(...allowed: string[]) {
  return (req: AuthedRequest, res: Response, next: NextFunction) => {
    if (!req.user || !allowed.includes(req.user.role)) {
      return res.status(403).json({ message: "Bu işlem için yetkin yok" });
    }
    next();
  };
}

Kullanımı route tanımında okunaklı kalıyor:

src/routes/orders.ts
router.get("/orders", authenticate, requireRole("admin"), listAllOrders);
router.get("/orders/me", authenticate, listMyOrders);

Sık yapılan bir hata: sahiplik kontrolünü atlamak

Rol kontrolü tek başına yeterli değil. GET /orders/:id gibi bir endpoint'te "giriş yapmış herhangi bir müşteri" kontrolü geçse bile, o siparişin gerçekten isteği yapan kullanıcıya ait olup olmadığını ayrıca kontrol etmek gerekiyor:

src/controllers/orders.ts
export async function getOrder(req: AuthedRequest, res: Response) {
  const order = await db.order.findUnique({ where: { id: req.params.id } });
  if (!order) return res.status(404).json({ message: "Sipariş bulunamadı" });
 
  const isOwner = order.userId === req.user!.userId;
  const isAdmin = req.user!.role === "admin";
  if (!isOwner && !isAdmin) return res.status(403).json({ message: "Bu siparişe erişimin yok" });
 
  res.json(order);
}

Bu satırı atlamak, requireRole("customer") geçen herhangi bir müşterinin URL'deki id'yi değiştirerek başka bir kullanıcının siparişini görebilmesi anlamına gelir. Rol kontrolü "müşteri misin" diye sorar, sahiplik kontrolü "bu kayıt senin mi" diye sorar; ikisi farklı sorular ve ikisi de gerekli.

Refresh token'ı iptal edebilmek

Access token'lar kısa ömürlü olduğu için imzalandıktan sonra iptal edilemez, süresinin dolmasını beklemek gerekir. Refresh token'lar için durum farklı: kullanıcı çıkış yaptığında ya da şifresini değiştirdiğinde o token'ın artık geçersiz olması gerekir. Bunun için refresh token'ların hash'ini veritabanında tutup, gelen token bu tabloda yoksa reddetmek yaygın bir yöntem. Token'ın kendisini değil hash'ini saklamak, veritabanı sızsa bile token'ların doğrudan kullanılabilir olmasını engelliyor.

Sonuç

Bu kurulumun özeti: kısa ömürlü access token bellekte, uzun ömürlü refresh token httpOnly cookie'de; kimlik doğrulama ve rol kontrolü ayrı ara katmanlarda; rol kontrolünün üstüne, kaydın gerçekten o kullanıcıya ait olup olmadığını kontrol eden bir sahiplik kontrolü. Küçük bir API için fazla gelebilir ama büyüyen bir projede bu ayrım sonradan söküp takmayı kolaylaştırıyor.

Sık sorulan sorular

JWT nedir ve Express'te nasıl kullanılır?

JWT, sunucunun imzaladığı ve içine kullanıcı id'si ile rol gibi bilgileri gömdüğü bir metindir. Kullanıcı giriş yapınca sunucu token üretir, istemci her istekte Authorization başlığında gönderir ve Express'te bir ara katman (middleware) imzayı doğrular.

Access token ile refresh token arasındaki fark nedir?

Access token kısa ömürlüdür (örneğin 15 dakika) ve her API isteğinde gönderilir. Refresh token uzun ömürlüdür ve yalnızca yeni bir access token almak için kullanılır; httpOnly cookie'de saklanması önerilir.

JWT token localStorage'da saklanmalı mı?

Önerilmez. localStorage JavaScript ile okunabildiği için bir XSS açığı token'ın çalınmasına yol açar. Access token'ı bellekte, refresh token'ı httpOnly cookie'de tutmak daha güvenlidir.

Rol kontrolü yetmiyor mu, sahiplik kontrolü neden gerekir?

Rol kontrolü kullanıcının müşteri ya da admin olduğuna bakar, ama bir kaydın o kullanıcıya ait olup olmadığını denetlemez. Sahiplik kontrolü yoksa giriş yapmış herhangi bir müşteri URL'deki id'yi değiştirerek başkasının siparişini görebilir.

Bu konuda takıldın mı, ya da ekibine geliştirici mi arıyorsun?

Yazıyla ilgili sorunu ya da iş teklifini bana yaz.

İletişime geç
Tüm yazılar