---
title: 'Oturum Tabanlı Kimlik Doğrulama'
source: 'https://academia.sh/tr/kurslar/kimlik-ve-yetki/oturum-tabanli-kimlik-dogrulama'
course: 'Kimlik Doğrulama ve Yetkilendirme'
language: tr
updated: '2026-08-17T18:06:48+00:00'
license: 'CC BY-SA 4.0'
---

# Oturum Tabanlı Kimlik Doğrulama

Sunucu tarafında tutulan oturum kaydının kurulması ve oturum kimliğinin çerezle taşınması: çerez niteliklerinin yanıt başlığında doğrulanması, girişte kimliğin yenilenmesinin ölçülmesi, çıkışın anında etkili olması ve boşta kalma ile mutlak ömür sınırları.

Temel şemanın çözmediği sorunların ortak kaynağı, sunucunun istekler arasında hiçbir şey
hatırlamamasıydı. Parola her istekte yeniden geçiyordu, çıkış diye bir işlem yoktu ve aynı
hesabın farklı istemcileri ayırt edilemiyordu. Sunucu bir kez doğrulayıp sonucu kendi
tarafında saklarsa üçü de değişir.

Bu derste kurulan yapı **oturumdur** (session): girişte sunucuda bir kayıt açılır, kayda
rastgele bir **oturum kimliği** (session identifier) verilir ve istemci sonraki isteklerde
yalnız bu kimliği taşır. Parola bir daha ağa çıkmaz. Kimliğin kendisi bir anlam taşımaz;
tek işlevi sunucudaki kayda işaret etmektir. Kurulumun doğru olup olmadığını belirleyen iki
ayrıntı, kimliğin hangi niteliklerle taşındığı ve girişte yenilenip yenilenmediğidir.

## Oturum Deposu

Aşağıdaki sunucu bir oturum deposu tutar, girişte kayıt açar, çerez kurar ve her istekte
çerezdeki kimliği kayda çevirir. İki bayrak alır: `guvenli`, çerezi TLS arkasındaki bir
kuruluma göre yollar; `sabit`, girişte kimliği yenilemeyen yanlış kurulumu üretir ve dersin
karşı örneğidir.

