İçeriğe geç
academia.sh

Ders 09 / 13

Kimlik Doğrulama ve Yetkilendirme

Giriş yapan kullanıcının kimliğinin hangi isteğe kadar kaldığının, yetki kararının ise neden her istekte yeniden çıktığının ölçümü.

Yayın Güncelleme

Bu derste

  • Kimlik doğrulama ile yetkilendirmeyi, kimliğin ve kararın kalışı üzerinden ayırır
  • Oturuma yazılan kullanıcı kimliğinin istemciye bağlı kaldığını kalış kuralıyla ölçer
  • Kimlikten türetilen bir yetki denetimini her istekte yeniden hesaplanan bir karar olarak kurar
  • Çıkış yapılan isteğin kimliği hangi istekte sildiğini kalış kuralıyla sınıflandırır
İçindekiler

Önceki derslerde bir değerin bir isteğin içinde doğup o istekle birlikte gittiğini gördük: yol parametresi, doğrulama hatası, sorgu önbelleği ve denetleyicinin örnek değişkeni. Sorgu önbelleği adına rağmen bu istek kalışındaydı; ikinci istek sayacı sıfırdan başlattı. Bildirim mesajı (flash) yalnız bir sonraki isteğe geçmişti. Bu derste ilk kez bir değerin oturumda, aynı istemcinin bütün sonraki isteklerinde kaldığını ölçüyoruz. Soru şu: giriş yapan kullanıcı, sonraki her sayfada kim olduğunu nereden biliyor?

Kimlik doğrulama (authentication), isteği gönderenin kim olduğunu belirleme işidir; sonucu bir kimliktir. Yetkilendirme (authorization), o kimliğin şu eylemi yapmaya izni olup olmadığını belirleme işidir; sonucu bir evet ya da hayırdır. İkisi sık birlikte anılır ama zaman bakımından farklı davranırlar ve bu ders o farkı ölçer. Ölçü ekseni kursun ekseni: bu değer hangi isteğe kadar kalıyor? Dört kalış vardır: bu istek, sonraki istek, oturum ve kalıcı. M11/K03’te de kalıcı ile oturum karşı karşıya gelmişti, ama orada soru bir değeri kimin görebildiğiydi. Burada kimsenin kimi gördüğü sorulmuyor; soru zamandır. Aynı değeri yazan istekten sonra kaç istek daha görüyor? M12/K03’te ise bir sonucu kimin değiştirebildiği ölçülmüştü; burada değiştirenle ilgilenmiyoruz, kalışı ölçüyoruz.

Çekirdek

Dersin ölçümleri küçük bir istek döngüsüne dayanır. Aşağıdaki dosya kursun çekirdeğidir ve olduğu gibi verilir. Uygulama bütün isteklerin ve istemcilerin ortak deposunu tutar; her istek yeni bir Baglam alır; oturum ise istemciye göre seçilen bir karma yapıdır. kalis(yaz, oku) bir değeri birinci istekte yazar, sonra aynı istemcinin iki isteğinde ve başka bir istemcinin bir isteğinde okur, değerin nerede görüldüğüne bakıp kalışı sınıflandırır.

# kalis.rb — kucuk bir istek dongusu ve bir degerin hangi istege kadar kaldigini siniflandiran yardimci

# Flash: bu istekte yazilan deger yalniz bir sonraki istege tasinir.
class Flash
  def initialize(gelen)
    @gelen = gelen
    @yeni = {}
  end

  def [](anahtar) = @yeni.fetch(anahtar) { @gelen[anahtar] }

  def []=(anahtar, deger)
    @yeni[anahtar] = deger
  end

  def birakilan = @yeni
end

# Her istek yeni bir baglam alir: params, flash ve istek deposu her istekte bostur.
class Baglam
  attr_reader :uygulama, :params, :oturum, :flash, :istek_deposu

  def initialize(uygulama, oturum)
    @uygulama = uygulama
    @params = {}
    @oturum = oturum
    @flash = Flash.new(oturum.delete(:flash) || {})
    @istek_deposu = {}
  end
end

