---
title: 'Yenileme Belirteçleri'
source: 'https://academia.sh/tr/kurslar/kimlik-ve-yetki/yenileme-belirtecleri'
course: 'Kimlik Doğrulama ve Yetkilendirme'
language: tr
updated: '2026-08-17T18:06:48+00:00'
license: 'CC BY-SA 4.0'
---

# Yenileme Belirteçleri

Kısa ömürlü erişim belirteci ile uzun ömürlü yenileme belirtecinin ayrılması: yenileme belirtecinin yalnız karma olarak saklanması, her kullanımda dönüştürülmesi ve kullanılmış bir belirtecin yeniden sunulmasının aile iptaliyle sonuçlanmasının ölçülmesi.

Önceki iki ders bir ikilem bıraktı. Durumsuz belirteçte iptal penceresini ömür belirliyordu:
ömür kısaldıkça pencere daralıyor, ama kullanıcı sık sık yeniden giriş yapmak zorunda
kalıyordu. İkilem, tek belirteç varsayımından doğar. İki belirteç kullanıldığında ömür ile
kullanım kolaylığı ayrı ayrı ayarlanabilir.

**Erişim belirteci** (access token) kısa ömürlüdür ve her isteğe iliştirilir. **Yenileme
belirteci** (refresh token) uzun ömürlüdür, yalnız tek bir uca — yenileme ucuna — gider ve
karşılığında taze bir erişim belirteci verir. Bu ayrımın kendisi bir kazanç sağlamaz;
kazanç, uzun ömürlü olanın sunucuda kayıtlı olmasından ve her kullanımda değiştirilmesinden
gelir.

## İki Belirteç, İki Görev

Erişim belirteci her istekte gider; dolayısıyla vekil sunucu, günlük, hata izleme kaydı gibi
bütün ara duraklardan geçer. Yenileme belirteci yalnız yenileme ucuna gider; geçtiği yol
kısadır ve o yol dışında hiçbir yerde görünmez.

İkincisi durumludur. Erişim belirteci imzasıyla doğrulanır ve hiçbir kayda bakılmaz;
yenileme belirteci ise sunucudaki bir satıra karşılık gelir. Bu, önceki dersteki
durumsuzluğun terk edilmesi değildir: kayıt okuması dakikada bir olan yenileme isteğinde
yapılır, saniyede yüzlerce olan kaynak isteğinde değil.

Belirtecin istemci tarafında nereye konacağı ayrı bir karardır ve Uygulama Mimarisi kursunun
Tarayıcıda Kimlik Doğrulama dersinde ele alınmıştır; bu ders sunucu tarafını kurar.

## Yenileme Belirtecinin Saklanması

Yenileme belirteci sunucuda **düz olarak saklanmaz**. Saklanan değer, belirtecin karmasıdır;
belirtecin kendisi yalnız istemcidedir. Belirteç iki parçadan oluşur: aramada kullanılan bir
kimlik ve doğrulamada kullanılan bir sır. Kimlik parçası, bütün satırları tek tek denemeden
doğrudan satıra ulaşmayı sağlar.

```js
// saklama.mjs — yenileme belirtecinin sunucuda hangi bicimde durdugu
// Kullanim: node saklama.mjs
import { randomBytes, createHash } from "node:crypto";
import { DatabaseSync } from "node:sqlite";

const db = new DatabaseSync(":memory:");
db.exec(`CREATE TABLE yenileme (kimlik TEXT PRIMARY KEY, aile TEXT NOT NULL, karma TEXT NOT NULL,
                                uretim INTEGER NOT NULL, kullanim INTEGER)`);

const karma = (sir) => createHash("sha256").update(sir).digest("base64url");

const kimlik = randomBytes(9).toString("base64url");
const sir = randomBytes(32).toString("base64url");
const istemciyeVerilen = `${kimlik}.${sir}`;
db.prepare("INSERT INTO yenileme (kimlik, aile, karma, uretim) VALUES (?, ?, ?, ?)")
  .run(kimlik, "A-1", karma(sir), 1785000000000);

const satir = db.prepare("SELECT * FROM yenileme WHERE kimlik = ?").get(kimlik);
console.log("istemciye verilen :", istemciyeVerilen);
console.log("tablodaki satır   :", JSON.stringify(satir));
console.log("satırda sır var mı:", JSON.stringify(satir).includes(sir));
console.log("sunulan değer doğrulanabiliyor mu:", karma(sir) === satir.karma);
```

