Mert Özen Her Satırda Daha İleri
Backend API Gateway ve YARP — #04

Gateway'de Cross-Cutting Concern'ler: Authentication, Authorization, Rate Limiting, Timeout ve Output Cache

Mert Özen 14 Kas 2026 14 dk 14 görüntülenme
Gateway'de Cross-Cutting Concern'ler: Authentication, Authorization, Rate Limiting, Timeout ve Output Cache

YARP route'a policy adını verir, işi ASP.NET Core middleware'leri yapar; bu yüzden middleware sırası ve fallback authorization policy gateway güvenliğinin temelidir. Bookstore.Gateway'de JWT doğrulamayı, kimliği backend'e token ve iç header ile taşımayı, kullanıcı başına rate limit ve 429 + Retry-After'ı, output cache'in token'lı istekleri neden cache'lemediğini ve request timeout'un YARP 2.3'te 504 yerine 400 döndürdüğünü ve bunun nasıl düzeltileceğini gerçek çıktılarla gösteriyoruz.

Sürüm notu: Bu yazıdaki kodlar .NET 10, Yarp.ReverseProxy 2.3 ve Microsoft.AspNetCore.Authentication.JwtBearer 10.0 ile derlenip denenmiştir.

Önce Tahmin Et

Bookstore.Gateway'de kitap okuma route'una 2 saniyelik bir request timeout policy'si bağladın ve timeout durumunda dönecek status code'u 504 olarak ayarladın. Catalog instance'larından biri 3 saniyede cevap veriyor. O instance'a düşen istemci 2 saniye sonra hangi status code'u görür? 504 diyorsan, YARP (Yet Another Reverse Proxy; .NET için reverse proxy library'si) ile .NET 10'un bu ikilisinde gerçek cevap farklı.

İkinci soru: app.UseOutputCache() satırını app.UseAuthorization()'dan önceye taşırsan ne olur? Derleme hatası yok, uygulama çalışıyor. Hangi route'ta kimin cevabının kime gidebileceğini düşün.

Route'a Policy Bağlamak ve Middleware Sırası

Gateway'i değerli yapan şey, sitedeki serilerin ortak örnek uygulaması Bookstore (bir online kitapçı) için kimlik, trafik limiti ve cache gibi her service'te tekrar yazılacak işleri tek yerde toplaması. YARP bunun için yeni bir mekanizma icat etmiyor: ASP.NET Core'un kendi middleware'lerini kullanıyor ve route config'inde yalnızca policy adını veriyorsun. YARP route'u endpoint olarak eklediğinde bu adları endpoint metadata'sına çeviriyor; middleware'ler de normal bir controller endpoint'ine nasıl davranıyorsa proxy route'una da öyle davranıyor.

"catalog-v1": {
  "ClusterId": "catalog",
  "AuthorizationPolicy": "Anonymous",
  "CorsPolicy": "bookstore-web",
  "TimeoutPolicy": "catalog-read",
  "OutputCachePolicy": "catalog-list",
  "Match": {
    "Path": "/api/v1/books/{**rest}"
  },
  "Transforms": [
    { "PathPattern": "/books/{**rest}" }
  ]
},
"orders-v1": {
  "ClusterId": "ordering",
  "AuthorizationPolicy": "customer",
  "CorsPolicy": "bookstore-web",
  "RateLimiterPolicy": "per-user-orders",
  "Match": {
    "Path": "/api/v1/orders/{**rest}"
  },
  "Transforms": [
    { "PathRemovePrefix": "/api/v1" }
  ]
}

Route property'lerinin bir kısmı sonradan geldi: release notlarına göre RateLimiterPolicy 2.0'da, Timeout ve TimeoutPolicy 2.1'de, OutputCachePolicy 2.2'de eklendi. Eski bir YARP sürümündeysen config'te bu alanları yazmak hiçbir şey yapmaz. Bizim tarafımızda önemli olan, middleware'lerin sırası:

var app = builder.Build();

// sıra önemli: CORS preflight'ı cevaplar, authentication rate limiter'ın partition'ı için
// User'ı doldurur, authorization cache'e ya da backend'e gitmeden önce reddeder
app.UseCors();
app.UseAuthentication();
app.UseRateLimiter();
app.UseAuthorization();
app.UseRequestTimeouts();
app.UseOutputCache();