# Uygulamanin deposu butun isteklerde ve butun istemcilerde aynidir;
# oturum istemciye gore secilir.
class Uygulama
  attr_reader :depo

  def initialize
    @oturumlar = Hash.new { |h, istemci| h[istemci] = {} }
    @depo = { urunler: {}, sema: { urunler: %i[ad fiyat] }, sablonlar: {},
              kuyruk: [], onbellek: {}, sayac: 0 }
  end

  def istek(istemci)
    baglam = Baglam.new(self, @oturumlar[istemci])
    sonuc = yield baglam
    baglam.oturum[:flash] = baglam.flash.birakilan unless baglam.flash.birakilan.empty?
    sonuc
  end
end

# Kural: istek 1'de (istemci A) deger yazilir; istek 2 ve 3 (A) ile istek 4 (B) okur.
# B goruyorsa kalici; A'nin ucuncu istegi goruyorsa oturum; yalniz ikinci istek
# goruyorsa sonraki; hicbiri gormuyorsa bu_istek.
def kalis(yaz, oku)
  uygulama = Uygulama.new
  ilk = uygulama.istek("A") { |b| yaz.call(b); oku.call(b) }
  raise ArgumentError, "deger yazildigi istekte okunamadi" if ilk.nil?

  gorulen = [["A", 2], ["A", 3], ["B", 4]].map do |istemci, sira|
    uygulama.istek(istemci) { |b| oku.call(b) }.nil? ? "--" : "#{istemci}#{sira}"
  end
  tur = if gorulen[2] != "--" then "kalici"
        elsif gorulen[1] != "--" then "oturum"
        elsif gorulen[0] != "--" then "sonraki"
        else "bu_istek"
        end
  [tur, ["A1", *gorulen].join(" "), ilk]
end

Bu blok kaynaktır; dosya diske yazılır ve require_relative ile çekilir. Çekirdek bir çerçeve değil, bir çerçevenin modelidir: oturum bir karma yapıdır, kalıcı depo ise süreç belleğindedir. Gerçek bir uygulamada oturum bir çerez ve bir depo üzerinden, kalıcı depo ise bir veritabanı ya da önbellek deposu üzerinden çalışır; kural değerin davranışını okur, saklandığı yeri okumaz. Ölçümler tek bir uygulama süreci içinde anlamlıdır.

Kimliğin Kalışı

Giriş yapmak, oturuma bir kullanıcı kimliği yazmaktır. Çıkış yapmak onu silmektir. Bir kimliği her istekte sormak yerine bir kez doğrulayıp oturumda tutmanın nedeni de budur: sonraki istekler parolayı yeniden almaz, oturumdaki kimliğe bakar. Aşağıdaki kesit giriş, çıkış ve kimlik okuma yardımcılarını, ayrıca bir yetki tablosunu tanımlar.

# kimlik.rb — giris, cikis, kimlik okuma ve kimlikten turetilen yetki
require_relative "kalis"

YETKILER = { 42 => %i[goster duzenle], 7 => %i[goster] }

def giris_yap(b, kullanici_id)
  b.oturum[:kullanici_id] = kullanici_id
end

def cikis_yap(b) = b.oturum.delete(:kullanici_id)

def kimlik(b) = b.oturum[:kullanici_id]

def izinli_mi?(b, eylem) = (YETKILER[kimlik(b)] || []).include?(eylem)

def goster(ad, tur, kanit, deger)
  printf("%-14s%-14s%-13s%s\n", ad, tur, kanit, deger.inspect)
end

Bu blok da kaynaktır. YETKILER kimlikten izin listesine giden bir tablodur; izinli_mi? kimliği oturumdan okur, tabloda arar ve bir mantıksal değer döndürür. Kimliği hiç bilinmeyen biri için YETKILER[nil] nil döner, || [] onu boş listeye çevirir ve sonuç false olur. Şimdi birinci gözlem: oturuma yazılan kullanıcı kimliği.

# kullanici.rb — kullanici kimligi hangi istege kadar kaliyor
require_relative "kimlik"

yaz = ->(b) { b.oturum[:kullanici_id] = 42 }
oku = ->(b) { b.oturum[:kullanici_id] }
goster("kimlik", *kalis(yaz, oku))
kimlik        oturum        A1 A2 A3 --  42