```js
// oturum.mjs — sunucu tarafi oturum deposu ve cerezle tasinan oturum kimligi
// Kullanim: node oturum.mjs <port> [guvenli] [sabit]
//   guvenli: cerez __Host- onekiyle, Secure niteligiyle yollanir (TLS arkasi kurulum)
//   sabit  : giriste oturum kimligi YENILENMEZ (yanlis kurulum; dersin karsi ornegi)
// Ortam: BOSTA_MS (bosta kalma siniri), MUTLAK_MS (mutlak omur)
import { createServer } from "node:http";
import { randomBytes, timingSafeEqual } from "node:crypto";

const PORT = Number(process.argv[2] ?? 8521);
const GUVENLI = process.argv.includes("guvenli");
const SABIT = process.argv.includes("sabit");
const BOSTA_MS = Number(process.env.BOSTA_MS ?? 15 * 60_000);
const MUTLAK_MS = Number(process.env.MUTLAK_MS ?? 8 * 3_600_000);

const CEREZ_ADI = GUVENLI ? "__Host-odunc_oturum" : "odunc_oturum";
const HESAPLAR = new Map([["deniz.aral", { parola: "üç-kitap-1876", kod: "U-1001", rol: "uye" }]]);

// Oturum deposu: kimlik -> kayit. Tek surecte Map; birden cok surecte paylasilan bir depo.
const DEPO = new Map();
const yeniKimlik = () => randomBytes(32).toString("base64url");

const esitMi = (a, b) => {
  const x = Buffer.from(a, "utf8"), y = Buffer.from(b, "utf8");
  return x.length === y.length && timingSafeEqual(x, y);
};

const cerezOku = (baslik, ad) =>
  (baslik ?? "").split(";").map((p) => p.trim().split("="))
    .filter(([a]) => a === ad).map(([, d]) => d)[0];

const cerezYaz = (kimlik, omurSn) => {
  const parcalar = [`${CEREZ_ADI}=${kimlik}`, "Path=/", "HttpOnly", "SameSite=Lax", `Max-Age=${omurSn}`];
  if (GUVENLI) parcalar.splice(2, 0, "Secure");
  return parcalar.join("; ");
};

const oturumCoz = (istek) => {
  const kimlik = cerezOku(istek.headers.cookie, CEREZ_ADI);
  if (!kimlik) return { kimlik: null, kayit: null };
  const kayit = DEPO.get(kimlik);
  if (!kayit) return { kimlik, kayit: null };
  const simdi = Date.now();
  if (simdi - kayit.sonErisim > BOSTA_MS || simdi - kayit.acilis > MUTLAK_MS) {
    DEPO.delete(kimlik);                       // suresi gecen kayit depodan dusulur
    return { kimlik, kayit: null };
  }
  kayit.sonErisim = simdi;
  return { kimlik, kayit };
};

const govdeOku = (istek) =>
  new Promise((coz) => { let v = ""; istek.on("data", (p) => (v += p)); istek.on("end", () => coz(v)); });

createServer(async (istek, yanit) => {
  yanit.sendDate = false;
  const yol = istek.url.split("?")[0];
  const json = (kod, govde, baslik = {}) => {
    yanit.writeHead(kod, { "content-type": "application/json; charset=utf-8", ...baslik });
    yanit.end(JSON.stringify(govde));
  };

  if (istek.method === "POST" && yol === "/giris") {
    const { kullanici, parola } = JSON.parse((await govdeOku(istek)) || "{}");
    const hesap = HESAPLAR.get(kullanici ?? "");
    if (!hesap || !esitMi(parola ?? "", hesap.parola)) return json(401, { hata: "kimlik dogrulanamadi" });

    const { kimlik: oncekiKimlik } = oturumCoz(istek);
    let kimlik;
    if (SABIT && oncekiKimlik) {
      kimlik = oncekiKimlik;                   // yanlis: giris oncesi kimlik korunuyor
    } else {
      if (oncekiKimlik) DEPO.delete(oncekiKimlik);   // dogru: eski kayit silinir
      kimlik = yeniKimlik();                         // dogru: yeni kimlik uretilir
    }
    DEPO.set(kimlik, { asil: { kod: hesap.kod, rol: hesap.rol }, acilis: Date.now(), sonErisim: Date.now() });
    return json(200, { girdi: hesap.kod }, { "set-cookie": cerezYaz(kimlik, Math.floor(MUTLAK_MS / 1000)) });
  }

  if (istek.method === "POST" && yol === "/cikis") {
    const { kimlik } = oturumCoz(istek);
    if (kimlik) DEPO.delete(kimlik);           // kayit silinir: karar bir sonraki istekte etkilidir
    return json(200, { cikildi: true }, { "set-cookie": cerezYaz("", 0) });
  }

  if (yol === "/ben") {
    const { kayit } = oturumCoz(istek);
    if (!kayit) return json(401, { hata: "oturum yok" });
    return json(200, { kod: kayit.asil.kod, rol: kayit.asil.rol });
  }

  // Tanilama ucu: yalniz bu derste, depoyu gozlemek icin. Gercek serviste bulunmaz.
  if (yol === "/tanilama/depo") {
    const { kimlik } = oturumCoz(istek);
    return json(200, {
      depodakiKayitSayisi: DEPO.size,
      cerezdekiKimlikOnEki: kimlik ? kimlik.slice(0, 12) : null,
      cerezdekiKimlikDepoda: kimlik ? DEPO.has(kimlik) : null,
    });
  }
  json(404, { hata: "kaynak yok" });
}).listen(PORT, "127.0.0.1", () => console.log(`oturum 127.0.0.1:${PORT} guvenli=${GUVENLI} sabit=${SABIT}`));
```

Oturum kimliği 32 baytlık rastgele veriden üretilir. Bu değerin tahmin edilememesi, kaydın
tek koruması olduğu için pazarlık konusu değildir: sayaçtan, kullanıcı kodundan ya da zaman
damgasından türetilen bir kimlik, kaydın kendisini herkese açar.

## Çerez Nitelikleri

Oturum kimliği çerezle taşınır ve çerezin nitelikleri kimliğin nereye, ne zaman ve kim
tarafından gönderileceğini belirler. Aynı sunucunun iki kurulumu, yanıt başlığında yan yana
görülebilir.

```bash
node oturum.mjs 8521 > /dev/null & YEREL=$!
node oturum.mjs 8522 guvenli > /dev/null & TLS=$!
sleep 1
echo "--- yerel kurulum (TLS yok): Set-Cookie"
curl -s -D - -o /dev/null -X POST -H "content-type: application/json" \
  -d "{\"kullanici\":\"deniz.aral\",\"parola\":\"üç-kitap-1876\"}" \
  http://127.0.0.1:8521/giris | grep -i "^set-cookie"
echo "--- TLS arkasi kurulum: Set-Cookie"
curl -s -D - -o /dev/null -X POST -H "content-type: application/json" \
  -d "{\"kullanici\":\"deniz.aral\",\"parola\":\"üç-kitap-1876\"}" \
  http://127.0.0.1:8522/giris | grep -i "^set-cookie"
kill $YEREL $TLS
```