app.MapGet("/gateway/ping", () => "pong").AllowAnonymous();

app.MapReverseProxy(proxyPipeline => { /* 01'deki pipeline */ });

Rate limiter'ı authentication'dan sonra ama authorization'dan önce koydum. Authentication'dan sonra olmalı, çünkü partition'ı kullanıcıya göre ayıracağız ve kullanıcıyı bilmesi gerekiyor. Authorization'dan önce olmasının sebebi de kimliksiz isteklerin de sayılması: aksi hâlde 401 alan bir saldırgan hiç limite takılmadan deneme yapabilir. Output cache ise authorization'dan sonra; "Önce Tahmin Et"in ikinci sorusunun cevabı burada. Output cache authorization'dan önce olsaydı, cache'te duran bir cevap authorization hiç çalışmadan dönebilirdi. Bizim kitap listesi gibi anonim bir route'ta fark etmez, ama bir gün biri kimlik isteyen bir route'a cache policy'si eklediğinde bu sıra o cevabı kimliksiz istemciye de verir. Middleware sırasının mantığını ASP.NET Core Web API serisinin Middleware Pipeline yazısında anlatmıştık; burada aynı kural trafik güvenliğine dönüşüyor.

Edge'de JWT Doğrulama

Gateway, JWT (JSON Web Token; imzalı kimlik bilgisi taşıyan token) doğrulamasını normal bir ASP.NET Core uygulaması gibi yapıyor. Token üretmeyi ve doğrulamayı ASP.NET Core Web API serisinin JWT yazısında, gerçek bir identity provider'dan gelen access token'ı doğrulamayı da Authentication ve Authorization serisinin API'yi korumak yazısında anlattık. Burada geliştirme için sahte, simetrik bir anahtar kullanıyoruz; anahtar yalnız appsettings.Development.json'da duruyor ve adından da anlaşılacağı gibi sır değil. Token'ı repo'daki tools/Bookstore.DevToken üretiyor; aşağıdaki komutlar bash sözdiziminde, PowerShell'de $ADA = dotnet run --project tools/Bookstore.DevToken -- ada yazman yeterli.

public static IServiceCollection AddGatewayAuthentication(
    this IServiceCollection services, IConfiguration configuration)
{
    IConfigurationSection auth = configuration.GetSection("Auth");

    services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(options =>
        {
            options.MapInboundClaims = false;
            options.TokenValidationParameters = new TokenValidationParameters
            {
                ValidIssuer = auth["Issuer"],
                ValidAudience = auth["Audience"],
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(auth["SigningKey"]!)),
                NameClaimType = "name",
                RoleClaimType = "role",
            };
        });

    services.AddAuthorizationBuilder()
        // AuthorizationPolicy yazılmamış bir route kazara herkese açık olmasın
        .SetFallbackPolicy(new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build())
        .AddPolicy(CustomerPolicy, policy => policy.RequireAuthenticatedUser().RequireRole("Customer"));

    return services;
}

Route'larda AuthorizationPolicy değeri ya bir policy adı ya da YARP'ın iki özel değerinden biri: Default (ASP.NET Core'un varsayılan policy'si, yani kimlikli kullanıcı) ve Anonymous. Bu ikisinin dışında kalan, yani AuthorizationPolicy yazılmamış route'lar için fallback policy devreye giriyor. Varsayılanı null, yani yazmadığın route herkese açık. Biz fallback'i "kimlikli kullanıcı" yaptık ve herkese açık olması gereken Catalog route'larına açıkça Anonymous yazdık. 02'de veritabanından route okuyan bir provider yazmıştık; o tablodaki catalog-v2 satırında hiç policy yok. Fallback sayesinde:

anon v2: 401
token v2: 200

Operasyon ekibinin tabloya eklediği yeni bir route, kimse düşünmediği için internete açık hâle gelmiyor. Şimdi dört kullanıcı tipiyle siparişlere bakalım:

ADA=$(dotnet run --project tools/Bookstore.DevToken -- ada)
STAFF=$(dotnet run --project tools/Bookstore.DevToken -- grace StoreStaff)

curl -i http://localhost:5300/api/v1/orders
HTTP/1.1 401 Unauthorized
Content-Length: 0
WWW-Authenticate: Bearer