Kimlik A’nın üç isteğinde de göründü, B’nin isteğinde görünmedi. Bu oturum kalışıdır: değer yazan istemcinin bütün isteklerine kalır, başka istemcilere geçmez. Kalıcı olmaması bilerek seçilmiş bir sonuçtur. Kimlik uygulamanın ortak deposuna yazılsaydı B de A’nın kimliğiyle görünürdü. UY1. Oturumun A’nın isteklerinde kaldığı yalnız üç istek boyunca ölçüldü; oturumun ne zaman sona ereceği, yani zaman aşımı ya da çerezin silinmesi bu çekirdekte modellenmedi.

Her İstemcinin Kendi Kimliği Vardır

Oturum istemciye göre seçildiği için iki istemci aynı uygulamada farklı kimliklerle çalışabilir ve birbirinin kimliğini görmez. Aşağıdaki ölçümde A kullanıcı 42 olarak, B kullanıcı 7 olarak girer; sonra her istemci kimliğini okur. Son adımda B çıkış yapar ve A’nın kimliğine yeniden bakılır.

# istemciler.rb — iki istemci, iki kimlik
require_relative "kimlik"

uygulama = Uygulama.new
uygulama.istek("A") { |b| giris_yap(b, 42) }
uygulama.istek("B") { |b| giris_yap(b, 7) }
["A", "B", "A"].each do |istemci|
  puts "#{istemci} #{uygulama.istek(istemci) { |b| kimlik(b) }.inspect}"
end
uygulama.istek("B") { |b| cikis_yap(b) }
puts "cikis sonrasi A #{uygulama.istek("A") { |b| kimlik(b) }.inspect}"
puts "cikis sonrasi B #{uygulama.istek("B") { |b| kimlik(b) }.inspect}"
A 42
B 7
A 42
cikis sonrasi A 42
cikis sonrasi B nil

Her istemci yalnız kendi kimliğini okudu ve B’nin çıkışı A’nın oturumuna dokunmadı. Kimliğin oturum kalışında olmasının pratik anlamı budur: değer istemciye bağlıdır, uygulamanın ortak deposuna değil. Bu ayrım, aynı sunucu sürecine bağlanan çok sayıda kullanıcının birbirine karışmamasının dayanaklarından biridir; ölçüm iki istemciyle yapıldı.

Yetki Kararı Her İstekte Yeniden Çıkar

Kimlik oturumda kaldığına göre yetki kararı da kalıyor mu? Karar kimlikten türetilir ve türetme ucuzdur; ders onu oturuma yazıp yazmamayı bir seçim olarak ele alır. Önce türetmenin kendisine bakalım: iki kullanıcı ve giriş yapmamış bir istemci, aynı iki eylem için sorulur.

# yetki.rb — ayni kimlik, iki eylem, uc istemci
require_relative "kimlik"

uygulama = Uygulama.new
uygulama.istek("A") { |b| giris_yap(b, 42) }
uygulama.istek("B") { |b| giris_yap(b, 7) }

["A", "B", "C"].each do |istemci|
  sonuc = uygulama.istek(istemci) { |b| [izinli_mi?(b, :goster), izinli_mi?(b, :duzenle)] }
  puts "#{istemci} #{sonuc.inspect}"
end
A [true, true]
B [true, false]
C [false, false]

Aynı iki soru, üç farklı cevap verdi ve fark yalnız oturumdaki kimlikten geldi. C hiç giriş yapmadığı için iki eylemde de reddedildi. Yetkilendirme, kimlik doğrulamanın ürettiği değeri okuyan ayrı bir adımdır: kimlik olmadan karar çıkmaz, ama karar da kimliğin içinde saklı değildir.

Şimdi kararın kalışını ölçelim. Karar giriş yapılan istekte hesaplanıp yalnız o isteğin istek deposuna konuyor.

# karar.rb — yetki karari hangi istege kadar kaliyor
require_relative "kimlik"

yaz = lambda do |b|
  giris_yap(b, 42)
  b.istek_deposu[:karar] = izinli_mi?(b, :duzenle)
end
oku = ->(b) { b.istek_deposu[:karar] }
goster("yetki karari", *kalis(yaz, oku))
yetki karari  bu_istek      A1 -- -- --  true

Karar bu istek kalışında. Sonraki istekte istek deposu boş başlar ve karar yeniden hesaplanır. Bunun pratik sonucu şudur: bir kullanıcının izni değişirse, bir sonraki istek yeni izinle çalışır; eski kararı taşıyan bir kopya yoktur. Karar oturuma yazılsaydı, izin tablosunun değişmesi o oturum bitene kadar görünmezdi. UY2. Burada karar ucuz bir tablo aramasıdır. Hesabı pahalı bir kararı saklamak ayrı bir tasarım sorusudur ve bir sonraki iki derste, işleri ve önbelleği anlatırken kalış açısından ele alınır.

