Ders 11 / 20
Güvenli Depolama
Oturum belirteci, parola ve anahtar gibi gizli değerler düz depoda durmaz; işletim sisteminin korumalı deposuna yazılır. Ders oturum kapanınca hesap alt ağacının sökülen olduğunu, belirtecin depodan silinmesinin ise ağaçta görünmediğini ölçer.
Yayın Güncelleme
Bu derste
- Gizli değerlerin neden düz anahtar–değer deposunda tutulmadığını açıklar
- Oturum kapanınca hesap alt ağacının sökülen olduğunu ölçer
- Açılışta belirtecin güvenli depodan okunup oturumu kurduğunu gösterir
- Belirtecin depodan silinmesinin görünüm ağacında görünmediğini ölçer
İçindekiler
Önceki ders uygulama kapansa da kalan veriyi ele aldı. O verinin taslak gibi sıradan bir metin olması işi kolaylaştırıyordu. Oturum belirteci, parola ya da şifreleme anahtarı sıradan değildir: bunları ele geçiren, kullanıcının yerine geçer. Bu ders gizli değerlerin nerede durduğunu ve oturum kapanınca görünüm ağacında ne olduğunu ele alır.
Kursun sorusu değişmez: bu güncelleme görünüm ağacındaki düğüme ne yapıyor? Akıbet korunan, güncellenen, kurulan ya da sökülendir; güncellenen, kurulan ve sökülen düğümlerin toplamı komuttur. Akıbeti elle atamayız, kural verir:
// fate.mjs — bir guncellemenin gorunum agacindaki her dugume ne yaptigini siniflandirir export const FATES = ["korunan", "guncellenen", "kurulan", "sokulen"]; export function el(type, props = {}, children = [], key = null) { return { type, props, children, key }; } function segment(node) { return node.key === null ? node.type : `${node.type}[${node.key}]`; } function identity(node, index) { return node.key === null ? `#${index}` : `key:${node.key}`; } function sameProps(a, b) { const names = [...new Set([...Object.keys(a), ...Object.keys(b)])].sort(); return names.every((n) => JSON.stringify(a[n]) === JSON.stringify(b[n])); } function whole(node, path, fate, ops) { const here = `${path}/${segment(node)}`; ops.push({ fate, path: here }); node.children.forEach((child) => whole(child, here, fate, ops)); } export function reconcile(prev, next, path = "", ops = []) { if (prev === null) { whole(next, path, "kurulan", ops); return ops; } if (next === null) { whole(prev, path, "sokulen", ops); return ops; } if (prev.type !== next.type || prev.key !== next.key) { whole(prev, path, "sokulen", ops); whole(next, path, "kurulan", ops); return ops; } const here = `${path}/${segment(next)}`; ops.push({ fate: sameProps(prev.props, next.props) ? "korunan" : "guncellenen", path: here }); const before = new Map(prev.children.map((child, i) => [identity(child, i), child])); const matched = new Set(); next.children.forEach((child, i) => { const id = identity(child, i); matched.add(id); reconcile(before.has(id) ? before.get(id) : null, child, here, ops); }); prev.children.forEach((child, i) => { if (!matched.has(identity(child, i))) reconcile(child, null, here, ops); }); return ops; } export function printOps(ops) { ops.forEach((op) => console.log(`${op.fate}: ${op.path}`)); } export function countFates(ops) { for (const name of FATES) { console.log(`${name}: ${ops.filter((op) => op.fate === name).length}`); } console.log(`komut: ${ops.filter((op) => op.fate !== "korunan").length}`); }
Model, bu akıbetin kuralını kurar; model çerçevenin kendisi değildir. Kural yalnız ağaçları karşılaştırır, gizli değerin nerede durduğunu görmez.
Gizli Değer Nerede Durur
Düz anahtar–değer deposu uygulamanın veri dizininde, korumasız bir dosyada durur. Yedeğe girebilir, kök yetkisi alınmış bir telefondan okunabilir. Gizli değer için işletim sistemi ayrı bir yer sunar: güvenli anahtar deposu. Değer orada şifreli tutulur, erişim uygulamanın kimliğine ve çoğu zaman telefonun kilit durumuna bağlanır. Her platformun deposu kendi adıyla ve kendi kuralıyla gelir; çerçeve bunların üstüne tek bir yazma ve okuma yüzü koyar.
// Platform kodu: SecretStore
import Security
func saveToken(_ token: Data) -> Bool {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: "session-token",
kSecValueData as String: token,
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
]
SecItemDelete(query as CFDictionary)
return SecItemAdd(query as CFDictionary, nil) == errSecSuccess
}
Bu parça ölçülmez; model platformun deposunu çalıştırmaz. Dikkat edilecek nokta, yazmanın uygulamanın görünümünden tamamen ayrı bir çağrı olmasıdır: depoya yazan kod ekrana hiçbir şey çizmez.
UK1. Model güvenli depoyu bellekteki bir Map olarak kurar ve okuma, yazma, silmeyi Promise ile döndürür. Şifrelemeyi ve erişim koşullarını modellemez; ölçülen şey depoya yazmanın ya da depodan silmenin ağaçta ne bıraktığıdır.
// vault.mjs — guvenli anahtar deposu: bellek ici Map, Promise ile yaz oku sil const secrets = new Map(); export function saveSecret(key, value) { return Promise.resolve().then(() => { secrets.set(key, value); }); } export function readSecret(key) { return Promise.resolve().then(() => (secrets.has(key) ? secrets.get(key) : null)); } export function deleteSecret(key) { return Promise.resolve().then(() => { secrets.delete(key); }); }
UK2. Model uygulamanın ağacını oturum durumundan kurar: belirteç yoksa Login, varsa Account ve altında Profile ile Orders. Başlık her iki durumda da vardır. Belirtecin değerinin kendisi ağaca girmez; ağaç yalnız belirtecin var ya da yok olmasına bakar.
// app.mjs — uygulamanin gorunum agacini oturum durumundan kurar import { el } from "./fate.mjs"; export function renderApp(session) { const body = session.token === null ? el("Login", {}) : el("Account", {}, [el("Profile", { ad: session.ad }), el("Orders", {})]); return el("App", {}, [el("Header", {}, [el("Title", { metin: "Hesabim" })]), body]); }
Çerçevede bu seçim, belirteç durumuna bakan küçük bir ekran kapısıdır.
// Platform kodu: AccountGate
function AccountGate({ token }) {
return token ? <Account /> : <Login />;
}
Oturum Kapanınca
Kullanıcı oturumu kapatır. Durumdaki belirteç silinir ve kapı Login ekranına döner.
// main.mjs import { reconcile, printOps, countFates } from "./fate.mjs"; import { renderApp } from "./app.mjs"; const signedIn = renderApp({ token: "t-1", ad: "Ayse" }); const signedOut = renderApp({ token: null, ad: "" }); const ops = reconcile(signedIn, signedOut); printOps(ops); countFates(ops);
korunan: /App korunan: /App/Header korunan: /App/Header/Title sokulen: /App/Account sokulen: /App/Account/Profile sokulen: /App/Account/Orders kurulan: /App/Login korunan: 3 guncellenen: 0 kurulan: 1 sokulen: 3 komut: 4
/App/Account sökülendir ve onunla birlikte altındaki Profile ile Orders de sökülür: alt ağaç tek parça gider. Yerine Login kurulur. Başlık ve uygulamanın kendisi korunandır. Kullanıcının hesabına ait ekran durumu, kaydırma konumu, yarım yazdığı alan, bu düğümlerle birlikte yok olur. Bu sonuç M10/K02’deki ayrımla örtüşür: sökülenin durumu yok olur; akıbet ömrün yerini değil, güncellemenin düğüme yaptığını sınıflar.
Açılışta Belirteci Okumak
Uygulama yeniden açıldığında bellekteki durum boştur. Oturumun sürüp sürmediğini yalnız güvenli depo bilir. Açılışta belirteç depodan okunur, durum buna göre kurulur.
// main.mjs import { reconcile, printOps, countFates } from "./fate.mjs"; import { renderApp } from "./app.mjs"; import { saveSecret, readSecret } from "./vault.mjs"; const opening = renderApp({ token: null, ad: "" }); saveSecret("oturum", "t-1") .then(() => readSecret("oturum")) .then((token) => { const ops = reconcile(opening, renderApp({ token, ad: "Ayse" })); printOps(ops); countFates(ops); });
korunan: /App korunan: /App/Header korunan: /App/Header/Title sokulen: /App/Login kurulan: /App/Account kurulan: /App/Account/Profile kurulan: /App/Account/Orders korunan: 3 guncellenen: 0 kurulan: 3 sokulen: 1 komut: 4
Açılış ekranı Login ile başlar. Belirteç bulununca Login sökülür, Account ve altındaki iki düğüm kurulur. Ağaçta görülen, depodan okumanın kendisi değil, okumanın durumu değiştirmesidir. Belirteç bulunamasaydı durum değişmez ve ağaç açılıştaki ağaçla aynı kalırdı.
Silmek Ağaçta Görünmez
Oturum kapatmak iki ayrı iştir: durumu temizlemek ve belirteci depodan silmek. İlki ağacı değiştirir. İkincisi ağacın dışında olur. Aşağıdaki ölçüm yalnız ikincisini yapar: durum yerinde dururken belirteç depodan silinir.
// main.mjs import { reconcile, countFates } from "./fate.mjs"; import { renderApp } from "./app.mjs"; import { saveSecret, readSecret, deleteSecret } from "./vault.mjs"; const session = { token: "t-1", ad: "Ayse" }; saveSecret("oturum", "t-1") .then(() => deleteSecret("oturum")) .then(() => readSecret("oturum")) .then((left) => { console.log("depoda kalan: " + left); countFates(reconcile(renderApp(session), renderApp(session))); });
depoda kalan: null korunan: 6 guncellenen: 0 kurulan: 0 sokulen: 0 komut: 0
Depo boşaldı ama ağaçta hiçbir şey olmadı: komut 0’dır, Account hâlâ yerinde durur. Ekran, durumdaki belirtece bakar; depoda ne kaldığına bakmaz. Bu yüzden silme tek başına yetmez ve tek başına görünmez.
Platform Parçasını Satır Satır Okumak
saveToken işlevi dört kararı bir arada taşır. Birincisi, sorgunun sınıfıdır: değer genel bir parola kaydı olarak tanımlanır. İkincisi, kaydın adıdır: oturum belirteci tek bir adla bulunur, bu yüzden okuyan ve silen kod da aynı adı kullanır. Üçüncüsü, değerin kendisidir. Dördüncüsü, erişim koşuludur: değer yalnızca telefon açıkken okunabilir ve başka bir telefona taşınamaz.
Yazmadan önce aynı sorgu ile bir silme çağrısı yapılır. Bunun nedeni, bu depoların aynı adla ikinci bir kayıt eklemeyi reddedebilmesidir; önce silmek, belirteç yenilendiğinde yazmanın başarısız olmasını önler. İşlev son olarak eklemenin başarılı olup olmadığını döner.
Bu parçanın hiçbir satırı ekrana dokunmaz. Yazma başarılı da olsa başarısız da olsa görünüm ağacı aynıdır. Ekrana ulaşan tek şey, uygulamanın bu sonucu durum olarak yazmasıdır: belirteç alındı, oturum açık. Depo ile ağaç arasındaki bu mesafe, bu dersin ölçümlerinin ortak konusudur.
Belirteç Yenilenince
Belirteçlerin ömrü sınırlıdır; uygulama zaman zaman yeni bir belirteç alır ve depodaki değeri değiştirir. Kullanıcı bunu fark etmemelidir. Ağaç açısından da fark edilmemesi gerekir: oturum sürüyor, ekran aynı kalıyor. Bunu iki yenilemeyle ölçelim. İlkinde yalnızca belirteç değişir. İkincisinde belirteçle birlikte kullanıcının adı da değişir.
// main.mjs import { reconcile, printOps, countFates } from "./fate.mjs"; import { renderApp } from "./app.mjs"; const before = renderApp({ token: "t-1", ad: "Ayse" }); console.log("yalniz belirtec:"); countFates(reconcile(before, renderApp({ token: "t-2", ad: "Ayse" }))); console.log("belirtec ve ad:"); printOps( reconcile(before, renderApp({ token: "t-2", ad: "Zeynep" })) .filter((op) => op.fate !== "korunan"), );
yalniz belirtec: korunan: 6 guncellenen: 0 kurulan: 0 sokulen: 0 komut: 0 belirtec ve ad: guncellenen: /App/Account/Profile
İlk yenilemede ağaç değişmedi: belirteç ağaca yazılmadığı için komut 0’dır. Kullanıcı ağaç açısından hiçbir şey görmez, hesap ekranı olduğu yerde durur. İkinci yenilemede yalnız Profile güncellenendir; çünkü değişen özellik, adı gösteren düğümün özelliğidir. Account ve altındaki öteki düğümler korunandır.
Bu iki sonuç aynı ilkenin iki yüzüdür. Gizli değer ağaca girmez; bu yüzden değişmesi ağaçta iz bırakmaz. Ağaca giren bilgi, ekranda görünen bilgidir; ve yalnız o değişince bir düğüm güncellenir. Gizli değerin ağaca girmemesi yalnızca bir sızıntı önlemi değildir, aynı zamanda gereksiz komutları da önler.
Karşı Örnek: Belirteci Ağaca Yazmak
Belirteci kolaylık olsun diye ekran özelliği yapmak akla gelebilir: Account düğümünün bir özelliği olarak belirteç taşınır. Bu çalışır, ama iki bedeli vardır. Birincisi, her yenilemede Account güncellenen olur; ekranda hiçbir şey değişmediği hâlde bir komut doğar. İkincisi ve daha ağırı, gizli değer artık görünüm ağacının bir parçasıdır. Ağaç hata raporlarında, hata ayıklama çıktılarında ve ekran kayıtlarında dolaşır; gizli değer de onunla dolaşır. Depo değeri korumak için vardır ve ağaca kopyalamak bu korumayı bozar.
Sınırlayıcı Ölçüm
Akıbet gizli değerin güvenliğini ölçmez. Aynı oturum kapatma iki biçimde yapılır: belirteç depoda bırakılarak ve depodan silinerek. Ağaç iki durumda da aynı değişir.
// main.mjs import { reconcile, countFates } from "./fate.mjs"; import { renderApp } from "./app.mjs"; import { saveSecret, readSecret, deleteSecret } from "./vault.mjs"; function signOut(withDelete) { const signedIn = renderApp({ token: "t-1", ad: "Ayse" }); const signedOut = renderApp({ token: null, ad: "" }); const clear = withDelete ? deleteSecret("oturum") : Promise.resolve(); return clear.then(() => readSecret("oturum")).then((left) => { console.log(withDelete ? "silerek:" : "silmeden:"); console.log("depo: " + (left === null ? "bos" : "dolu")); countFates(reconcile(signedIn, signedOut)); }); } saveSecret("oturum", "t-1") .then(() => signOut(false)) .then(() => signOut(true));
silmeden: depo: dolu korunan: 3 guncellenen: 0 kurulan: 1 sokulen: 3 komut: 4 silerek: depo: bos korunan: 3 guncellenen: 0 kurulan: 1 sokulen: 3 komut: 4
İki koşumda da komut sayısı aynıdır: hesap alt ağacı sökülür, Login kurulur. Fark yalnız depoda: ilkinde belirteç dolu kalır, ikincisinde boştur. Ağaca bakan biri oturumun gerçekten kapandığını söyleyemez; belirteç depoda duruyorsa uygulama bir sonraki açılışta oturumu yeniden kurar. Akıbet, güncellemenin düğüme yaptığını sınıflar; belirtecin akıbetini sınıflamaz.
Yaygın Yanılgılar
- “Oturum kapandı, ekran değişti, demek ki belirteç silindi.” Ekran durumdan çizilir, depodan değil. Silme ayrı bir çağrıdır ve ayrıca doğrulanır.
- “Gizli değer ekranda göründüğü sürece belleğe girmiştir, depo anlamsızdır.” Bellekteki kopya geçicidir; depo değerin uygulama kapanınca nerede kaldığını belirler.
- “Belirteç düz depoda da durabilir, sonuçta küçük bir metin.” Boyut güvenliği belirlemez; erişimi koruyan şey deponun kendisidir.
- “Silme başarılıysa kullanıcı oturumdan çıkmıştır.” Depodan silmek ile ağaçtan oturum ekranını çıkarmak ayrı işlerdir. Silme ağaçta komut doğurmaz; ekranı değiştiren, belirtecin durumdan kalkmasıdır. İki adım birlikte yapılmazsa depo boşken ekran hesabı göstermeye devam eder, ya da ekran girişe dönmüşken eski belirteç depoda kalır.
- “Belirteç yenilenince ekran da yenilenir.” Yenileme ağaçta komut doğurmaz; belirteç ekranda görünmediği için hiçbir düğüm değişmez.
- “Güvenli depo yalnız belirteç içindir.” Parola, anahtar ve kullanıcıya ait başka gizli değerler de aynı yere yazılır. Seçimi belirleyen değerin biçimi değil, ifşa edilirse doğuracağı zarardır.
- “Oturum kapatınca yalnız belirteç gider.” Ağaçtan hesaba ait bütün ekran gider, ona bağlı yerel görünüm de onunla birlikte. Bir sonraki kullanıcı önceki kullanıcının ekran durumunu görmez; bu istenen bir sonuçtur ve ölçümde alt ağacın tek parça sökülmesiyle görünür.
Özet
- Bu dersin akıbeti: sökülen. Oturum kapanınca
/App/Accountile altındakiProfileveOrdersdüğümleri tek parça sökülür, yerineLoginkurulur. - Açılışta güvenli depodan okunan belirteç durumu değiştirdiğinde
Loginsökülür ve hesap alt ağacı kurulur. - Belirtecin depodan silinmesi ağacı değiştirmez: durum yerinde durdukça
Accountdüğümü korunandır. - Aynı oturum kapatma depoyu dolu ya da boş bırakabilir; ağaç iki durumda da aynı değişir, akıbet gizli değerin akıbetini sınıflamaz.
- Gizli değer düz depoda değil, platformun güvenli anahtar deposunda durur.
Sonraki Adım
Bu derse kadar ağaçtaki değişiklikler durumdan geldi: bir ekranın açılması, bir verinin yüklenmesi, bir oturumun kapanması. Bir de dışarıdan gelen değişiklik vardır: bildirime dokunan kullanıcı uygulamayı belirli bir ekranda açar. Sıradaki ders, Bildirimler ve Derin Bağlantı, bir bağlantının ağaca nasıl girdiğini ölçer.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.