Musa Yılmaz
·11 dk okuma

Server Component'te Django Oturumu Neden Boş Geliyor?

nextjsdjangoauthenticationserver-components

Next.js ve Django'yu nginx arkasında aynı origin'de çalıştırdığınızda oturum işi kendiliğinden yürüyor gibi görünür. Tarayıcı /api/auth/login/ adresine POST atar, Django sessionid çerezini bırakır, sonraki her istekte bu çerez otomatik gider. CORS yok, token saklama yok, localStorage yok. Bu kurulumu tek origin mimarisi yazısında anlatmıştım.

Sonra sayfadaki yükleniyor animasyonundan kurtulmak istersiniz ve kullanıcı bilgisini çeken fetch çağrısını Client Component'ten Server Component'e taşırsınız. Kod neredeyse aynıdır. Ama sayfa artık giriş yapmış kullanıcıya bile "misafir" der.

Bu yazı o anın neden yaşandığını ve düzgün çözümünü anlatıyor. Örnekler Next.js 16 ve App Router içindir; 16 ile birlikte gelen bir isim değişikliği de var, ona da geleceğiz.

Tarayıcıda çalışan şey sunucuda neden çalışmıyor?

Çerez tarayıcıya ait bir durumdur. sessionid çerezi kullanıcının tarayıcısında durur ve tarayıcı onu yalnızca kendi yaptığı isteklere ekler.

Server Component ise tarayıcıda çalışmaz. Sunucudaki Node.js süreci içinde çalışır ve oradan attığınız fetch çağrısı Django açısından bambaşka bir istemcidir: yeni bir TCP bağlantısı, boş bir çerez kavanozu. Django'nun gördüğü şey kimliksiz bir istektir, request.user da AnonymousUser olur.

Yani hata mesajı vermez, yetki hatası da vermez. Sadece kullanıcıyı tanımaz. Bu yüzden kafa karıştırıcıdır: aynı endpoint tarayıcının ağ sekmesinde 200 ve dolu bir gövde dönerken, sunucudan çağrıldığında 200 ve boş bir gövde döner.

Django REST Framework kullanıyorsanız ve endpoint IsAuthenticated ile korunuyorsa daha net bir sinyal alırsınız. DRF dokümanı bunu açıkça yazıyor: SessionAuthentication ile "kimliği doğrulanmamış ve izin reddedilen istekler HTTP 403 Forbidden yanıtı üretir". Yani token tabanlı kurulumlarda beklediğiniz 401 yerine 403 görürsünüz — çünkü DRF 401 yanıtına WWW-Authenticate başlığı eklemek zorundadır ve oturum tabanlı kimlik doğrulamada gönderecek bir şema yoktur.

Çerezi elle taşımak

Çözüm karmaşık değil: gelen isteğin çerezini okuyup giden isteğe elle koyacaksınız. Next.js'te gelen isteğin çerezlerine next/headers içindeki cookies ile ulaşılır.

Buradaki ilk tuzak sürüm farkı. cookies artık asenkron bir fonksiyon; dönen değeri await etmeden kullanamazsınız. Next.js 14 ve öncesinde senkrondu, 15'te geriye dönük uyumluluk için senkron erişim bir süre daha çalıştı, ama artık beklenen kullanım bu:

// src/lib/api.ts
import { cookies } from 'next/headers'
 
const TASINACAK_CEREZLER = ['sessionid', 'csrftoken']
 
export async function apiFetch(path: string, init: RequestInit = {}) {
  const store = await cookies()
 
  const cookieHeader = TASINACAK_CEREZLER.flatMap((name) => {
    const cookie = store.get(name)
    return cookie ? [`${cookie.name}=${cookie.value}`] : []
  }).join('; ')
 
  return fetch(`${process.env.API_INTERNAL_URL}${path}`, {
    ...init,
    headers: {
      ...init.headers,
      ...(cookieHeader ? { cookie: cookieHeader } : {}),
    },
  })
}