Kararı Oturuma Yazarsak

Yetki kararının neden her istekte yeniden hesaplandığını yukarıda bir seçim olarak ele aldık. Seçimin sonucunu ölçmek için öteki yolu deneyelim: karar giriş yapılan istekte hesaplanıp kimlikle birlikte oturuma yazılıyor.

# oturumda_karar.rb — oturuma yazilan yetki karari hangi istege kadar kaliyor
require_relative "kimlik"

yaz = lambda do |b|
  giris_yap(b, 42)
  b.oturum[:karar] = izinli_mi?(b, :duzenle)
end
oku = ->(b) { b.oturum[:karar] }
goster("oturumda karar", *kalis(yaz, oku))
oturumda kararoturum        A1 A2 A3 --  true

Karar kimlik gibi oturum kalışına geçti: aynı istemcinin bütün sonraki isteklerinde görünüyor. Kalışın uzaması kararı doğru yapmaz. Bunu görmek için izin tablosunu karar yazıldıktan sonra değiştirelim ve iki yolu yan yana koyalım: oturumda saklanan karar ile her istekte yeniden hesaplanan karar.

# bayat_karar.rb — izin tablosu degisince saklanan karar ile yeniden hesaplanan karar
require_relative "kimlik"

uygulama = Uygulama.new
uygulama.istek("A") do |b|
  giris_yap(b, 42)
  b.oturum[:karar] = izinli_mi?(b, :duzenle)
end
YETKILER[42] = %i[goster]
saklanan = uygulama.istek("A") { |b| b.oturum[:karar] }
yeniden = uygulama.istek("A") { |b| izinli_mi?(b, :duzenle) }
puts "saklanan #{saklanan}, yeniden hesaplanan #{yeniden}"
saklanan true, yeniden hesaplanan false

İzin geri alındığı hâlde oturumdaki karar eski değeri söylemeye devam etti; yeniden hesaplanan karar ise yeni durumu gösterdi. Bayatlama, kalışı uzatılmış bir değerin kaynağıyla bağının kopmasıdır. Aynı sorunla Önbellekleme dersinde bir parça için karşılaşacağız. Kimliği oturumda tutmanın ve kararı tutmamanın nedeni de buradadır: kimlik, çıkış yapılana kadar (ya da oturum sona erene kadar) değişmeyen bir olgudur; karar ise kimlikten başka bir şeye, izin tablosuna da bağlıdır.

Giriş Yapmamış ve Tanınmayan Kimlik

Yetki kararı kimlikten türediği için kimliğin bulunmadığı iki durum vardır: istemci hiç giriş yapmamıştır ya da oturumdaki kimlik izin tablosunda yer almaz. İki durumda da izinli_mi? bir hata fırlatmak yerine karar vermek zorundadır. Aşağıdaki ölçüm üç istemciyi yan yana koyar: girişsiz bir istemci, tabloda olmayan 99 numaralı kullanıcı ve tabloda olan 42 numaralı kullanıcı.

# taninmayan.rb — kimligi olmayan ve tanimayan istemcilerin yetki karari
require_relative "kimlik"

uygulama = Uygulama.new
uygulama.istek("B") { |b| giris_yap(b, 99) }
uygulama.istek("C") { |b| giris_yap(b, 42) }
%w[A B C].each do |istemci|
  kimlik, karar = uygulama.istek(istemci) { |b| [kimlik(b), izinli_mi?(b, :goster)] }
  puts "#{istemci} #{kimlik.inspect} #{karar}"
end
A nil false
B 99 false
C 42 true

Girişsiz istemcinin kimliği boştur ve karar olumsuzdur; 99 numaralı kullanıcının kimliği vardır, ama izin tablosunda karşılığı olmadığı için karar yine olumsuzdur. Kimlik ile yetki ayrı iki sorudur: birincisi “bu istemci kim?”, ikincisi “bu kişi bunu yapabilir mi?”. Kimliğin oturumda bulunması, yetkinin bulunduğu anlamına gelmez. Karar tablodan okunduğu için, tabloda olmayan her kimlik varsayılan olarak yetkisiz kalır.