```
istemciye verilen : eYxdnXk2HLZc.1wAt5gNZ-E91wUNx_xjYpsdxA7eS7AxYLHqWuQefbCY
tablodaki satır   : {"kimlik":"eYxdnXk2HLZc","aile":"A-1","karma":"SeGl3OEK1MKA2ifVK51meydjt7F-1iABmT01wPg0C3o","uretim":1785000000000,"kullanim":null}
satırda sır var mı: false
sunulan değer doğrulanabiliyor mu: true
```

Kimlik ve sır her çalıştırmada değişir. Satırda sır bulunmuyor; buna karşılık istemcinin
sunduğu değer doğrulanabiliyor. Veritabanının okunması, geçerli yenileme belirteçlerini
vermez.

Burada kullanılan tek geçişli özet, parola saklamada yeterli olmaz. Fark girdinin
öngörülebilirliğindedir: sır 32 bayt rastgele veriden üretilmiştir ve denenerek bulunması
söz konusu değildir. Parolalar için bu koşul sağlanmadığından ayrı bir yöntem gerekir; o
konu ayrıca ele alınacaktır.

## Yenileme Ucu

Aşağıdaki sunucu iki belirteci de üretir, yenileme ucunu kurar ve bir bayrakla dönüşümsüz
kurulumu da üretebilir. Yenileme belirteçleri bir **aileye** (family) bağlıdır: bir girişte
açılan aile, o girişten türeyen bütün yenileme belirteçlerini kapsar.

```js
// yenileme.mjs — kisa omurlu erisim belirteci + uzun omurlu yenileme belirteci
// Kullanim: node yenileme.mjs <port> [dondurmesiz]
//   dondurmesiz: yenilemede ayni yenileme belirteci gecerli kalir (karsi ornek)
// Ortam: ERISIM_MS (erisim belirteci omru, ontanimli 3000)
import { createServer } from "node:http";
import { createHmac, randomBytes, timingSafeEqual, createHash } from "node:crypto";
import { DatabaseSync } from "node:sqlite";

const PORT = Number(process.argv[2] ?? 8541);
const DONDURMESIZ = process.argv.includes("dondurmesiz");
const ERISIM_MS = Number(process.env.ERISIM_MS ?? 3000);
const AILE_OMRU_MS = 30 * 24 * 3_600_000;              // ailenin mutlak omru

const ANAHTAR = randomBytes(32);
const HESAPLAR = new Map([["deniz.aral", { parola: "üç-kitap-1876", kod: "U-1001", rol: "uye" }]]);

const db = new DatabaseSync(":memory:");
db.exec(`
  CREATE TABLE aile (kod TEXT PRIMARY KEY, uye TEXT NOT NULL, acilis INTEGER NOT NULL,
                     iptal INTEGER NOT NULL DEFAULT 0, iptalNedeni TEXT);
  CREATE TABLE yenileme (kimlik TEXT PRIMARY KEY, aile TEXT NOT NULL, karma TEXT NOT NULL,
                         uretim INTEGER NOT NULL, kullanim INTEGER);