API_INTERNAL_URL Docker ağındaki servis adresidir, örneğin http://backend:8000. Bu değişkende NEXT_PUBLIC_ öneki kullanmayın: bu adres tarayıcıya gitmemeli, çünkü tarayıcı iç ağdaki servis adını zaten çözemez.

Artık her Server Component tek bir yerden konuşur:

// app/[locale]/panel/page.tsx
import { apiFetch } from '@/lib/api'
 
export default async function PanelPage() {
  const res = await apiFetch('/api/me/')
  const user = await res.json()
 
  return <h1>Merhaba {user.username}</h1>
}

Bu fonksiyonu bir kere yazıp her yerde kullanmanın asıl faydası tutarlılık: çerez taşımayı unuttuğunuz tek bir çağrı, sayfanın yarısının kullanıcıyı tanımamasına yol açar ve bunu ancak canlıda fark edersiniz.

Hangi çerezleri taşımalısınız?

Kolay yol store.toString() çağırıp gelen bütün çerezleri backend'e yollamaktır. Çalışır, ama gereğinden fazlasını yapar.

Tarayıcıda tema tercihi, dil seçimi, analitik kimliği, üçüncü taraf araçların bıraktığı çerezler birikir. Hepsini Django'ya göndermek, backend'in loglarına ve hata raporlarına hiç ihtiyaç duymadığı veriyi taşır. Yukarıdaki listede olduğu gibi ihtiyacınız olan iki çerezi adıyla seçmek hem daha küçük bir istek başlığı hem de daha dar bir veri yüzeyi demek.

İki çerezin rolleri de farklı. Django'nun kendi dokümanı SESSION_COOKIE_HTTPONLY ayarının "JavaScript'ten erişimi engellemek için True bırakılmasını" öneriyor — varsayılan da budur. csrftoken ise tam tersine varsayılan olarak JavaScript'e açıktır (CSRF_COOKIE_HTTPONLY varsayılanı False), çünkü istemcinin onu okuyup bir başlığa koyabilmesi gerekir.

Oturum açmayı sunucuya taşımayın

Mantıklı bir sonraki adım gibi görünür: giriş formunu da Server Action'a taşıyıp Django'ya oradan POST atmak. Bu noktada işler zorlaşır.

Next.js dokümanı Server Component render'ı sırasında çerez yazılamayacağını söylüyor; set ve delete yalnızca Server Function veya Route Handler içinde çalışır. Bunun sebebi HTTP: yanıt akmaya başladıktan sonra çerez atanamaz.

Asıl sorun bu da değil. Giriş isteğini sunucudan atarsanız Django Set-Cookie başlığını Next.js sunucusuna döner, tarayıcıya değil. O çerezi kullanıcıya ulaştırmak için Django'nun yanıtındaki Set-Cookie başlıklarını ayrıştırıp cookies().set() ile yeniden kurmanız gerekir — Max-Age, Path, SameSite, Secure niteliklerini elle taşıyarak. Bir niteliği atlarsanız oturum tarayıcıda sessizce farklı davranır.

Tek origin kurulumunda buna hiç gerek yok. Giriş isteğini tarayıcıdan atın:

// src/app/[locale]/giris/form.tsx (Client Component)
const res = await fetch('/api/auth/login/', {
  method: 'POST',
  headers: {
    'content-type': 'application/json',
    'x-csrftoken': csrfToken,
  },
  body: JSON.stringify({ username, password }),
})

İstek aynı origin'e gittiği için credentials ayarı yapmanıza gerek yok, fetch varsayılanı zaten aynı origin'e çerez gönderir. Django Set-Cookie başlığını doğrudan tarayıcıya döner, çerez doğru niteliklerle kurulur, ortada taşınacak bir şey kalmaz. Sonrasında sayfayı tazelediğinizde Server Component'ler apiFetch üzerinden kullanıcıyı görür.

Tek origin mimarisinin en somut kazancı burada ortaya çıkıyor: kimlik doğrulamanın en kırılgan adımını hiç yazmıyorsunuz.

Mutasyonlarda CSRF nasıl geçilir?

Okuma işleri çerezi taşımakla hallolur. Yazma işlerinde Django bir de CSRF token ister.

Django dokümanı beklentiyi net koyuyor: her güvensiz istekte "CSRF_HEADER_NAME ayarında belirtildiği gibi özel bir X-CSRFToken başlığı" token değeriyle gönderilmeli. CSRF_HEADER_NAME varsayılanı HTTP_X_CSRFTOKEN, yani başlık adı X-CSRFToken. Token'ın önerilen kaynağı da csrftoken çerezi.

Server Action'da ikisini birlikte yapmanız gerekiyor — çerezi taşımak ve aynı değeri başlığa koymak:

// src/app/[locale]/panel/actions.ts
'use server'
 
import { cookies } from 'next/headers'
import { revalidatePath } from 'next/cache'
import { apiFetch } from '@/lib/api'
 
export async function profilGuncelle(formData: FormData) {
  const store = await cookies()
  const csrfToken = store.get('csrftoken')?.value
 
  if (!csrfToken) {
    return { hata: 'Oturum bulunamadı, tekrar giriş yapın.' }
  }
 
  const res = await apiFetch('/api/profile/', {
    method: 'PATCH',
    headers: {
      'content-type': 'application/json',
      'x-csrftoken': csrfToken,
    },
    body: JSON.stringify({ isim: formData.get('isim') }),
  })
 
  if (!res.ok) {
    return { hata: 'Güncelleme başarısız.' }
  }
 
  revalidatePath('/panel')
  return { tamam: true }
}

Token'ın çerezden gelmesi ve aynı zamanda çerez olarak da gitmesi tuhaf görünebilir ama korumanın çalışma biçimi budur: saldırgan bir site kurbanın tarayıcısına çerez gönderttirebilir, fakat o çerezi okuyup başlığa koyamaz. Django ikisinin eşleştiğini görmek ister.

X-CSRFToken başlığını hiç göndermezseniz Django isteği reddeder ve oturum geçerli olsa bile 403 alırsınız. apiFetch çerezi taşıdığı hâlde 403 görüyorsanız sorun oturumda değil, bu adımdadır.

HTTPS üzerinden çağırıyorsanız Origin'e dikkat

Django'nun CSRF kontrolünün iki ayrı dalı var ve hangisine düştüğünüz backend'e nasıl ulaştığınıza bağlı.

Dokümandan birebir: "CsrfViewMiddleware, tarayıcı tarafından sağlanmışsa Origin başlığını mevcut host'a ve CSRF_TRUSTED_ORIGINS ayarına karşı doğrular." Devamı ise şu: "Ek olarak, HTTPS istekleri için Origin başlığı sağlanmamışsa CsrfViewMiddleware katı referer kontrolü yapar." HTTP istekleri için referer kontrolü yapılmaz, çünkü doküman bu başlığı HTTP altında yeterince güvenilir bulmuyor.

Pratikte bunun anlamı şu:

  • İç ağ üzerinden çağırıyorsanız (http://backend:8000) istek düz HTTP'dir ve Origin başlığı yoktur. Referer kontrolü devreye girmez, yalnızca token karşılaştırılır. Ek ayar gerekmez — önerdiğim yol budur.
  • Genel HTTPS adresi üzerinden çağırıyorsanız Origin başlığı yine yoktur (sunucudan atılan fetch bunu kendiliğinden eklemez), ama istek HTTPS olduğu için katı referer kontrolü devreye girer ve referer da olmadığı için istek reddedilir.

İkinci duruma düşmek zorundaysanız Origin başlığını elle ekleyin ve o origin'i Django tarafında güvenilir listeye koyun:

# settings.py
CSRF_TRUSTED_ORIGINS = ["https://example.com"]

Bu ayar şemayı da içermek zorundadır; sadece alan adı yazmak yetmez. Ama asıl tavsiyem iç ağ adresini kullanmak: hem bir ağ atlaması eksilir, hem de bu kontrolle hiç uğraşmazsınız.

proxy.ts ile korumak yeterli mi?

Next.js 16 ile birlikte middleware.ts dosyasının adı proxy.ts oldu, dışa aktarılan fonksiyon da middleware yerine proxy. Doküman değişikliği şöyle özetliyor: "Next.js 16'dan itibaren Middleware, amacını daha iyi yansıtması için Proxy olarak adlandırılıyor. İşlevsellik aynı kalıyor." Sürüm 16'dan itibaren varsayılan çalışma ortamı da Node.js runtime; runtime ayarını burada kullanmak hata fırlatıyor.

Korumalı rotalar için ilk akla gelen yer burasıdır:

// src/proxy.ts
import { NextResponse, type NextRequest } from 'next/server'
 
const KORUMALI_YOLLAR = ['/panel']
 
export function proxy(request: NextRequest) {
  const path = request.nextUrl.pathname
 
  if (!KORUMALI_YOLLAR.some((yol) => path.startsWith(yol))) {
    return NextResponse.next()
  }
 
  if (!request.cookies.get('sessionid')) {
    return NextResponse.redirect(new URL('/giris', request.url))
  }
 
  return NextResponse.next()
}
 
export const config = {
  matcher: ['/((?!api|_next/static|_next/image|.*\\..*).*)'],
}

Bu kod işe yarar ama tek başına bir yetkilendirme katmanı değildir ve öyle davranmayın. Next.js dokümanı bu konuda alışılmadık biçimde doğrudan: Proxy "tam bir oturum yönetimi veya yetkilendirme çözümü olarak kullanılmamalı", yalnızca "iyimser kontroller" için uygundur.

Sebebi de yazılı: Proxy her rotada, önden getirilen (prefetch) rotalar dahil çalışır. Yani kullanıcı bağlantının üzerine geldiğinde bile tetiklenir. Buraya bir veritabanı sorgusu veya backend çağrısı koyarsanız, kullanıcı hiç tıklamadan backend'inize istek yağar. Doküman bu yüzden "yalnızca çerezden oturumu okuyun, veritabanı kontrollerinden kaçının" diyor.

Yukarıdaki kodun yaptığı tam olarak bu: çerezin varlığına bakıyor, geçerliliğine değil. Süresi dolmuş, iptal edilmiş, hatta uydurulmuş bir sessionid bu kontrolü geçer. Buradaki kazanç güvenlik değil kullanıcı deneyimidir — giriş yapmamış kullanıcı boş bir panel görüp sonra yönlendirilmek yerine doğrudan giriş sayfasına gider.

Bir not daha: Proxy içinde fetch çağrısına verdiğiniz cache, next.revalidate ve next.tags seçeneklerinin hiçbir etkisi yoktur. Doküman bunu ayrıca belirtiyor.

Kontrolü veriye yaklaştırın

Gerçek kontrol veriye en yakın yerde yapılmalı. Next.js dokümanının önerdiği kalıp, veri isteklerini ve yetki mantığını tek bir katmanda toplamak ve React'in cache fonksiyonuyla render başına bir kez çalışmasını sağlamak:

// src/lib/dal.ts
import { cache } from 'react'
import { redirect } from 'next/navigation'
import { apiFetch } from './api'
 
export const getUser = cache(async () => {
  const res = await apiFetch('/api/me/')
 
  if (res.status === 401 || res.status === 403) {
    return null
  }
 
  if (!res.ok) {
    throw new Error(`Kullanıcı bilgisi alınamadı: ${res.status}`)
  }
 
  return res.json()
})
 
export async function requireUser() {
  const user = await getUser()
 
  if (!user) {
    redirect('/giris')
  }
 
  return user
}

cache sarmalayıcısı olmadan aynı sayfadaki beş ayrı bileşen getUser çağırdığında Django'ya beş istek gider. cache ile tek render geçişinde bir kez gider.

Bunu Server Action'larda da çağırın. Doküman Server Action'ları "herkese açık API uç noktalarıyla aynı güvenlik hassasiyetiyle" ele almayı söylüyor — çünkü gerçekten öyledirler; istemciden çağrılabilen uç noktalardır ve formun ekranda görünmüyor olması kimseyi engellemez.

Layout'ta oturum kontrolü yapmayın

Sık yapılan bir hata, kontrolü korumalı bölümün layout.tsx dosyasına koymaktır. Bir kez yazıp altındaki her sayfayı kapsıyormuş gibi görünür.

Görünmüyor. Next.js dokümanı sebebini açıklıyor: kısmi render (Partial Rendering) nedeniyle layout'lar gezinme sırasında yeniden render edilmez, dolayısıyla oturum her rota değişiminde kontrol edilmez. Kullanıcı panele girdikten sonra oturumu sona erse bile, layout'taki kontrol yeniden çalışmadığı için alt sayfalar arasında gezinmeye devam edebilir.

Doğrusu, kontrolü veri kaynağına ya da koşullu render edilen bileşene yakın yapmak. Yani requireUser çağrısı layout'ta değil, sayfada veya veriyi çeken katmanda olmalı.

Layout'ta kullanıcı bilgisini yalnızca göstermek için çekiyorsanız (üstteki kullanıcı menüsü gibi) bunun ayrı bir maliyeti var: layout'un en üstündeki await ilk akan parçayı geciktirir ve altındaki içeriği bekletir. Doküman bu durumda await çağrısını iç içe bir Server Component'e taşıyıp Suspense ile sarmalamayı öneriyor, böylece sayfanın geri kalanı önce akar.

Oturum verisini önbelleğe almayın

Next.js 16'da fetch çağrıları varsayılan olarak önbelleğe alınmaz; önbellek artık use cache ile açıkça istenir. Bu, kullanıcıya özel veri açısından iyi bir varsayılan.

use cache kullanan bir bileşende cookies() çağırmak da mümkün değil — doküman bunun hemen hata vereceğini söylüyor. Önerilen kalıp, değerleri önbellekli kapsamın dışında okuyup argüman olarak geçirmek.

Buradaki asıl tuzak da bu kalıpta saklı. Oturum çerezini argüman olarak geçirirseniz kod çalışır, ama bir şey kazanmazsınız: önbellek anahtarı girdilerden üretilir ve doküman "farklı kullanıcı ve filtre kombinasyonları ayrı önbellek kayıtlarına sahip olur" diyor. Yani her kullanıcı için ayrı bir kayıt oluşur, isabet oranı düşer, önbellek sadece şişer.

Pratik kural: use cache yalnızca oturumdan bağımsız veriye. Ürün listesi, blog yazıları, kategori ağacı önbelleğe uygundur; sepet, bildirim sayısı ve profil uygun değildir.

Özet

Next.js ve Django arasındaki oturum sorunlarının çoğu tek bir gerçeği atlamaktan çıkıyor: çerez tarayıcıya aittir, sunucudan attığınız istek onu kendiliğinden taşımaz.

Bu kurulumda işe yarayan düzen şu:

  • Çerezi cookies() ile okuyup giden isteğe elle koyan tek bir apiFetch yazın; her yerde onu kullanın. Gelen tüm çerezleri değil, ihtiyacınız olanları taşıyın.
  • Giriş isteğini tarayıcıdan atın. Set-Cookie başlığını elle yeniden kurmaya çalışmayın; tek origin bunu gereksiz kılıyor.
  • Yazma isteklerinde csrftoken değerini X-CSRFToken başlığına koyun. Backend'e iç ağ üzerinden gidin, referer kontrolü dalına hiç girmeyin.
  • proxy.ts yalnızca iyimser bir yönlendirme katmanıdır — prefetch'te de çalıştığı için oraya backend çağrısı koymayın.
  • Gerçek kontrolü veriye yakın yapın, cache ile sarmalayın, layout'a güvenmeyin.
  • Oturuma bağlı veriyi use cache kapsamına sokmayın.

Bu düzeni kurmadan önce altındaki mimarinin oturmuş olması gerekiyor. Nginx'i tek giriş noktası yapan kurulum tek origin mimarisi yazısında, üç servisi bir arada ayağa kaldıran tam yapılandırma ise Docker Compose ile Next.js + Django production kurulumu yazısında anlatılıyor.