Çıkış Yapmak

Çıkış, oturumdan kimliği silmektir. Kuralımız yazıyı yalnız birinci istekte yapar; çıkışı ölçmek için oku lambda’sı kendi çağrı sayısını tutar ve ikinci istekte kimliği okuduktan sonra siler. Birinci ve ikinci istek kimliği görür, üçüncü ve dördüncü görmez.

# cikis.rb — ikinci istekte cikis yapilirsa kalis ne olur
require_relative "kimlik"

yaz = ->(b) { giris_yap(b, 42) }
cagri = 0
oku = lambda do |b|
  cagri += 1
  deger = kimlik(b)
  cikis_yap(b) if cagri == 2
  deger
end
goster("cikis", *kalis(yaz, oku))
cikis         sonraki       A1 A2 -- --  42

Kural bu değeri sonraki istek olarak sınıflandırdı, çünkü yalnız ikinci istek onu gördü. Aynı kimlik, çıkış olmadan oturum kalışındaydı. Bu bir çelişki değil, kuralın davranışı okuduğunun kanıtıdır: değer oturumda saklanıyor, ama çıkış onu erken siliyor ve kalış kısalıyor. Çıkışın amacı da budur. UY3. Çıkışı ikinci istekte yaptırmak ölçümün düzeneğidir; gerçek bir uygulamada çıkışın hangi istekte geleceğini kimse önceden bilmez.

Sınırlayıcı Ölçüm

Bu dersin kuralı bir sınırı da taşır. kalis, değeri yazıldığı istekte bile okuyamıyorsa bir sınıflandırma yapmaz, istisna fırlatır. Bunu yazan tarafın hiçbir şey yazmadığı bir düzenekle ölçelim.

# sinir.rb — hic deger yazilmazsa kural ne yapiyor
require_relative "kimlik"

begin
  kalis(->(b) { }, ->(b) { kimlik(b) })
rescue => e
  puts e.class
end
ArgumentError

Kural ArgumentError fırlattı. Yani yazıldığı istekte okunamayan bir değer hiçbir kalışa sınıflandırılamaz; kalış, değerin en az bir kez görünmesini ön koşul sayar. Giriş yapılmamış bir istemcinin kimliği bu yüzden bir kalış taşımaz: yoktur. Dersin dört ölçümü de bu sınırın içinde kaldı. Kimlik oturumda, karar bu istekte, oturuma yazılan karar oturumda, çıkışta kimlik sonraki istekte kaldı. Sınır ayrıca bir uyarıdır: bir değerin kalışını ölçmeden önce, değerin ilk istekte gerçekten var olduğundan emin olmak gerekir. Kimliği okuyan bir oku lambda’sı yanlış anahtara bakarsa, kuralın cevabı kalış değil bir hata olur.

Özet

  • Kimlik doğrulama bir kimlik üretir, yetkilendirme o kimlikten bir evet ya da hayır türetir; ikisi ayrı adımlardır.
  • Oturuma yazılan kullanıcı kimliği, yazan istemcinin bütün isteklerinde görünür ve başka istemcide görünmez; iki istemci aynı uygulamada farklı kimliklerle çalışır. Bu dersin kalışı oturumdur.
  • Yetki kararı kimlikten her istekte yeniden hesaplanırsa bu istek kalışındadır ve izin değişikliği bir sonraki istekte görünür; oturuma yazılırsa kalışı oturuma uzar ve izin değişince bayatlar.
  • Girişsiz ya da tabloda olmayan kimlik varsayılan olarak yetkisiz kalır; kimliğin bulunması yetkinin bulunduğunu göstermez.
  • Çıkış yapılan istek kimliği oturumdan siler; silen istekten sonra kimlik kaybolur ve kural onu sonraki istek olarak görür.
  • kalis, yazıldığı istekte okunamayan değeri sınıflandırmaz ve ArgumentError fırlatır.

Sonraki Adım

Kimlik oturumda kalıyor, karar her istekte yeniden çıkıyor; peki istek bittikten sonra yapılması gereken bir iş nerede bekler? Bir sonraki ders arka plan işlerini ölçer: kuyruğa konan bir işin hangi isteğe kadar kaldığını ve bu kalışın oturumdan neden farklı olduğunu.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat