İçeriğe atla

ASP.NET Core 6/Cookie'ler

Vikikitap, özgür kütüphane

Varsayılan durumda HTTP protokolü stateless bir protokoldür, diğer bir deyişle hafızası yoktur. Sunucu her gelen talebe aynı muameleyi çeker. HTTP protokolünün bu eksikliği cookie'ler aracılığıyla giderilir. Cookie'ler sayesinde sunucu belirli bir istemciden gelen ardışık istekleri birbiriyle ilişkilendirip daha daha sonraki isteklerde yapılan işlemlerin daha önceden yapılan isteklerden etkilenmesine imkan tanır. Bir alışveriş sitesinde oturum açmamış olsanız bile belirli bir kategorideki ürünleri ziyaret ettikten sonra sonraki sayfalarda benzer ürünlerin önerilmesi bu yüzdendir.

Cookie'ler çok basit bir mantıkla çalışır. İstemci sunucuyu ilk kez ziyaret ettiğinde sunucu istemciye cookie denilen metin bazlı bir dosya verir. Bu dosyada istemciyi diğer istemcilerden ayıran bilgiler vardır. İstemci daha sonraki her talebinde cookie'yi sunucuya tekrar gönderir. Sunucu kendi tarafında dağıttığı cookie'lerin bir kaydını tutar. Kaydını tuttuğu bir cookie tekrar geldiğinde ilişkilendirmeyi yapar.

Cookie'leri nüfus cüzdanına benzetebiliriz. Doğduğunuzda devlet size nüfus cüzdanı verir. Daha sonra devletle her işiniz olduğunda nüfus cüzdanınızla devlet dairesine başvurursunuz ve devlet sizi hatırlar.

Cookie'lerin kullanımı

[değiştir]

ASP.NET Core'da cookie'lere endpoint'ler ve middleware'ler tarafından HttpContext nesnesi üzerinden erişilen HttpRequest ve HttpResponse nesneleri üzerinden erişilir. Örnek (Program.cs dosyası):

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/cookie", async context => {
    int counter1 = int.Parse(context.Request.Cookies["counter1"] ?? "0") + 1;
    context.Response.Cookies.Append("counter1", counter1.ToString(), new CookieOptions { MaxAge = TimeSpan.FromMinutes(30) });
    int counter2 = int.Parse(context.Request.Cookies["counter2"] ?? "0") + 1;
    context.Response.Cookies.Append("counter2", counter2.ToString(), new CookieOptions { MaxAge = TimeSpan.FromMinutes(30) });
    await context.Response.WriteAsync($"Counter1: {counter1}, Counter2: {counter2}");
});
app.MapGet("clear", context => {
    context.Response.Cookies.Delete("counter1");
    context.Response.Cookies.Delete("counter2");
    context.Response.Redirect("/");
    return Task.CompletedTask;
});
app.MapFallback(async context => await context.Response.WriteAsync("Hello World!"));
app.Run();

Bu programda şunlar yapılmaktdır:

  • "/cookie" path'ı talep gelirse:
    • Eğer counter1 isimli bir cookie varsa bu cookie'nin değerinin 1 artırılmışını counter1 değişkenine ata. Eğer counter1 isimli bir cookie yoksa 1 değerini counter1 değişkenine ata.
    • counter1 değişkeninin değerini counter1 isimli cookie'ye yaz. Böyle bir cookie yoksa oluşturup yaz.
    • Eğer counter2 isimli bir cookie varsa bu cookie'nin değerinin 1 artırılmışını counter2 değişkenine ata. Eğer counter2 isimli bir cookie yoksa 1 değerini counter2 değişkenine ata.
    • counter2 değişkeninin değerini counter2 isimli cookie'ye yaz. Böyle bir cookie yoksa oluşturup yaz.
    • counter1 ve counter2 değişkenlerinin değerlerini ekrana yaz.
  • "/clear" path'ına bir talep gelirse bütün cookie'leri temizle ve akışı / path'ına yönlendir.

Yapılan işlem aslında basitçe cookie değişkenini almak, 1 artırmak, sonra tekrar cookie'ye yazmaktır. Diğer bir deyişle her gelen talepte cookie'nin değerini 1 artırmaktır. İlk gelen talepte cookie olmadığı için başlangıç değerini 0 olarak ele alır.

İstemci tarafından gönderilen cookie'lere HttpRequest nesnesi üzerinden erişilir. İstemciye cookie göndermek için HttpResponse nesnesi kullanılır. HttpResponse sınıfının Cookies özelliğinin Append() metodu belirtilen cookie varsa değerini değiştirir, cookie yoksa ekleme yapar. Burada üretilen her iki cookie'nin de ömrü 30 dakikadır. 30 dakika sonra tarayıcı cookie'leri siler. Cookie'lerin ömrünü ayarlamak için kullanılabilecek CookieOptions sınıfının benzer özellikleri şunlardır:

Domain: Tarayıcının cookie'yi göndereceği hostları (sunucuları) belirtir. Varsayılan durumda cookie'yi ilk hangi sunucu göndermişse daha sonraki taleplerde cookie sadece o sunucuya gönderilir.
Expires: Cookie'nin belirli bir tarih-zamandan sonra ömrünün dolmasını sağlar.
HttpOnly: Değeri true yapıldığında yalnızca tarayıcı tarafından açık talep yapıldığında cookie gönderilir. Arkaplanda JavaScript tarafından yapılan taleplerde cookie gönderilmez.
IsEssential: Cookie'leri esas cookie'ler ve yardımcı cookie'ler olmak üzere ikiye ayrılır. Bu özelliğin true olması cookie'nin esas olduğu anlamına gelir.
MaxAge: Cookie'nin ne kadar süre yaşayacağını belrtir.
Path: Eğer belirli bir path değeri ayarlanırsa host'un sadece belirli bir path'ına yapılan taleplerde cookie gönderilir.
SameSite: Çapraz site taleplerinde cookie'nin gönderilip gönderilmeyeceğini belrtir. Olası değerler Lax, Strict ve None'dır. Varsayılanı None'dır.
Secure: true yapılırsa cookie yalnızca HTTPS bağlantılarında gönderilir.

Çoğu tarayıcı belirli bir web sayfasındayken ilgili web sayfasının gönderdiği cookie'leri görmemize izin verir. Örneğin Chrome'da açık olan web sayfasının gönderdiği cookie'leri görmek için Chrome menüsünü açarız, menüdeki "Diğer araçlar" altındaki "Geliştirici araçları"na tıklarız, açılan pencerede sol taraftaki "Storage" grubunun altındaki "Cookies" ögesinin altındaki host adresine tıklarız. Web sayfasının gönderdiği tüm cookie'ler tüm özellikleriyle beraber gözükür. İşte biraz önce CookieOptions sınıfı aracılığıyla belirttiğimiz özellikler bu özelliklerdir. Bu pencerede cookie'leri silebilir ve değerlerini değiştirebiliriz.

[değiştir]

Çoğu sitede ilgili sitenin cookie kullandığı belirtilir ve cookie'lere izin verip vermeyeceğimizi belirttiğimiz kutucuklar bulunur. ASP.NET Core'un da kullanıcının rıza verip vermediği bilgisini tutma, kullanıcı rıza vermemişse esas olmayan cookie'leri devredışı bırakma, kullanıcı rıza vermişse bütün cookileri'i serbest bırakma yeteneği vardır. Örnek (Program.cs dosyası):

var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<CookiePolicyOptions>(opts => {
    opts.CheckConsentNeeded = context => true;
});
var app = builder.Build();
app.UseCookiePolicy();
app.MapGet("/cookie", async context => {
    int counter1 = int.Parse(context.Request.Cookies["counter1"] ?? "0") + 1;
    context.Response.Cookies.Append("counter1", counter1.ToString(), new CookieOptions
    {
        MaxAge = TimeSpan.FromMinutes(30),
        IsEssential = true
    });
    int counter2 = int.Parse(context.Request.Cookies["counter2"] ?? "0") + 1;
    context.Response.Cookies.Append("counter2", counter2.ToString(), new CookieOptions { MaxAge = TimeSpan.FromMinutes(30) });
    await context.Response.WriteAsync($"Counter1: {counter1}, Counter2: {counter2}");
});
app.MapGet("clear", context => {
    context.Response.Cookies.Delete("counter1");
    context.Response.Cookies.Delete("counter2");
    context.Response.Redirect("/");
    return Task.CompletedTask;
});
app.MapFallback(async context =>
await context.Response.WriteAsync("Hello World!"));
app.Run();

Options pattern'i aracılığıyla UseCookiePolicy middleware'i konfigüre edilir. UseCookiePolicy middleware'inin ayar sınıfı olan CookiePolicyOptions sınıfının önemli özellikleri şunlardır:

CheckConsentNeeded: Tipi, paramtere olarak HttpContext alan ve geriye bool döndüren bir temsilcidir. Belirtilen fonksiyon her gelen request'te çalıştırılır ve geri dönüş değeri cookie rızası gerekip gerekmediğini belirtir. Fonksiyon geriye true değer döndürüyorsa cookie rızası gerekiyordur, geriye false değer döndürüyorsa rıza gerekmiyordur. Varsayılan durumdaki fonksiyon (bu özelliğe fonksiyon atanmadığındaki durum) her request'te false döndürür.
ConsentCookie: Rıza cookie'sidir. Kullanıcının cookie'lere izin verip vermediği bilgisi de cookie aracılığıyla tutulur.
HttpOnly: Cookie'lerin HttpOnly özelliği için varsayılan değeri belirtir.
MinimumSameSitePolicy: Cookie'lerin SameSite özelliği için olası en düşük güvenlik seviyesini belirtir.
Secure: Cookie'lerin Secure özelliği için varsayılan değeri belirtir.

Örneğimizde

builder.Services.Configure<CookiePolicyOptions>(opts => {
    opts.CheckConsentNeeded = context => true;
});

koduyla her gelen istek için rıza gerektiğini belirtmekteyiz. Ayrıca kodumuza

app.UseCookiePolicy();

satırını ekleyerek cookie'ler için rıza kontrolü yapan middleware'i eklemiş bulunmaktayız. Sonuç olarak bu middleware yapılandırıldığı şekilde her gelen istekte rıza kontrolü yapmaktadır. Henüz rıza verilmediği için IsEssential olarak işaretlenmemiş cookie'lerin gönderilmesini engellemektedir. Programı çalıştırdıktan sonra ilk iş "clear" path'ına talepte bulunarak cookie'lerin silinmesini sağlayın. Programımızda counter1 cookie'si IsEssential olarak işaretlendiği, counter2 cookie'si IsEssential olarak işaretlenmediği için sadece counter1'in sayacı artacaktır.

[değiştir]

Programımızda mevcut durumda IsEssential olarak işaretlenmemiş cookie'ler kullanılamamaktadır. Eğer kullanıcı rıza vermişse esas olmayan cookie'lerin de kilidini açabilmeliyiz.

Rıza yönetimi bir request feature'ı aracılığıyla yapılmaktadır. Request feature'larına HttpContext sınıfının Features özelliği aracılığıyla erişilir. Request feature'ları nadir ihtiyaç duyulan alt seviye request-response işlemlerini bize sağlar. HttpContext.Features özelliğiyle erişilen her bir feature geriye bir arayüz nesnesi döndürür. Cookie rızası işlemleriyle ilgilenen arayüz ITrackingConsentFeature'dır. Bu arayüz aşağıdaki özellik ve metotları içerir:

CanTrack: Esas olmayan cookie'lere izin veriliyorsa true döndürür. İki şekilde true döndürür. Ya kullanıcı esas olmayan cookie'lere izin vermiştir ya da cookie rızası gerekmiyordur.
CreateConsentCookie(): Rıza cookie'si üretir. JavaScript istemcileri bu cookie'yi rıza belirtme amacıyla kullanabilir.
GrantConsent(): Esas olmayan cookie'lere izin verir. Bunu response'a bir cookie koyarak yapar.
HasConsent: Esas olmayan cookie'lere izin verilip verilmediği bilgisini tutar.
IsConsentNeeded: Geçerli request için esas olmayan cookie'ler için rıza gerekip gerekmediği bilgisini tutar.
WithdrawConsent(): Rızayı kaldırır. Bunu rıza cookie'sini silerek yapar.

Şimdi projemize ConsentMiddleware.cs isminde ve içeriği aşağıdaki gibi olan bir sınıf ekleyelim:

using Microsoft.AspNetCore.Http.Features;
public class ConsentMiddleware
{
    private RequestDelegate next;
    public ConsentMiddleware(RequestDelegate nextDelgate)
    {
        next = nextDelgate;
    }
    public async Task Invoke(HttpContext context)
    {
        if (context.Request.Path == "/consent")
        {
            ITrackingConsentFeature consentFeature = context.Features.Get<ITrackingConsentFeature>();
            if (consentFeature != null)
            {
                if (!consentFeature.HasConsent)
                {
                    consentFeature.GrantConsent();
                }
                else
                {
                    consentFeature.WithdrawConsent();
                }
                await context.Response.WriteAsync(consentFeature.HasConsent ? "Consent Granted \n" : "Consent Withdrawn\n");
            }
        }
        else
        {
            await next(context);
        }
    }
}

Bu middleware "/consent" path'ına gelen taleplere cevap verir. Kullanıcı cookie izni vermemiş gözüküyorsa cookie izni verilir, eğer kullanıcı izin vermiş gözüküyorsa izin kaldırılır. Diğer bir deyişle izin durumunu ters çevirir. Bu middleware'i request pipeline'a UseCookiePolicy() metot çağrısından hemen sonra ekleyin.

Programın çalışmasını görmek için öncelikle /clear path'ına talepte bulunarak cookie'leri temizleyin. Ardından /cookie path'ına talepte bulunun, sadece ilk sayacın ilerlediğini görün, daha sonra cookie'lere izin vermek için /consent path'ına talepte bulunun, bu aşamadan sonra tekrar /cookie path'ına talepte bulunduğunuzda iki sayacın da ilerlediğini göreceksiniz. Daha sonra tekrar /consent path'ına talepte bulunun, bu sefer ikinci sayacın durduğunu tekrar göreceksiniz.

Rızayı kaldırdıktan sonra cookie'nin değerinin sıfırlanmadığına dikkat edin. Rızayı kaldırmak mevcut cookie'yi silmez. Sadece sunucunun cookie'yi istemciye göndermesini engeller. İstemci yine kendindeki cookie'yi sunucuya göndermye devam eder. Ancak sunucu kendisine gelen cookie'nin değerini 1 artırıp tekrar istemciye gönderemediği için istemcideki cookie sabit kalır.