curl -H "Authorization: Bearer $ADA" http://localhost:5300/api/v1/orders
[{"id":1001,"customerId":"ada","customerEmail":"ada@example.com","total":605.50,"status":"Shipped"}]

curl -o /dev/null -w "%{http_code}\n" -H "Authorization: Bearer $ADA" http://localhost:5300/api/v1/orders/1002
404

curl -i -H "Authorization: Bearer $STAFF" http://localhost:5300/api/v1/orders
HTTP/1.1 403 Forbidden

Kimliksiz istek gateway'de 401 aldı ve backend'e hiç gitmedi. grace kimlikli ama Customer rolünde değil, o yüzden 403. Ada kendi siparişini görüyor; Alan'ın 1002 numaralı siparişine baktığında 404 alıyor. O 404'ü gateway değil Ordering veriyor, çünkü "bu sipariş bu kullanıcının mı" sorusu bir business kuralı ve gateway'in işi değil. İmzası bozulmuş bir token gönderdiğimde cevap şu oldu:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token", error_description="The signature key was not found"

Kimliği Backend'e Taşımak: Token Passthrough ve İç Header

Gateway kullanıcıyı doğruladı. Ordering bu kullanıcının kim olduğunu nereden bilecek? İki yol var ve Microservices serisinin Microservice'lerde Güvenlik yazısında ikisinin ödünleşimlerini konuştuk. Burada YARP tarafında nasıl göründüklerine bakıyoruz.

Birincisi token passthrough. YARP header'ları varsayılan olarak kopyaladığı için Authorization header'ı kendiliğinden backend'e gidiyor. Ordering aynı token'ı kendisi de doğruluyor:

// gateway token'ı zaten doğruladı, Ordering yine de kendisi doğrular
IConfigurationSection auth = builder.Configuration.GetSection("Auth");
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.MapInboundClaims = false;
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidIssuer = auth["Issuer"],
            ValidAudience = auth["Audience"],
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(auth["SigningKey"]!)),
            NameClaimType = "name",
            RoleClaimType = "role",
        };
    });

RouteGroupBuilder group = app.MapGroup("/orders").RequireAuthorization();

group.MapGet("/", (ClaimsPrincipal user) =>
    orders.Where(o => o.CustomerId == user.FindFirstValue("sub")));
curl -o /dev/null -w "%{http_code}\n" http://localhost:5302/orders
401

Ordering'e gateway'i atlayarak doğrudan gittiğimde de 401 alıyorum. Buna defense in depth deniyor: ağ yanlış yapılandırılır, bir iç servis Ordering'i doğrudan çağırır ya da biri port'u dışarı açarsa, Ordering yine de kimliksiz isteği kabul etmez. Bedeli, her service'in token doğrulama ayarını bilmesi; gerçek bir identity provider ile bu ayar zaten issuer adresinden ibaret.

İkincisi iç header. Gateway doğrulanmış token'dan kullanıcı ID'sini okuyup kendi koyduğu bir header'la backend'e iletiyor. 02'de X-Bookstore- önekli header'ları gelişte silen bir transform yazmıştık; şimdi o transform kullanıcı ID'sini ekliyor:

// yalnız authentication'dan sonra: değer istemciden değil, doğrulanmış token'dan gelir
string? userId = transformContext.HttpContext.User.FindFirstValue("sub");
if (userId is not null)
{
    transformContext.ProxyRequest.Headers.TryAddWithoutValidation(InternalHeaders.UserId, userId);
}

Ada'nın token'ıyla ama Alan gibi görünmeye çalışarak bir istek attım:

curl "http://localhost:5300/debug/catalog" -H "Authorization: Bearer $ADA" -H "X-Bookstore-User-Id: alan"
{
    "path": "/debug/request",
    "query": "",
    "host": "localhost:5300",
    "headers": {
        "X-Bookstore-Route": "catalog-debug",
        "X-Bookstore-User-Id": "ada",
        "X-Forwarded-For": "::1",
        "X-Forwarded-Host": "localhost:5300",
        "X-Forwarded-Proto": "http"
    }
}

