Ders 04 / 19
Kimlik Doğrulama
Taşıyıcı belirteçlerin saklanması ve yenilenmesi; iki yenileme yanıtının farklı sırayla saklanmasının eski belirteci geri getirip getirmediğinin ölçümü.
Yayın Güncelleme
Bu derste
- Erişim belirteci ile yenileme belirtecinin rollerini ayırır
- İki yenileme yanıtı ters sırayla gelince son yazanın eski belirteci geri getirdiğini ölçer
- Sürüm numarası büyük olanı tutan saklamanın aynı durumda sağlam kaldığını gösterir
- Yenileme belirtecinin ikinci kullanımının reddedilmesini yinelemeye duyarlılık olarak ölçer
İçindekiler
Çoğu arka uç isteği kimlik doğrulama (authentication) ister. İstemci her isteğe bir taşıyıcı belirteç (bearer token) koyar: sunucu belirteci taşıyanı yetkili sayar. Belirteç kısa ömürlüdür; süresi dolunca istemci uzun ömürlü bir yenileme belirteci ile yenisini ister. Belirteçlerden kısa ömürlü olana erişim belirteci denir.
Bu mekanizma iki yerde duyarlıdır. Birincisi saklamadır: yenileme yanıtları ters sırayla gelirse hangisinin saklandığı önemlidir. İkincisi sunucudur: yenileme belirteci genellikle tek kullanımlıktır ve ikinci kullanımı reddedilir. Çekirdek aynıdır.
// Replay.swift — bir islemin yinelemeye ve siraya duyarliligini siniflandirir
struct Trial<State: Equatable & CustomStringConvertible> {
let label: String
let once: State
let twice: State
let forward: State
let backward: State
var sensitivity: String {
switch (once != twice, forward != backward) {
case (false, false): return "saglam"
case (true, false): return "yinelemeye-duyarli"
case (false, true): return "siraya-duyarli"
case (true, true): return "cift-duyarli"
}
}
func report() -> String {
print("\(label) | bir=\(once) iki=\(twice) ab=\(forward) ba=\(backward) -> \(sensitivity)")
return sensitivity
}
}
func replay<State>(_ label: String, from start: State, _ a: (State) -> State, then b: (State) -> State) -> Trial<State> {
Trial(label: label, once: a(start), twice: a(a(start)), forward: b(a(start)), backward: a(b(start)))
}
func countSensitivities(_ results: [String]) {
for name in ["saglam", "yinelemeye-duyarli", "siraya-duyarli", "cift-duyarli"] {
print("\(name): \(results.filter { $0 == name }.count)")
}
}
Belirteç Saklama
Model, belirteci bir sayaçla damgalar: serial belirtecin kaçıncı yenileme olduğunu söyler. Süre gerçek saat yerine bu sayaçla modellenir; böylece ölçüm saate bağlı olmaz.
// Vault.swift — belirtec, kasa ve sunucunun yenileme durumu
struct Token: Equatable {
let value: String
let serial: Int
}
struct Vault: Equatable, CustomStringConvertible {
var current: Token?
var description: String { current.map { "\($0.value)#\($0.serial)" } ?? "bos" }
}
func store(_ token: Token) -> (Vault) -> Vault {
{ _ in Vault(current: token) }
}
func storeNewer(_ token: Token) -> (Vault) -> Vault {
{ vault in
guard let held = vault.current, held.serial >= token.serial else {
return Vault(current: token)
}
return vault
}
}
struct Issuer: Equatable, CustomStringConvertible {
var valid: String
var issued: Int = 1
var rejected: Int = 0
var description: String { "gecerli=\(valid) verilen=\(issued) reddedilen=\(rejected)" }
}
func redeem(_ refresh: String) -> (Issuer) -> Issuer {
{ issuer in
guard refresh == issuer.valid else {
return Issuer(valid: issuer.valid, issued: issuer.issued, rejected: issuer.rejected + 1)
}
return Issuer(valid: "r\(issuer.issued + 1)", issued: issuer.issued + 1, rejected: issuer.rejected)
}
}
store gelen belirteci koşulsuz saklar; son yazan kazanır. storeNewer yalnız daha büyük damgalı belirteci saklar; eşit ya da küçük damgalı gelen yok sayılır. Aşağıdaki ölçümde iki yenileme yanıtı vardır: eski olan a1 birinci, yeni olan a2 ikinci yenilemeden gelir. İki eşzamanlı yenileme isteği aynı anda yola çıkarsa yanıtların hangi sırayla döneceği belli değildir.
// main.swift — iki yenileme yanitinin saklanmasi
let stale = Token(value: "a1", serial: 1)
let fresh = Token(value: "a2", serial: 2)
let lastWins = replay("son-yazan-kazanir", from: Vault(), store(stale), then: store(fresh)).report()
let newerWins = replay("buyuk-damga-kazanir", from: Vault(), storeNewer(stale), then: storeNewer(fresh)).report()
countSensitivities([lastWins, newerWins])
son-yazan-kazanir | bir=a1#1 iki=a1#1 ab=a2#2 ba=a1#1 -> siraya-duyarli buyuk-damga-kazanir | bir=a1#1 iki=a1#1 ab=a2#2 ba=a2#2 -> saglam saglam: 1 yinelemeye-duyarli: 0 siraya-duyarli: 1 cift-duyarli: 0
son-yazan-kazanir sıraya duyarlıdır. Eski yanıt sonra gelirse ba sütunundaki gibi kasada a1 kalır; uygulama süresi dolmuş ya da yenisiyle değiştirilmiş eski bir belirteçle istek atmaya devam eder. buyuk-damga-kazanir sağlamdır: iki sırada da a2 kalır ve aynı yanıtı iki kez saklamak bir şey değiştirmez.
AG10. Model sunucunun her yenilemede artan bir damga verdiğini varsayar. Damga vermeyen bir sunucuda istemci sırayı kendi başına kuramaz.
AG11. Eski belirteç sunucuda hâlâ geçerli olabilir ya da olmayabilir. Ölçüm yalnız istemcinin neyi sakladığına bakar.
İstekte Belirteç ve Tek Uçuş
Taşıyıcı belirteç isteğe Authorization başlığıyla eklenir.
import Foundation
func authorize(_ request: URLRequest, token: String) -> URLRequest {
var signed = request
signed.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
return signed
}
Bu parça KAYNAK kalır. Sıra sorununu saklamadan önce çözmenin başka bir yolu tek uçuştur (single flight): aynı anda yalnız bir yenileme yürür, öteki çağıranlar o yenilemenin sonucunu bekler. Bir aktör bunu doğal biçimde kurar.
// belirtec saglayici — KAYNAK
actor TokenProvider {
private var refreshTask: Task<String, Error>?
func validToken(refresh: @escaping @Sendable () async throws -> String) async throws -> String {
if let task = refreshTask { return try await task.value }
let task = Task { try await refresh() }
refreshTask = task
defer { refreshTask = nil }
return try await task.value
}
}
Bu parça da ölçülmez. İkinci çağıran refreshTask’ı dolu bulur ve yeni bir yenileme başlatmaz. Ancak aktörün durumu yalnız bu await noktaları arasında tutarlıdır; yenileme sürerken kasaya başka yerden yazılırsa aynı sıra sorunu geri döner.
Erişim ve Yenileme Belirtecinin Ömrü
İki belirteç ayrı işler görür. Erişim belirteci her istekle gider; ömrü kısadır, çünkü çalınırsa zararı da kısa sürer. Yenileme belirteci yalnız yenileme isteğinde kullanılır; ömrü uzundur, çünkü kullanıcının her yarım saatte bir yeniden giriş yapmasını önler. Uzun ömürlü olan daha az yerde, daha güvenli saklanır. Uygulamalarda yenileme belirteci genellikle sistemin anahtar deposunda durur; erişim belirteci ise bellekte tutulabilir, çünkü kaybolursa yenilemeyle yeniden alınır.
Bu ayrım ölçümün neden iki yenileme yanıtına bakmasını açıklar. Erişim belirtecinin süresi dolduğunda istemci yenileme belirteciyle yeni bir çift ister. Birden fazla istek aynı anda süresi dolmuş belirteçle yola çıkmışsa, hepsi 401 alır ve hepsi yenilemek ister. Sunucuya aynı yenileme belirteciyle iki istek ulaşır ve yanıtlardan biri ötekinden önce döner. İki yanıt iki ayrı çift taşır; istemci hangisini sakladığına göre ya taze, ya bayat belirteçle devam eder.
Gerçek Uygulamada Nerede Görülür
Bir ana sayfa açıldığında uygulama aynı anda profil, bildirim sayısı ve öneri listesi için üç istek gönderir. Erişim belirtecinin süresi o anda dolmuşsa üçü de 401 döner. Saf bir istemci üç ayrı yenileme isteği gönderir. Sunucu yenileme belirteçlerini bir kez kullanımlık tutuyorsa ilki başarılı olur, öteki ikisi reddedilir ve kullanıcı oturumunun kapandığını görür. Bu, ölçümdeki belirtec-donusumu satırının gündelik biçimidir: ilk kullanım belirteci tüketir ve ikincisi reddedilir.
Sorun kullanıcıya bir hata olarak görünmeyebilir. Uygulama sessizce giriş ekranına dönmüş olabilir ve kullanıcı “kendiliğinden çıkış yaptı” diye şikâyet eder. Günlüklerde yalnız “yenileme reddedildi” satırı görünür. Gerçek neden, yenilemenin birden fazla kez başlatılmasıdır. Bu yüzden aynı anda yalnız bir yenilemenin sürmesini sağlayan bir kural gerekir. Bu kurala tek uçuş (single flight) denir: yenileme sürerken gelen her istek yeni bir yenileme başlatmaz, süren yenilemenin sonucunu bekler.
Yanlış Kurulum ve Düzeltmesi
Yanlış kurulumda her başarısız istek kendi yenilemesini başlatır.
import Foundation
// Yanlış: üç eşzamanlı 401 üç ayrı yenileme başlatır
func refreshAccessToken(using session: URLSession, refresh: String) async throws -> String {
var request = URLRequest(url: URL(string: "https://api.example.com/refresh")!)
request.httpMethod = "POST"
request.httpBody = Data(refresh.utf8)
let (data, _) = try await session.data(for: request)
return String(decoding: data, as: UTF8.self)
}
Bu parça KAYNAK kalır. Düzeltme, yenilemeyi tek bir yerde toplamaktır: bir aktör, süren yenilemeyi bir görev olarak tutar ve ona gelen her çağrıya aynı görevin sonucunu verir.
import Foundation
actor RefreshCoordinator {
private var inFlight: Task<String, Error>?
func freshToken(using perform: @escaping () async throws -> String) async throws -> String {
if let inFlight { return try await inFlight.value }
let task = Task { try await perform() }
inFlight = task
defer { inFlight = nil }
return try await task.value
}
}
Bu da KAYNAK kalır. Aktör, kendi durumuna aynı anda tek bir çağrının dokunmasını garanti eder; bu yüzden inFlight iki çağrı arasında yarışmaz. İlk çağrı görevi başlatır, sonrakiler bekler ve hepsi aynı belirteçle devam eder. Çözüm yinelemeyi sıfırlar: sunucuya tek bir yenileme ulaşır. Ölçümdeki belirtec-donusumu satırı bunun için bir koruma sağlamaz, çünkü model sunucunun yaptığını kurar; korumayı uygulayan taraf istemcidir.
Yine de bu çözümün sınırı vardır. Tek uçuş eşzamanlı yenilemeyi önler; iki yanıtın saklanma sırasını önlemez. Bir önceki yenilemeden kalma geç bir yanıt, yenisinden sonra saklanırsa hâlâ bayat belirteci kasaya yazabilir. Bu yüzden storeNewer benzeri bir damga karşılaştırması da gerekir: iki koruma ayrı iki sorunu çözer.
Belirteç Nerede Saklanır
Modeldeki Vault bir kasadır; gerçek uygulamada karşılığı iki katmanlıdır. Bellek, uygulamanın çalıştığı süre boyunca hızlı erişim sağlar ve uygulama kapanınca kaybolur. Sistemin anahtar deposu ise kapanmadan sonra da kalır ve verisini şifreli tutar. Belirteci kullanıcı tercihleri gibi düz bir depoya yazmak yaygın ve ciddi bir hatadır: düz depo, yedeklere ve dosya dökümlerine olduğu gibi girer. Sonraki konuda anahtar deposu ayrıca ele alınacak; burada önemli olan, saklamanın bir yazma işlemi olduğu ve yazma işleminin de bu kursun bütün ölçüsüne tabi olduğudur.
Saklama sırasındaki ince nokta, anahtar deposuna yazmanın eşzamansız bir iş olabilmesidir. İki yenileme yanıtı art arda gelirse iki yazma da yola çıkar ve depoya hangisinin sonra ulaştığı, hangisinin sonra çağrıldığıyla aynı olmayabilir. Ölçümdeki son-yazan-kazanir satırı bu yüzden gerçekçidir: model, yazmanın kendisini sıraya duyarlı bir işlem olarak kurar. Düzeltme, sıra bilgisini değerin kendisinde taşımaktır; buyuk-damga-kazanir satırının sağlam çıkmasının nedeni budur.
Çıkış Yapmak
Kullanıcı çıkış yaptığında istemci iki şey yapar: kasadaki belirteçleri siler ve sunucuya yenileme belirtecini iptal ettirir. Silme yerel bir işlemdir ve yinelemeye karşı sağlamdır: boş bir kasayı bir kez daha boşaltmak bir şey değiştirmez. İptal isteği ise sunucuya gider ve yeniden denenebilir olmalıdır; bu yüzden iptal, etkisiz bir işlem olarak tasarlanır. Çıkış sırasında hâlâ süren bir yenileme varsa, geç dönen yanıt kasayı yeniden doldurabilir. Kullanıcı çıkış yapmışken oturumun geri gelmesi, sıra sorununun en rahatsız edici biçimidir. Bunu önlemek için çıkış, süren yenilemeyi iptal eder ve dönen yanıtı saklamadan önce oturumun hâlâ açık olup olmadığına bakılır.
Yenilemenin Kendisi Başarısız Olursa
Yenileme isteği de bir istektir ve başarısız olabilir. Burada iki durumu ayırmak gerekir. Yenileme belirteci reddedilmişse, yani sunucu yanıt verip belirteci geçersiz saymışsa, oturum gerçekten sona ermiştir ve kullanıcının yeniden giriş yapması gerekir. Yenileme isteği ise ağ hatasıyla düşmüşse oturum sağlamdır; yalnız bağlantı geçicidir. Bu iki durumu karıştıran istemci, bağlantısı kısa süre kopan kullanıcıyı gereksiz yere oturumdan atar.
Doğru davranış üçüncü dersteki sınıflandırmayı burada da uygulamaktır. Zaman aşımı ve bağlantı yokluğu denenebilir hatalardır; yenileme bütçe dâhilinde yeniden denenir ve kasadaki belirteç silinmez. Sunucunun açıkça reddi ise kasayı boşaltır ve kullanıcıyı giriş ekranına yönlendirir. belirtec-donusumu satırındaki reddedilen sayacı bu ikinci durumun modelidir. Sayacın sıfırdan büyük olması, sunucunun bir yenileme belirtecini reddettiğini gösterir; istemci bunu gördüğünde yeniden denemek yerine oturumu sonlandırmalıdır.
Süre Dolmadan Yenilemek
Yenileme iki zamanda yapılabilir. Tepkisel yaklaşımda istek 401 alınca yenilenir; kod basittir, ama her süre dolumunda bir istek boşa gider ve aynı anda yola çıkmış tüm istekler başarısız olur. Önceden yenileme yaklaşımında erişim belirtecinin süresi dolmadan kısa süre önce yenilenir; böylece 401 hiç görülmez. Bu, sunucunun süre bilgisini yanıtta vermesini gerektirir ve istemci saatine güvenir. Saat kaymışsa belirteç gerçekte henüz geçerliyken yenilenir ya da süresi dolmuşken geçerli sanılır.
Pratikte ikisi birlikte kullanılır: önceden yenileme çoğu hatayı önler, tepkisel yenileme saat kayması gibi kaçan durumları yakalar. İki yolun da tek uçuş kuralından geçmesi gerekir; yoksa önceden yenileme ile tepkisel yenileme aynı anda yola çıkıp aynı yenileme belirtecini iki kez kullanır.
Sık Yapılan Hatalar
- Yenileme yanıtını koşulsuz saklamak. Geç gelen eski yanıt yeni belirteci ezer.
- Her başarısız isteğin ardından ayrı bir yenileme başlatmak. İstekler aynı anda
401alırsa birden çok yenileme yarışır. - Yenileme belirtecini ikinci kez kullanmak. Sunucu bunu reddedebilir ve oturumu da kapatabilir.
- Belirteci günlüğe yazmak ya da hata iletisine koymak.
- Süre dolmasını istemci saatine bağlamak. Saat kayması yanlış kararlar doğurur.
Ölçülen Sonucun Yorumu
İki saklama kuralı aynı iki yanıtı aldı ve farklı sonuç verdi. Fark yanıtlarda değil, kuralda: biri sıraya baktı, öteki damgaya. Damga sırayı yanıtın içine taşıdığı için varış sırası önemsizleşti. Bu, ağda sık kullanılan bir kalıptır: sırayı ağdan beklemek yerine veriye yazmak.
Sınırlayıcı Ölçüm
Saklama sağlam olsa da yenileme belirteci hâlâ yinelemeye karşı korumasızdır. Aşağıda aynı yenileme belirteci sunucuda iki kez kullanılır.
// main.swift — ayni yenileme belirtecinin ikinci kullanimi
let reuse = replay("belirtec-donusumu", from: Issuer(valid: "r1"), redeem("r1"), then: redeem("r1")).report()
countSensitivities([reuse])
belirtec-donusumu | bir=gecerli=r2 verilen=2 reddedilen=0 iki=gecerli=r2 verilen=2 reddedilen=1 ab=gecerli=r2 verilen=2 reddedilen=1 ba=gecerli=r2 verilen=2 reddedilen=1 -> yinelemeye-duyarli saglam: 0 yinelemeye-duyarli: 1 siraya-duyarli: 0 cift-duyarli: 0
İlk kullanım r1’i tüketip r2’yi geçerli kılar; ikinci kullanım r1’i geçersiz bulur ve reddeder. Bu belirteç dönüşümüdür (token rotation) ve ölçümde yinelemeye duyarlı çıktı. İki sıra aynı sonucu verdi, çünkü iki işlem aynıydı. Kural bu reddin bir güvenlik özelliği mi yoksa bir hata mı olduğunu söylemez.
AG12. Sunucunun eski yenileme belirtecini hemen reddettiği varsayılır. Bazı sunucular kısa bir hoşgörü süresi tanır; bu davranış ölçülmedi.
Özet
- Erişim belirteci kısa ömürlüdür; yenileme belirteci uzun ömürlü olup yenisini almaya yarar.
- Yenileme yanıtlarını koşulsuz saklamak sıraya duyarlıdır; geç gelen eski yanıt yeni belirteci ezer.
- Damgası büyük olanı tutan saklama iki sırada da aynı belirteci bıraktı.
- Tek uçuş, aynı anda tek yenilemeyi çalıştırarak sorunu başka yerden çözer.
- Yenileme belirtecinin ikinci kullanımı reddedildi ve bu yinelemeye duyarlı çıktı.
- Bu dersin duyarlılığı: sıraya duyarlı.
Sonraki Adım
Belirteç ve yanıtlar geldikten sonra bazılarının saklanması gerekir. Sıradaki derste Görsel ve Veri Önbelleği, aynı işlem çiftinin boş bir önbellekte sağlam, dolu bir önbellekte ise sıraya duyarlı çıkıp çıkmadığını ölçecek.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.