ASP.NET Middleware'leri Anlamak
Selamlar, bu yazıda middleware'leri anlatmak istiyorum. Şimdi size sırayla neler anlatacağımı yazayım:
- İlk önce HTTP nasıl çalışır, onun akışını kabaca anlatacağım.
- İstek geldiğinde ASP.NET nasıl hareket eder, ondan bahsedeceğim.
- İsteğiniz controller'a veya ilgili route'a nasıl ulaşır, ondan bahsedeceğim.
- Makaleyi bitireceğim.
Aslında burada yazmış olduğum maddeli yazı middleware'in çalışma mantığını gösteriyor. Middleware, client (kullanıcı/istemci) tarafından gelen isteklerin hangi sırayla işleneceğini belirten yapıya verilen isim. Sadece ASP.NET içerisinde değil, bütün web API programlama dillerinde ilgili middleware yapısına sahibiz. Hatta kullanıcının authenticate veya authorize olup olmadığı bilgisi de middleware'ler ile sağlanıyor. Daha da ileri gidecek olursak idempotent check yapabiliriz middleware içerisinde (benim tercihim olmaz).
Middleware Nedir?
.NET içerisinde mevcutta bazı middleware'ler bulunuyor zaten ve bunları kendimiz de ekliyoruz. app.UseAuthentication() dediğimizde aslında arka tarafta bir middleware ekliyoruz:
public static IApplicationBuilder UseAuthentication(this IApplicationBuilder app)
{
ArgumentNullException.ThrowIfNull(app);
app.Properties[AuthenticationMiddlewareSetKey] = true;
return app.UseMiddleware<AuthenticationMiddleware>();
}
Yukarıdaki gördüğünüz örnekte bir middleware örneği mevcut. Gördüğünüz gibi aslında UseAuthentication methodu araya bir AuthenticationMiddleware ekliyor.
Middleware'ler Nasıl Çalışır?
Middleware'ler aşama aşama ilerliyor ve her birisi eline gelen HttpRequest'i bozmayacak hareketler yaparak ilgili işlemini tamamlayıp devam ediyor.
Eğer authenticate middleware'in içeriğine bakarsak göreceğimiz şey aslında Invoke methodudur. Invoke methodu her middleware'de zorunlu olarak olması gereken bir methoddur. Bu methodun imzasına bakarak aslında bir listeye ekleme yapıyoruz. Her app.UseMiddleware dediğimizde bir listeye ekleme yapmış oluyoruz.
Eğer kodları inspect ile en detayına kadar incelerseniz veya dotnet'in kendi reposu üzerinde incelerseniz Use middleware sizi şuraya yönlendiriyor:
public IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware)
{
_components.Add(middleware);
_descriptions?.Add(CreateMiddlewareDescription(middleware));
return this;
}
Tabi buradaki koda direkt baktığınızda _components herhangi bir şey olabilir diyebilirsiniz, şu an anonymous duruyor. Ancak tanıma baktığınızda sonuç nettir:
private readonly List<Func<RequestDelegate, RequestDelegate>> _components = new();
Ve bu listenin sırası önemlidir. Siz .NET içerisinde middleware listesine ekleme yaptığınız sıralama geçerli olacaktır. Yani çalışırken de sizin bu eklediğiniz sıraya göre çalışacaktır.
Sıralama Neden Önemli?
Bu niye önemli? Bildiğiniz gibi REST üzerinde belli başlı hiyerarşiler vardı, authentication ve authorization bunlardan birisi. Authentication sizin kim olduğunuzu tanımlar, adınızı soyadınızı söyler. Authorization ise sizin bir işlemi yapmaya yetkinizin olup olmadığını söyler.
Bu senaryoda siz middleware ekleme sıranızı şu şekilde yaparsanız:
app.UseAuthorization();
app.UseAuthentication();
Yüksek ihtimalle yetki gerektiren endpoint'lerinizin tamamında hata alacaksınız. Çünkü kullanıcının kim olduğunu bilmeden yetkisini sorgulamak istiyor olacaksınız (authorization önce eklendiği için).
Eğer authorization middleware'in içeriğini inceleyecek olursanız göreceğiniz kod:
var authenticateResult = await policyEvaluator.AuthenticateAsync(policy, context);
if (authenticateResult?.Succeeded ?? false)
{
if (context.Features.Get<IAuthenticateResultFeature>() is IAuthenticateResultFeature authenticateResultFeature)
{
authenticateResultFeature.AuthenticateResult = authenticateResult;
}
else
{
var authFeatures = new AuthenticationFeatures(authenticateResult);
context.Features.Set<IHttpAuthenticationFeature>(authFeatures);
context.Features.Set<IAuthenticateResultFeature>(authFeatures);
}
}
Burada dikkat ederseniz önce authenticate ediyor, buranın hemen ardından gelen kodlarda ise authorize işlemi oluyor.
Aslında authentication middleware kullanıcının kim olduğunu tespit etmekten ziyade, authorize ve diğer kullanım senaryoları için de ilgili kullanıcının kimliğini doğruluyor gibi düşünebilirsiniz. Çoğu kullanıcı kendi kimliğini bize bildirir (JWT, cookie veya diğer yöntemlerle).
Bu kimlik bildirimi çeşitli onaylardan (doğrulama aşamalarından) geçtikten sonra kullanıcının bilgileri kayıt edilir ve middleware _next diyerek sonraki aşamaya geçer:
var defaultAuthenticate = await Schemes.GetDefaultAuthenticateSchemeAsync();
if (defaultAuthenticate != null)
{
var result = await context.AuthenticateAsync(defaultAuthenticate.Name);
if (result?.Principal != null)
{
context.User = result.Principal;
}
if (result?.Succeeded ?? false)
{
var authFeatures = new AuthenticationFeatures(result);
context.Features.Set<IHttpAuthenticationFeature>(authFeatures);
context.Features.Set<IAuthenticateResultFeature>(authFeatures);
}
}
await _next(context);
Middleware Pipeline (Sıralama)
Aşağıda tipik bir ASP.NET Core middleware pipeline'ının çalışma sırasını görüyorsunuz. Her middleware bir öncekinden HttpContext'i alır, kendi işini yapar ve _next() ile bir sonrakine iletir. Response dönüşünde ise ters sırada ilerlenir.
flowchart TB C[Client / Istemci] C -->|request| M1[1 - UseExceptionHandler] M1 -->|request| M2[2 - UseHsts] M2 -->|request| M3[3 - UseHttpsRedirection] M3 -->|request| M4[4 - UseStaticFiles] M4 -->|request| M5[5 - UseRouting] M5 -->|request| M6[6 - UseCors] M6 -->|request| M7[7 - UseAuthentication] M7 -->|request| M8[8 - UseAuthorization] M8 -->|request| EP[Controller] EP -.->|response| M8 M8 -.->|response| M7 M7 -.->|response| M6 M6 -.->|response| M5 M5 -.->|response| M4 M4 -.->|response| M3 M3 -.->|response| M2 M2 -.->|response| M1 M1 -.->|response| C
Bu sıralama kritiktir. Birkaç önemli nokta:
- Routing yetkilendirmeden önce gelmelidir ki hangi endpoint'in hedeflendiği bilinsin.
- CORS, routing'den sonra ama authentication'dan önce gelmelidir.
- Authentication, authorization'dan önce gelmek zorundadır.
- Static files, authentication'dan önce gelmelidir ki giriş yapmadan statik dosyalara erişilebilsin.
Gerçek Dünya Senaryoları
Diğer programlama dillerinde middleware'lerin çoğunu kendiniz yazmak durumundasınız. Ancak ASP.NET framework'ü oldukça zengin ve yaşlı olduğu için çoğu tekrar eden durumu kendi bünyesine dahil ediyor. Bunların içerisinde authentication, authorization ve rate limit bile var.
Ve bunların sıralaması gerçekten çok önemlidir. Örneğin rate limit için eğer kullanıcı bazlı bir rate limit koymak istiyorsanız, ilgili rate limit middleware'inizi UseAuthentication middleware'inden sonra koymanız gerekmektedir, çünkü öncesi senaryolarda geçersiz olacaktır.
Aynı zamanda siteniz için static file'lar barındıracaksanız, bunları da UseAuthentication middleware'inden önce koymanız gerekir. Bu sayede giriş yapmadan kullanıcılar static file'lara erişebilir (.css, .js gibi dosyalardan bahsediyorum).
Kaynaklar
- ASP.NET Core Middleware — Microsoft Docs
- dotnet/aspnetcore — GitHub
ApplicationBuilder.cs— middleware'lerin_componentslistesine eklendiği sınıfAuthenticationMiddleware.cs—UseAuthentication'ın arka planda eklediği middlewareAuthorizationMiddleware.cs—UseAuthorizationmiddleware implementasyonu
- dotnet/runtime — GitHub — .NET runtime