İstemcinin yazdığı alan silindi, yerine token'daki ada geldi. İç header yaklaşımı backend'i basitleştiriyor ama tek bir şartla güvenli: backend'e gateway'den başka hiçbir yoldan ulaşılamaması ya da backend'in isteğin gateway'den geldiğini doğrulaması; örneğin TLS (Transport Layer Security; HTTPS'in altındaki şifreleme protokolü) bağlantısında iki tarafın da certificate ile kimliğini kanıtladığı mTLS ile. Bookstore'da Ordering token'ı kendisi doğruluyor; iç header'ı da log ve audit için ek bilgi olarak kullanıyoruz.

Rate Limiting'i Gateway'de Toplamak

Sipariş route'una kullanıcı başına bir limit koyuyoruz: 10 saniyede 5 istek. Kimliksiz istekler istemci IP'sine göre gruplanıyor ve daha sıkı bir limite tabi: 10 saniyede 2.

services.AddRateLimiter(options =>
{
    options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
    options.AddPolicy(PerUserOrders, context =>
    {
        // kimlikli kullanıcıya kendi bucket'ı; diğerleri istemci IP'sine göre gruplanır
        string? userId = context.User.FindFirstValue("sub");
        return userId is not null
            ? RateLimitPartition.GetFixedWindowLimiter($"user:{userId}", _ => new FixedWindowRateLimiterOptions
            {
                PermitLimit = 5,
                Window = TimeSpan.FromSeconds(10),
                QueueLimit = 0,
            })
            : RateLimitPartition.GetFixedWindowLimiter($"ip:{context.Connection.RemoteIpAddress}", _ => new FixedWindowRateLimiterOptions
            {
                PermitLimit = 2,
                Window = TimeSpan.FromSeconds(10),
                QueueLimit = 0,
            });
    });
    options.OnRejected = (context, _) =>
    {
        if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out TimeSpan retryAfter))
        {
            context.HttpContext.Response.Headers.RetryAfter =
                ((int)Math.Ceiling(retryAfter.TotalSeconds)).ToString(CultureInfo.InvariantCulture);
        }
        return ValueTask.CompletedTask;
    };
});
# Ada, 7 istek art arda
200
200
200
200
200
Retry-After: 10
429
Retry-After: 10
429

# aynı anda Alan
200

# token'sız, 4 istek
401 401 429 429

Ada beşinci istekten sonra 429 aldı ve Retry-After header'ı ne kadar beklemesi gerektiğini söylüyor; Alan aynı anda kendi bucket'ından etkilenmeden devam etti. Token'sız istekler önce 401, sonra 429 aldı: rate limiter authorization'dan önce çalıştığı için kimliksiz denemeler de sayılıyor. Algoritma seçimi (fixed window, sliding window, token bucket, concurrency), partition key'lerin tasarımı ve chained limiter'lar ASP.NET Core Güvenliği serisinin Rate Limiting yazısında ayrıntılı anlatıldı. Gateway açısından bilmen gereken en önemli şey şu: bu sayaçlar gateway instance'ının belleğinde duruyor. Üç gateway instance'ı varsa Ada fiilen 10 saniyede 15 istek atabilir. Tam tutarlılık gerekiyorsa paylaşılan bir store (Redis) ya da limiti instance sayısına bölmek gibi yaklaşımlar gerekiyor; o yazıda bu konu da var.

Partition key seçerken IP'ye dikkat et. Gateway bir load balancer'ın arkasındaysa RemoteIpAddress load balancer'ın adresidir ve bütün kimliksiz kullanıcılar tek bucket'a düşer. Önce ForwardedHeaders middleware'ini güvenilen proxy listesiyle ayarlamak gerekir; bunu Web Server'lar ve Hosting serisinin forwarded header yazısında anlattık.

Timeout: Route Bazında Süre Sınırı ve Beklenmeyen 400

03'te cluster'ın ActivityTimeout'unu gördük: backend'den veri gelmeden geçebilecek en uzun süre. Route bazında toplam süre sınırı için .NET 8 ile gelen request timeout middleware'ini kullanıyoruz:

services.AddRequestTimeouts(options =>
{
    options.AddPolicy(CatalogRead, new RequestTimeoutPolicy
    {
        Timeout = TimeSpan.FromSeconds(2),
        TimeoutStatusCode = StatusCodes.Status504GatewayTimeout,
    });
});

Route'ta TimeoutPolicy ile policy adı ya da Timeout ile doğrudan süre verilebiliyor; ikisini birden yazmak config'in reddedilmesine yol açıyor. catalog-2'yi 3 saniyelik gecikmeyle başlatıp kitap isteklerini attım:

200 0.001913s
200 0.000970s
400 2.014835s
400 2.003367s

"Önce Tahmin Et"in ilk sorusunun cevabı: 400 Bad Request. Timeout doğru zamanda, 2 saniyede devreye girdi, ama status code yanlış. Sebebi YARP'ın kaynak kodunda. Request timeout middleware'i süre dolunca HttpContext.RequestAborted token'ını iptal ediyor. YARP'ın forwarder'ı iptali gördüğünde bunu "istemci bağlantıyı kopardı" olarak yorumluyor, hatayı RequestCanceled olarak raporluyor ve 400 yazıyor. Exception fırlamadığı için timeout middleware'inin 504'ü hiç uygulanmıyor. Üstelik 2.2'den beri istemci iptalleri Debug seviyesinde loglandığı için logda da hiçbir şey görmüyorsun. İstemci tarafında 400 "senin isteğin bozuk" demek; retry yapmayan, hatayı kullanıcıya yansıtan bir istemci için yanlış bir mesaj.

Çözüm, YARP pipeline'ına forwarder'dan sonra durumu düzelten küçük bir adım eklemek:

public static IReverseProxyApplicationBuilder UseRequestTimeoutStatus(this IReverseProxyApplicationBuilder proxyPipeline)
{
    proxyPipeline.Use(async (context, next) =>
    {
        await next();

        bool timedOut = context.Features.Get<IHttpRequestTimeoutFeature>()?.RequestTimeoutToken.IsCancellationRequested == true;
        if (timedOut
            && context.GetForwarderErrorFeature()?.Error == ForwarderError.RequestCanceled
            && !context.Response.HasStarted)
        {
            context.Response.StatusCode = StatusCodes.Status504GatewayTimeout;
        }
    });
    return proxyPipeline;
}

Bu adımı 01'deki logging middleware'inin hemen arkasına, UseSessionAffinity()'den önce ekledim; log satırına da forwarder hata türünü koydum:

504 2.009918s
404 0.002555s
504 2.002800s

      Forwarded to catalog-2[localhost:5311], status 504, error RequestCanceled
      Forwarded to catalog-1[localhost:5301], status 404, error none
      Forwarded to catalog-2[localhost:5311], status 504, error RequestCanceled

Ayrımı IHttpRequestTimeoutFeature.RequestTimeoutToken yapıyor: gerçekten istemci gittiyse bu token iptal edilmemiş oluyor ve 400 kalıyor. İki not daha. Passive health check, istemci iptallerini destination hatası saymıyor; yani route timeout'u yavaş instance'ı devre dışı bırakmıyor, onu active check'e bırakıyor. Ve request timeout'ları debugger bağlıyken uygulanmıyor; ActivityTimeout ise her zaman uygulanıyor. Debug ederken "timeout çalışmıyor" ya da "debug sırasında bağlantı kopuyor" şikâyetlerinin çoğu bu farktan geliyor.

Output Cache ve CORS

Kitap listesi herkese aynı ve sık okunuyor; gateway'de 30 saniyelik bir output cache policy'si bağladık:

services.AddOutputCache(options =>
{
    options.AddPolicy(CatalogList, policy => policy.Expire(TimeSpan.FromSeconds(30)).Tag("catalog"));
});
# token'sız, 3 istek
X-Catalog-Instance: catalog-2
--
Age: 0
X-Catalog-Instance: catalog-2
--
Age: 0
X-Catalog-Instance: catalog-2

# Ada'nın token'ıyla, 2 istek
X-Catalog-Instance: catalog-2
--
X-Catalog-Instance: catalog-1

Token'sız üç istekten yalnız ilki Catalog'a gitti (logda tek Proxying to satırı); diğer ikisi gateway'in belleğinden, Age header'ıyla döndü. Token'lı istekler ise her seferinde backend'e gitti. Bu bir hata değil: ASP.NET Core output cache'in temel policy'si Authorization header'ı olan istekleri cache'ten servis etmiyor ve cache'e de yazmıyor. Kişiye özel bir cevabın başka birine gitmesini engelleyen varsayılan bu. Kendi policy'ni yazıp bu kontrolü kaldırırsan, örneğin "giriş yapmış kullanıcıya da kitap listesini cache'ten verelim" diye, cache key'ini kullanıcıya göre ayırmadığın her route'ta Ada'nın siparişleri Alan'a gidebilir. Vary-by, tag ile eviction ve Redis store gibi konular Caching serisinin Output Cache yazısında; gateway için kuralım basit: yalnız herkese aynı olan cevapları cache'le.