```
--- yerel kurulum (TLS yok): Set-Cookie
set-cookie: odunc_oturum=ThJZuB7-8PQvlgmrsRE-F9akNm_i_Rp3KIhauu9VLpc; Path=/; HttpOnly; SameSite=Lax; Max-Age=28800
--- TLS arkasi kurulum: Set-Cookie
set-cookie: __Host-odunc_oturum=ehLOT_FJv58aS-OxyMFUTrWNGvkZNewoIirc7qnhRsc; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=28800
```

Kimlik değerleri her çalıştırmada değişir; sabit olan niteliklerdir.

`HttpOnly`, çerezi betikten okunamaz kılar. Etkisi tarayıcıda ortaya çıkar: `document.cookie`
bu çerezi görmez. Frontend Kalitesi kursundaki Siteler Arası Betik Çalıştırma dersinde
gösterilen sızıntı yolu, oturum kimliği için bu nitelikle kapatılır. Sunucu tarafında
görülebilecek tek şey niteliğin yollanmış olmasıdır; uygulanması istemcinin işidir.

`Secure`, çerezin yalnız TLS bağlantısında gönderilmesini şart koşar. Yukarıdaki iki çıktının
tek anlamlı farkı budur ve TLS arkasındaki kurulum için isteğe bağlı değildir: niteliksiz
çerez, şifresiz bir isteğe düşen istemcide açık biçimde ağa çıkar. Yerel kurulumda niteliğin
verilmemesinin nedeni, tarayıcının şifresiz bir kökenden gelen `Secure` çerezi zaten
saklamamasıdır; bu, geliştirme ortamının TLS ile çalışması için ayrıca bir gerekçedir.

`SameSite=Lax`, çerezin başka bir siteden başlatılan isteklere iliştirilmesini sınırlar.
Sunucunun ortam yetkisiyle çalışması — yani çerezin isteğe kim başlatırsa başlatsın
eklenmesi — siteler arası istek sahteciliğinin kaynağıdır; bu nitelik yüzeyi daraltır ama
tek başına yeterli savunma değildir.

`Path=/` çerezin hangi yollara gönderileceğini, `Max-Age` ise istemcide ne kadar
saklanacağını söyler. `__Host-` öneki, konak öneki kuralı gereği çerezin `Secure` olmasını,
`Path=/` taşımasını ve `Domain` belirtmemesini şart koşar; alt alan adlarından çerez
yazılmasını engeller.

Bu niteliklerin hiçbiri sunucudaki kaydın ömrünü belirlemez. `Max-Age` istemcinin çerezi ne
zaman unutacağını söyler; kaydın ne zaman geçersizleşeceğine sunucu karar verir. İkisi
karıştırıldığında ortaya "istemci sildi ama sunucuda hâlâ geçerli" durumu çıkar.

## Girişte Kimliğin Yenilenmesi

Giriş öncesinde istemcinin üzerinde bir oturum kimliği bulunabilir. Sunucu girişte bu kimliği
korursa, giriş öncesinde bilinen bir değer giriş sonrasında yetkili bir kayda işaret etmeye
başlar. Bu duruma **oturum sabitlemesi** (session fixation) denir; değerin istemciye nasıl
yerleştiği ayrı bir konudur, sunucunun ödevi değeri devralmamaktır.

Aşağıdaki ölçüm, giriş öncesi ve sonrası kimlikleri karşılaştırır.

```js
// sabitleme.mjs — giriste oturum kimliginin yenilenip yenilenmediginin olculmesi
// Kullanim: node oturum.mjs 8523 sabit &  ve  node oturum.mjs 8524 &  calisirken
//           node sabitleme.mjs 8523   /   node sabitleme.mjs 8524
const PORT = Number(process.argv[2]);
const TABAN = `http://127.0.0.1:${PORT}`;
const CEREZ_ADI = "odunc_oturum";

// Giris oncesi istemcinin uzerinde bulunan, disaridan belirlenmis bir deger.
const ONCEDEN_BELIRLENMIS = "onceden-belirlenmis-deger-0001";

const girisYap = async (cerezDegeri) => {
  const cevap = await fetch(`${TABAN}/giris`, {
    method: "POST",
    headers: { "content-type": "application/json", cookie: `${CEREZ_ADI}=${cerezDegeri}` },
    body: JSON.stringify({ kullanici: "deniz.aral", parola: "üç-kitap-1876" }),
  });
  const kurulan = (cevap.headers.get("set-cookie") ?? "").split(";")[0].split("=").slice(1).join("=");
  return { kod: cevap.status, kurulan };
};