`);

// Yenileme belirteci: "<kimlik>.<sir>". Sunucuda yalniz sirrin karmasi durur.
const karma = (sir) => createHash("sha256").update(sir).digest("base64url");
const yenilemeUret = (aile) => {
  const kimlik = randomBytes(9).toString("base64url");
  const sir = randomBytes(32).toString("base64url");
  db.prepare("INSERT INTO yenileme (kimlik, aile, karma, uretim) VALUES (?, ?, ?, ?)")
    .run(kimlik, aile, karma(sir), Date.now());
  return `${kimlik}.${sir}`;
};

const b64 = (v) => Buffer.from(v).toString("base64url");
const erisimUret = (asil) => {
  const govde = b64(JSON.stringify({ ...asil, exp: Date.now() + ERISIM_MS }));
  return `${govde}.${createHmac("sha256", ANAHTAR).update(govde).digest("base64url")}`;
};
const erisimDogrula = (belirtec) => {
  const [govde, imza] = (belirtec ?? "").split(".");
  if (!govde || !imza) return null;
  const beklenen = createHmac("sha256", ANAHTAR).update(govde).digest("base64url");
  const a = Buffer.from(beklenen, "utf8"), b = Buffer.from(imza, "utf8");
  if (a.length !== b.length || !timingSafeEqual(a, b)) return null;
  const icerik = JSON.parse(Buffer.from(govde, "base64url").toString("utf8"));
  return Date.now() < icerik.exp ? icerik : null;
};

const aileIptal = (kod, neden) =>
  db.prepare("UPDATE aile SET iptal = 1, iptalNedeni = ? WHERE kod = ?").run(neden, kod);

// Yenileme belirtecinin degerlendirilmesi: dort ayri ret nedeni ayirt edilir.
const yenilemeCoz = (deger) => {
  const [kimlik, sir] = (deger ?? "").split(".");
  const satir = kimlik ? db.prepare("SELECT * FROM yenileme WHERE kimlik = ?").get(kimlik) : undefined;
  if (!satir) return { hata: "taninmiyor" };

  const a = Buffer.from(karma(sir ?? ""), "utf8"), b = Buffer.from(satir.karma, "utf8");
  if (a.length !== b.length || !timingSafeEqual(a, b)) return { hata: "karma" };

  const aile = db.prepare("SELECT * FROM aile WHERE kod = ?").get(satir.aile);
  if (aile.iptal) return { hata: "aile iptal", aile };
  if (Date.now() - aile.acilis > AILE_OMRU_MS) return { hata: "aile omru doldu", aile };

  // Kullanilmis bir belirtec yeniden sunuldu: kopyanin dolasimda oldugunun isareti.
  if (satir.kullanim !== null && !DONDURMESIZ) {
    aileIptal(aile.kod, "yeniden kullanim saptandi");
    return { hata: "yeniden kullanim", aile };
  }
  return { satir, aile };
};

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) => {
    yanit.writeHead(kod, { "content-type": "application/json; charset=utf-8" });
    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 || parola !== hesap.parola) return json(401, { hata: "kimlik dogrulanamadi" });
    const aile = randomBytes(6).toString("base64url");
    db.prepare("INSERT INTO aile (kod, uye, acilis) VALUES (?, ?, ?)").run(aile, hesap.kod, Date.now());
    return json(200, { erisim: erisimUret({ kod: hesap.kod, rol: hesap.rol }), yenileme: yenilemeUret(aile) });
  }

  if (istek.method === "POST" && yol === "/yenile") {
    const { yenileme } = JSON.parse((await govdeOku(istek)) || "{}");
    const sonuc = yenilemeCoz(yenileme);
    if (sonuc.hata) return json(401, { hata: sonuc.hata });

    const uye = db.prepare("SELECT uye FROM aile WHERE kod = ?").get(sonuc.aile.kod).uye;
    const govde = { erisim: erisimUret({ kod: uye, rol: "uye" }) };
    if (DONDURMESIZ) {
      govde.yenileme = yenileme;                       // ayni belirtec gecerli kalir
    } else {
      db.prepare("UPDATE yenileme SET kullanim = ? WHERE kimlik = ?").run(Date.now(), sonuc.satir.kimlik);
      govde.yenileme = yenilemeUret(sonuc.aile.kod);   // dönüşüm: yeni belirtec
    }
    return json(200, govde);
  }

  if (yol === "/ben") {
    const tasiyici = (istek.headers.authorization ?? "").startsWith("Bearer ")
      ? istek.headers.authorization.slice(7) : null;
    const icerik = erisimDogrula(tasiyici);
    return icerik ? json(200, { kod: icerik.kod }) : json(401, { hata: "erisim belirteci gecersiz" });
  }

  if (yol === "/tanilama") {
    return json(200, {
      aileler: db.prepare("SELECT kod, iptal, iptalNedeni FROM aile").all(),
      yenilemeSayisi: db.prepare("SELECT COUNT(*) n FROM yenileme").get().n,
      kullanilmisSayisi: db.prepare("SELECT COUNT(*) n FROM yenileme WHERE kullanim IS NOT NULL").get().n,
    });
  }
  json(404, { hata: "kaynak yok" });
}).listen(PORT, "127.0.0.1", () => console.log(`yenileme 127.0.0.1:${PORT} dondurmesiz=${DONDURMESIZ}`));
```