CORS (Cross-Origin Resource Sharing; tarayıcının başka origin'e yaptığı istekleri yöneten kurallar) için route'a CorsPolicy veriyoruz. Policy, Angular geliştirme sunucusunun adresine izin veriyor:

curl -i -X OPTIONS http://localhost:5300/api/v1/orders -H "Origin: http://localhost:4200" \
  -H "Access-Control-Request-Method: GET" -H "Access-Control-Request-Headers: authorization"
HTTP/1.1 204 No Content
Access-Control-Allow-Headers: Authorization,Content-Type
Access-Control-Allow-Methods: GET,POST
Access-Control-Allow-Origin: http://localhost:4200

curl -i -X OPTIONS http://localhost:5300/api/v1/orders -H "Origin: http://evil.example" \
  -H "Access-Control-Request-Method: GET"
HTTP/1.1 204 No Content

Preflight'ı gateway cevapladı; istek ne authorization'a takıldı ne de Ordering'e gitti. İzinli olmayan origin de 204 aldı ama Access-Control-Allow-* header'ları yok, tarayıcı asıl isteği göndermeyecek. 05'te Angular'ı gateway ile aynı origin'den sunduğumuzda CORS'a hiç gerek kalmayacak; BFF'nin (Backend for Frontend; belirli bir istemci için yazılmış ara backend) avantajlarından biri bu.

WebSocket, SignalR ve gRPC'yi Proxy'lemek

YARP WebSocket bağlantılarını varsayılan olarak proxy'liyor; ek bir route ayarı gerekmiyor. HTTP/1.1'de bağlantı 101 Switching Protocols ile yükseltiliyor ve YARP sonrasında iki yönlü bir akış taşıyor. 2.0'dan beri HTTP/2 üzerinden WebSocket de destekleniyor ve gelen ile giden tarafın HTTP sürümü farklı olabiliyor. Bu yazıda WebSocket'i ve gRPC'yi gerçekten çalıştırmadım; aşağıdakiler YARP dokümantasyonundan ve 03'teki HTTP/2 denememizden.

Uzun bağlantılarda timeout'lar farklı davranıyor. Request timeout'ları WebSocket handshake'inden sonra devre dışı kalıyor, ama cluster'ın ActivityTimeout'u bağlantı açık kaldığı sürece geçerli: iki yönde de veri ya da keep-alive frame'i geçmezse YARP bağlantıyı kapatıyor. Dokümantasyonun altını çizdiği tuzak şu: tarayıcılar kendiliğinden keep-alive göndermiyor, ASP.NET Core'un WebSocketOptions.KeepAliveInterval varsayılanı 2 dakika, YARP'ın ActivityTimeout varsayılanı 100 saniye. Yani hiçbir ayara dokunmazsan sessiz kalan bir WebSocket bağlantısı 100 saniyede kopar. Ya backend'in keep-alive aralığını kısalt ya da o cluster'ın ActivityTimeout'unu artır. SignalR'ın kendi keep-alive'ı varsayılan olarak 15 saniyede bir gönderildiği için SignalR'da bu sorun genelde görülmez; long polling transport'u ise 03'teki session affinity gereksinimini getirir.

gRPC tamamen HTTP/2'ye dayanıyor ve request timeout'ları gRPC isteklerine de uygulanıyor. 03'te gördüğümüz gibi şifresiz bağlantıda HTTP/2 her iki tarafın açık ayarını gerektiriyor: gateway'in gelen tarafında Kestrel endpoint'i Http2 ile ya da HTTPS üzerinden dinlemeli; backend'e giden tarafta cluster'a "Version": "2" ve uygun VersionPolicy verilmeli. Streaming gRPC çağrıları için de route timeout'u ve ActivityTimeout'u akışın gerçek süresine göre ayarla, yoksa uzun bir server streaming çağrısı ortasında kesilir.

Üretimde Dikkat

Gateway'deki her policy'nin bir sahibi ve ölçümü olsun. 401, 403 ve 429 oranlarını route bazında izle; ani bir 401 artışı süresi dolmuş bir signing key'i ya da yanlış ayarlanmış bir issuer'ı, ani bir 429 artışı da yanlış bir partition key'ini (örneğin load balancer IP'si) gösterir.