const benKim = async (cerezDegeri) => {
  const cevap = await fetch(`${TABAN}/ben`, { headers: { cookie: `${CEREZ_ADI}=${cerezDegeri}` } });
  return `${cevap.status} ${await cevap.text()}`;
};

const { kurulan } = await girisYap(ONCEDEN_BELIRLENMIS);
console.log("giriş öncesi değer      :", ONCEDEN_BELIRLENMIS);
console.log("giriş sonrası kurulan   :", kurulan.slice(0, 30) + (kurulan.length > 30 ? "…" : ""));
console.log("kimlik yenilendi mi     :", kurulan !== ONCEDEN_BELIRLENMIS);
console.log("eski değerle /ben       :", await benKim(ONCEDEN_BELIRLENMIS));
console.log("yeni değerle /ben       :", await benKim(kurulan));
```

```bash
node oturum.mjs 8523 sabit > /dev/null & SABIT=$!
node oturum.mjs 8524 > /dev/null & DOGRU=$!
sleep 1
echo "— giriste kimlik yenilenmiyor (yanlis kurulum) —"
node sabitleme.mjs 8523
echo "— giriste kimlik yenileniyor (dogru kurulum) —"
node sabitleme.mjs 8524
kill $SABIT $DOGRU
```

```
— giriste kimlik yenilenmiyor (yanlis kurulum) —
giriş öncesi değer      : onceden-belirlenmis-deger-0001
giriş sonrası kurulan   : onceden-belirlenmis-deger-0001
kimlik yenilendi mi     : false
eski değerle /ben       : 200 {"kod":"U-1001","rol":"uye"}
yeni değerle /ben       : 200 {"kod":"U-1001","rol":"uye"}
— giriste kimlik yenileniyor (dogru kurulum) —
giriş öncesi değer      : onceden-belirlenmis-deger-0001
giriş sonrası kurulan   : Qdk_zPWOapHWgrDAvPOPnKvzzhQA-z…
kimlik yenilendi mi     : true
eski değerle /ben       : 401 {"hata":"oturum yok"}
yeni değerle /ben       : 200 {"kod":"U-1001","rol":"uye"}
```

İlk kurulumda giriş öncesinde bilinen değer, giriş sonrasında U-1001 üyesinin kaydını
döndürüyor. İkinci kurulumda aynı değer 401 alıyor; kayıt yeni ve tahmin edilemez bir
kimliğe taşınmış, eskisi depodan silinmiştir. Kural tek cümledir: yetki düzeyinin değiştiği
her noktada — giriş, hesap değiştirme, ikinci etmen doğrulaması, yetki yükseltme — oturum
kimliği yenilenir.

## Çıkışın Etkisi ve Ömür Sınırları

Oturum kaydı sunucuda durduğu için silme işlemi doğrudan bir sonuç üretir. Kayıt yoksa bir
sonraki istek 401 alır; arada bekleme yoktur.

Kayıt kendiliğinden de düşer. **Boşta kalma sınırı** (idle timeout), son istekten bu yana
geçen süreyi sınırlar; her istek sayacı sıfırlar. **Mutlak ömür** (absolute lifetime),
kaydın açılışından itibaren toplam süreyi sınırlar ve istekle uzamaz. Aşağıdaki ölçümde
boşta kalma sınırı 1,5 saniyeye çekilmiştir.

```js
// omur.mjs — cikisin ve bosta kalma sinirinin ne zaman etkili oldugu
// Kullanim: BOSTA_MS=1500 node oturum.mjs 8525 &  calisirken  node omur.mjs 8525
const PORT = Number(process.argv[2] ?? 8525);
const TABAN = `http://127.0.0.1:${PORT}`;
const CEREZ_ADI = "odunc_oturum";

const giris = await fetch(`${TABAN}/giris`, {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ kullanici: "deniz.aral", parola: "üç-kitap-1876" }),
});
const cerez = (giris.headers.get("set-cookie") ?? "").split(";")[0];
const bas = { cookie: cerez };
const durum = async (yol) => (await fetch(TABAN + yol, { headers: bas })).status;
const depo = async () => await (await fetch(`${TABAN}/tanilama/depo`, { headers: bas })).json();

console.log("giriş sonrası /ben      :", await durum("/ben"), "| depo:", (await depo()).depodakiKayitSayisi);
await fetch(`${TABAN}/cikis`, { method: "POST", headers: bas });
console.log("çıkış sonrası /ben      :", await durum("/ben"), "| depo:", (await depo()).depodakiKayitSayisi);

// Ikinci oturum: bosta kalma siniri
const giris2 = await fetch(`${TABAN}/giris`, {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ kullanici: "deniz.aral", parola: "üç-kitap-1876" }),
});
const bas2 = { cookie: (giris2.headers.get("set-cookie") ?? "").split(";")[0] };
const durum2 = async () => (await fetch(`${TABAN}/ben`, { headers: bas2 })).status;
console.log("yeni oturum /ben        :", await durum2());
await new Promise((c) => setTimeout(c, 800));
console.log("0,8 sn sonra /ben       :", await durum2(), "(sınır 1,5 sn; her istek sayacı sıfırlar)");
await new Promise((c) => setTimeout(c, 1800));
console.log("1,8 sn boşta sonra /ben :", await durum2());
```

```bash
BOSTA_MS=1500 node oturum.mjs 8525 > /dev/null & SUNUCU=$!
sleep 1
node omur.mjs 8525
kill $SUNUCU
```

```
giriş sonrası /ben      : 200 | depo: 1
çıkış sonrası /ben      : 401 | depo: 0
yeni oturum /ben        : 200
0,8 sn sonra /ben       : 200 (sınır 1,5 sn; her istek sayacı sıfırlar)
1,8 sn boşta sonra /ben : 401
```

Çıkış isteğinden sonra depoda kayıt kalmadı ve aynı çerezle yapılan istek 401 aldı. Bu,
oturum tabanlı kurulumun ayırt edici özelliğidir ve sonraki dersin karşılaştırma noktasıdır.

## Deponun Bedeli

Depo sunucuda durduğu için bir bedeli vardır. Her istek deponun okunmasını gerektirir; tek
süreçte bu bir `Map` erişimidir, birden çok süreçte paylaşılan bir depoya yapılan ağ
çağrısıdır. Uygulama yatay ölçeklendiğinde kayıt bütün süreçlerin ulaşabildiği bir yerde
durmalıdır; her sürecin kendi belleğinde tuttuğu depo, isteği başka sürece düşen kullanıcıyı
oturumsuz bırakır. Bu sorunu isteği hep aynı sürece yönlendirerek çözmek, süreç düştüğünde
o sürece bağlı bütün oturumları düşürür.

Buna karşılık depo, ölçülebilir bir yetenek verir: hangi hesabın kaç açık oturumu olduğu,
hangi oturumun ne zaman kullanıldığı ve hangi oturumun tek bir istekle kapatılacağı
sunucunun bildiği bilgilerdir.

## Özet

- Oturum, girişte sunucuda açılan bir kayıttır; istemci yalnız o kayda işaret eden rastgele
  bir oturum kimliği taşır ve parola bir daha ağa çıkmaz.
- Çerez nitelikleri kimliğin nereye ve ne zaman gönderileceğini belirler: `HttpOnly` betikten
  okumayı, `Secure` şifresiz gönderimi, `SameSite` başka siteden başlatılan isteklere
  iliştirilmesini sınırlar; `__Host-` öneki bu kuralları zorunlu kılar.
- `Max-Age` istemcinin çerezi ne kadar saklayacağını söyler; kaydın geçerliliğine sunucu
  karar verir ve ikisi ayrı ömürlerdir.
- Yetki düzeyinin değiştiği her noktada oturum kimliği yenilenir; yenilenmeyen kurulumda
  giriş öncesinde bilinen bir değer giriş sonrasında yetkili kayda işaret eder.
- Kayıt sunucuda durduğu için çıkış anında etkilidir; boşta kalma sınırı son istekten,
  mutlak ömür ise kaydın açılışından ölçülür.
- Deponun bedeli her istekte bir okuma ve yatay ölçeklemede paylaşılan bir depo
  gereksinimidir; karşılığında açık oturumların sayılabilmesi ve tek tek kapatılabilmesidir.

## Sonraki Adım

Deponun bedeli, onu ortadan kaldırma isteğini doğurur. Sunucu kimliği bir kayda çevirmek
yerine, asılın kendisini isteğin taşıdığı değerin **içine** yazabilir ve o değeri imzalayarak
değiştirilmediğinden emin olabilir. Böyle bir kurulumda doğrulama yerel bir hesaba iner,
paylaşılan depo gereksinimi ortadan kalkar ve servisler birbirinin oturum deposuna
bağlanmadan aynı kimliği tanıyabilir. Sonraki ders bu kurulumu yazar ve bedelini ölçer:
sunucuda kayıt yoksa çıkış neyi siler ve silinmeyen bir değer ne kadar süre geçerli kalır?