## Erişim Belirtecinin Ömrü

Erişim belirtecinin ömrü üç saniyeye çekildiğinde akış gözlenebilir hâle gelir: belirteç
süresi dolunca 401 alınır, yenileme ucundan taze bir belirteç alınır ve istek yeniden
çalışır.

```js
// erisim-omru.mjs — erisim belirtecinin omru ve yenilemeyle tazelenmesi
// Kullanim: ERISIM_MS=3000 node yenileme.mjs 8543 &  calisirken  node erisim-omru.mjs 8543
const PORT = Number(process.argv[2] ?? 8543);
const TABAN = `http://127.0.0.1:${PORT}`;
const gonder = async (yol, govde) =>
  (await fetch(TABAN + yol, {
    method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(govde),
  })).json();
const durum = async (erisim) =>
  (await fetch(`${TABAN}/ben`, { headers: { authorization: `Bearer ${erisim}` } })).status;

const giris = await gonder("/giris", { kullanici: "deniz.aral", parola: "üç-kitap-1876" });
const bas = Date.now();
console.log("giriş anında /ben        :", await durum(giris.erisim));
await new Promise((c) => setTimeout(c, 3200));
console.log("3,2 sn sonra /ben        :", await durum(giris.erisim),
  `(erişim ömrü 3 sn, geçen ${((Date.now() - bas) / 1000).toFixed(1)} sn)`);

const taze = await gonder("/yenile", { yenileme: giris.yenileme });
console.log("yenilemeden sonra /ben   :", await durum(taze.erisim));
console.log("eski erişim belirteciyle :", await durum(giris.erisim));
```

```bash
ERISIM_MS=3000 node yenileme.mjs 8543 > /dev/null 2>&1 & S=$!
sleep 1.2
node erisim-omru.mjs 8543
kill $S
```

```
giriş anında /ben        : 200
3,2 sn sonra /ben        : 401 (erişim ömrü 3 sn, geçen 3.2 sn)
yenilemeden sonra /ben   : 200
eski erişim belirteciyle : 401
```

Son satır önemlidir: yenileme, eski erişim belirtecini geçerli kılmaz. Her erişim belirteci
kendi `exp` değerine bağlıdır ve süresi dolduğunda geri gelmez. Yenilemenin yaptığı tek şey
yeni bir belirteç üretmektir.

## Dönüşümlü Kullanım

**Dönüşüm** (rotation), yenileme belirtecinin her kullanımda değiştirilmesidir: sunucu eski
satırı kullanılmış olarak işaretler ve yeni bir belirteç üretir. Bunun sonucu, her yenileme
belirtecinin **tek kullanımlık** olmasıdır.

Tek kullanımlık olmanın ölçülebilir kazancı, kullanılmış bir belirtecin yeniden sunulduğunda
bir **işaret** taşımasıdır. Normal bir istemci bunu yapmaz; çünkü elindeki belirteci
kullandığında yenisini almıştır. Aynı belirtecin ikinci kez sunulması, o belirtecin bir
kopyasının dolaşımda olduğunu söyler.

```js
// donusum.mjs — donusumlu kullanim ve yeniden kullanim saptamasinin olculmesi
// Kullanim: ERISIM_MS=3000 node yenileme.mjs 8541 &              (donusum var)
//           ERISIM_MS=3000 node yenileme.mjs 8542 dondurmesiz &  (donusum yok)
//           node donusum.mjs 8541   /   node donusum.mjs 8542
const PORT = Number(process.argv[2]);
const TABAN = `http://127.0.0.1:${PORT}`;
const gonder = async (yol, govde) =>
  (await fetch(TABAN + yol, {
    method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(govde),
  })).json();