Gateway'de JWT doğrularken signing key'i dosyada tutma. Gerçek bir identity provider ile çalışırken anahtarlar Authority adresinden (OpenID Connect discovery) otomatik alınır ve rotate edilir; bu yazıdaki simetrik anahtar yalnız geliştirme içindir.

Gateway'e eklediğin her middleware her isteğin gecikmesine eklenir. Authentication ve rate limiter genellikle mikrosaniyeler düzeyinde; asıl maliyet, gateway'in cevabı bekleyen bağlantıları ve output cache'in bellek tüketimi. Output cache varsayılan olarak instance belleğinde; çok instance'lı bir gateway'de her instance kendi kopyasını tutar ve tag ile eviction yalnız o instance'ı temizler.

Sık Düşülen Tuzaklar

Birincisi, business kuralını gateway'e koymak. "Kurumsal müşteriler en az 20 adet sipariş verebilir" ya da "bu sipariş bu kullanıcının mı" gateway'in bilmesi gereken şeyler değil. Gateway'in policy'leri kimlik, rol ve trafik düzeyinde kalmalı. Her yeni business kuralı gateway deploy'u gerektirmeye başladıysa, Microservices serisinin API Gateway yazısında anlattığımız "god gateway"e doğru gidiyorsun demektir.

İkincisi, kişiye özel cevabı cache'lemek. Output cache'in temel policy'sini devre dışı bırakan özel bir policy, cache key'i kullanıcıya göre ayrılmadıkça bir kullanıcının verisini diğerine gösterir.

Üçüncüsü, iç header'a güvenip backend'i doğrudan erişilebilir bırakmak. X-Bookstore-User-Id yalnız isteğin gerçekten gateway'den geldiği garanti edildiğinde anlamlı.

Dördüncüsü, timeout'u ayarlayıp gerçek status code'u test etmemek. Yukarıda gördüğün gibi config doğru olsa bile istemci 504 yerine 400 alabiliyor.

Beşincisi, fallback policy olmadan çalışmak. Yeni bir route'a AuthorizationPolicy yazmayı unutmak, özellikle route'lar veritabanından ya da başka bir ekipten geliyorsa, o route'u sessizce herkese açar.

Küçük Bir Deneme

★☆☆ app.UseRateLimiter() satırını app.UseAuthorization()'dan sonraya taşı ve token'sız olarak sipariş route'una dört istek at. Status code'lar nasıl değişti? Hangi sıranın hangi saldırıya karşı daha iyi olduğunu kendi cümlelerinle yaz.

★★☆ Sipariş route'u için RequireRole("Customer") yerine bir scope kontrolü yapan orders.read policy'si yaz (token'a scope claim'i ekleyecek şekilde DevToken aracını da güncelle). Scope'suz bir token'la 403 aldığını, scope'lu token'la 200 aldığını doğrula.

★★★ Kitap listesini giriş yapmış kullanıcılar için de cache'lemek istiyorsun, ama güvenli şekilde. Authorization header'ı olan isteklerin de cache'lenmesine izin veren bir output cache policy'si yaz; yalnız /api/v1/books route'una bağla ve cevabın kullanıcıya göre değişmediğini kanıtlamak için Catalog'un o endpoint'te hiçbir kimlik bilgisi okumadığını kodla göster. Sonra aynı policy'yi yanlışlıkla sipariş route'una bağlayıp Ada ile Alan'ın birbirinin cevabını görüp görmediğini test et ve sonucu not et.

Gateway artık kimliği doğruluyor, trafiği sınırlıyor, süreyi kesiyor ve ortak cevapları cache'liyor. Bir sonraki yazıda aynı uygulamanın içine kendi endpoint'lerimizi koyacağız: Angular istemcisi için bir BFF, kitap detay ekranı için Catalog, Inventory ve Ordering'i paralel çağıran bir aggregation endpoint'i ve kısmi hata yönetimi.