const giris = await gonder("/giris", { kullanici: "deniz.aral", parola: "üç-kitap-1876" });
const ilkYenileme = giris.yenileme;

// Uc kez yenile; her seferinde eldeki yenileme belirtecini kullan.
let elde = ilkYenileme;
for (let i = 1; i <= 3; i++) {
  const c = await gonder("/yenile", { yenileme: elde });
  console.log(`${i}. yenileme:`, c.hata ? `ret (${c.hata})` : "kabul",
    "| yeni yenileme belirteci mi:", !c.hata && c.yenileme !== elde);
  if (!c.hata) elde = c.yenileme;
}

// Ilk yenileme belirteci yeniden sunuluyor (bir kopyanin dolasimda oldugu durum).
const tekrar = await gonder("/yenile", { yenileme: ilkYenileme });
console.log("ilk belirteç yeniden  :", tekrar.hata ? `ret (${tekrar.hata})` : "kabul");

// En son gecerli belirtecle yeniden denenir.
const sonrasi = await gonder("/yenile", { yenileme: elde });
console.log("güncel belirteçle     :", sonrasi.hata ? `ret (${sonrasi.hata})` : "kabul");
console.log("tanılama:", JSON.stringify(await (await fetch(`${TABAN}/tanilama`)).json()));
```

```bash
ERISIM_MS=3000 node yenileme.mjs 8541 > /dev/null 2>&1 & DONUSUMLU=$!
ERISIM_MS=3000 node yenileme.mjs 8542 dondurmesiz > /dev/null 2>&1 & DONUSUMSUZ=$!
sleep 1.2
echo "— dönüşümlü kullanım —"
node donusum.mjs 8541
echo "— dönüşüm yok —"
node donusum.mjs 8542
kill $DONUSUMLU $DONUSUMSUZ
```

```
— dönüşümlü kullanım —
1. yenileme: kabul | yeni yenileme belirteci mi: true
2. yenileme: kabul | yeni yenileme belirteci mi: true
3. yenileme: kabul | yeni yenileme belirteci mi: true
ilk belirteç yeniden  : ret (yeniden kullanim)
güncel belirteçle     : ret (aile iptal)
tanılama: {"aileler":[{"kod":"hwmd0rUj","iptal":1,"iptalNedeni":"yeniden kullanim saptandi"}],"yenilemeSayisi":4,"kullanilmisSayisi":3}
— dönüşüm yok —
1. yenileme: kabul | yeni yenileme belirteci mi: false
2. yenileme: kabul | yeni yenileme belirteci mi: false
3. yenileme: kabul | yeni yenileme belirteci mi: false
ilk belirteç yeniden  : kabul
güncel belirteçle     : kabul
tanılama: {"aileler":[{"kod":"0hYwgfbk","iptal":0,"iptalNedeni":null}],"yenilemeSayisi":1,"kullanilmisSayisi":0}
```

Aile kodları her çalıştırmada değişir. İki blok arasındaki fark, dönüşümün ne kazandırdığını
tek başına anlatıyor.

Dönüşümlü kurulumda üç yenilemenin ardından tabloda dört satır vardır ve üçü kullanılmıştır.
İlk belirteç yeniden sunulduğunda sunucu bunu **saptar**: satırın `kullanim` alanı doludur.
Kararın sonucu yalnız o isteği reddetmek değildir — aile iptal edilir ve bir sonraki satırda
görüldüğü gibi **güncel** belirteç de artık çalışmaz. Bunun nedeni, hangi tarafın kopya
olduğunun bilinememesidir: yeniden sunan taraf da, elinde güncel belirteci tutan taraf da
aynı aileye aittir. Güvenli karar, ikisini de düşürüp yeniden giriş istemektir.

Dönüşümsüz kurulumda aynı belirteç dört kez kullanıldı, hepsi kabul edildi ve tabloda tek
satır var; `kullanilmisSayisi` sıfır. Bir kopyanın dolaşımda olup olmadığını gösterecek
hiçbir iz yoktur. Sunucunun saptayamadığı bir durum, sunucunun yanıt veremediği bir durumdur.

## Yanlış Alarm ve Ailenin Ömrü

Saptamanın bir kusuru vardır: aynı yenileme belirteciyle **eşzamanlı** iki istek de aynı
işareti üretir. Uygulamanın iki sekmesi ya da iki isteği aynı anda 401 alıp aynı belirteçle
yenilemeye giderse, ikincisi kullanılmış bir belirteç sunmuş olur ve aile boşuna iptal
edilir. Çözüm istemci tarafındadır ve Uygulama Mimarisi kursunun Oturum Yenileme dersinde
**tek uçuş** adıyla kurulmuştur: aynı anda yalnız bir yenileme isteği çıkar, diğerleri onun
sonucunu bekler. Sunucu tarafında ise küçük bir hoşgörü penceresi tanınabilir — dönüşümden
sonraki birkaç saniye içinde gelen aynı istek, aynı yeni belirteçle yanıtlanır.

Ailenin bir de mutlak ömrü vardır. Dönüşüm her kullanımda yeni belirteç ürettiği için,
düzenli kullanılan bir aile kendiliğinden hiç bitmez. `AILE_OMRU_MS` bunu sınırlar: ilk
girişten belirli bir süre sonra aile, ne kadar kullanılırsa kullanılsın kapanır ve yeniden
giriş gerekir. Aile kaydı ayrıca çıkışın karşılığıdır: çıkış isteği aileyi iptal eder,
böylece o girişten türeyen bütün yenileme belirteçleri tek işlemle düşer.

Böylece iptal penceresi iki farklı süreye ayrılmış olur. Erişim belirteci için pencere onun
kısa ömrüdür — üç saniye, on beş dakika, ne seçilmişse. Yenileme belirteci içinse pencere
sıfırdır, çünkü karar veritabanındaki bir satırdır.

## Özet

- Erişim belirteci kısa ömürlüdür ve her isteğe iliştirilir; yenileme belirteci uzun
  ömürlüdür ve yalnız yenileme ucuna gider.
- Yenileme belirteci sunucuda karma olarak saklanır; belirteç, aramada kullanılan bir kimlik
  ile doğrulamada kullanılan bir sırdan oluşur ve veritabanının okunması geçerli belirteç
  vermez.
- Dönüşüm, yenileme belirtecini her kullanımda değiştirir ve onu tek kullanımlık kılar;
  kullanılmış bir belirtecin yeniden sunulması bir kopyanın dolaşımda olduğunun işaretidir.
- Saptama üzerine aile iptal edilir; hangi tarafın kopya olduğu bilinemediği için güncel
  belirteç de düşer.
- Dönüşümsüz kurulumda aynı belirteç sınırsız kullanılır ve yeniden kullanımı gösterecek
  hiçbir iz kalmaz.
- Eşzamanlı yenileme yanlış alarm üretebilir; istemcide tek uçuş, sunucuda kısa bir hoşgörü
  penceresi bunu karşılar. Ailenin mutlak ömrü ve çıkışta iptali, oturumun sonunu belirler.

## Sonraki Adım

Buraya kadar kimlik doğrulamanın bütün adımları tek bir tarafta geçti: kullanıcı parolasını
ödünç servisine verdi, ödünç servisi belirteci üretti ve yine kendisi doğruladı. Kütüphanenin
ilçe şubelerinin ortak bir üye kaydı kullandığı ya da bir üyenin ödünç geçmişini üçüncü taraf
bir okuma uygulamasına açmak istediği durumda bu düzen çalışmaz: parolanın o uygulamaya
verilmesi gerekirdi. Sonraki ders bu sorunu çözen yetki devri protokolünü kurar — kaynak
sahibi parolasını yalnız kendi kimlik sağlayıcısına verir, uygulamaya ise sınırlı bir yetki
çıkar. Yetki kodu akışı, `state` parametresi ve doğrulayıcı, kendi yerel sunucularınızla
uçtan uca çalıştırılacak.
