2026-03-15
Service Layer ile Merkezi Yetkilendirme
Dağınık izin kontrollerini merkezi bir service layer'a taşıyın, Next.js middleware guard'ları ekleyin ve derinlemesine savunma yetkilendirme mimarisi oluşturun.
Özet
Post 101’de dağınık izin kontrollerinin kök sorunlarını teşhis ettik: tutarsız mantık, güvenlik açıkları ve bakım yükü. Bu yazı ilk çözümü sunuyor — service layer pattern. Tüm yetkilendirme mantığı, uygulama kodunuz ile veritabanı arasında tek bir katmana taşınıyor. Next.js middleware ile route koruması ve birden fazla seviyede guard pattern’leriyle birleştiğinde, izin kontrollerinin bir kez ve doğru şekilde yapıldığı bir derinlemesine savunma mimarisi oluşuyor.
Dağınık Kontroller Sorunu — Kök Neden
Post 101’de aynı “bu kullanıcı bu kaynağa erişebilir mi?” sorusunun bir sayfa bileşeninde, bir sunucu eyleminde ve bir API route’unda farklı şekilde yanıtlandığı üç kod örneği gösterdik. Her birinde farklı bir hata vardı. Belirtiler tutarsızlık, eksik kontroller ve aşırı izindi.
Kök neden mimariseldir: tüm veri erişiminin geçmesi gereken bir darboğaz noktası yoktur. Sayfalar, sunucu eylemleri ve API route’ları veritabanına doğrudan erişir. İzin kontrolleri her giriş noktasına isteğe bağlı eklemelerdir. Geliştiricilerin bunları eklemeyi hatırlaması gerekir — ve “hatırlama” bir güvenlik stratejisi değildir.
Çözüm bu darboğaz noktasını oluşturmaktır. Her kapıya güvenlik görevlisi yerleştirip hiçbirinin eksik olmadığını ummak yerine, tüm trafiği tek bir kontrol noktasından geçirin.
Service Layer Pattern
Tanım
Martin Fowler, Service Layer’ı “kullanılabilir operasyonlar kümesi oluşturan ve her operasyonda uygulamanın yanıtını koordine eden bir sınır” olarak tanımlar. Yetkilendirme bağlamında service layer, izin kararlarının verildiği ve uygulandığı tek yerdir.
Bu, Next.js’in resmi dokümantasyonundaki önerilen Data Access Layer (DAL) pattern’ine doğrudan karşılık gelir. Temel fikir aynıdır: üç sorumluluğu birleştiren, yalnızca sunucuda çalışan bir katman oluşturun:
- Kimlik doğrulama kontrolü — kullanıcı iddia ettiği kişi mi?
- Yetkilendirme kontrolleri — bu kullanıcı bu kaynakta bu eylemi gerçekleştirmeye yetkili mi?
- Veri erişimi ve dönüştürme — veriyi getir ve yalnızca kullanıcının görmesi gerekeni döndür.
Tek Sorumluluk Prensibi
Yaygın bir yanılgı, SRP’nin “bir fonksiyon bir şey yapar” anlamına geldiğidir. Aslında “bir modülün değişmek için tek bir nedeni olmalı” demektir. Service layer’ın değişme nedeni, izin kuralları veya veri erişim kalıpları değiştiğindedir. Sayfalar UI nedenleriyle değişir. Sunucu eylemleri form yönetimi nedenleriyle değişir. İkisi de yetkilendirme mantığı taşımamalıdır.
Mimari Genel Bakış
Teknik detaylar:
server-onlypaketi, service layer kodunun istemci bileşenlerine dahil edilmesini engeller. Client Component tarafından import edilen bir dosyada bu paketi import etmek derleme hatasına neden olur.- React’ın
cache()fonksiyonu, oturum doğrulamasını tek bir istek/render geçişi içinde memoize eder. Birden fazla bileşeninverifySession()çağırması yalnızca tek bir çerez çözme işlemiyle sonuçlanır.
Refactoring: Dağınık Kontrollerden Merkezi Servise
Adım 1: Oturum Doğrulama
Temel yapı, memoize edilmiş bir oturum doğrulama aracıdır. Her servis metodu önce bunu çağırır.
// lib/auth.ts
import 'server-only';
import { cache } from 'react';
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
// Tek bir istek/render geçişi içinde memoize edilir
export const verifySession = cache(async () => {
const cookieStore = await cookies();
const token = cookieStore.get('session')?.value;
if (!token) {
return null;
}
try {
const session = await decryptAndValidate(token);
return { userId: session.userId, role: session.role };
} catch {
return null; // Fail closed -- geçersiz token = oturum yok
}
});
// Kimliği doğrulanmamış kullanıcıları yönlendiren yardımcı
export const requireSession = cache(async () => {
const session = await verifySession();
if (!session) {
redirect('/login');
}
return session;
});
İki fonksiyon, iki amaç. verifySession() kimliği doğrulanmamış istekler için null döndürür — giriş yapmış ve anonim kullanıcılar için farklı render yapan bileşenler için kullanışlıdır. requireSession() hemen yönlendirir — korunan sayfalar ve servis metotları için kullanışlıdır.
Adım 2: Özel Hata Sınıfları
Servisleri oluşturmadan önce yapılandırılmış hata tipleri tanımlayın. Bunlar genel throw new Error('Unauthorized') çağrılarını belirli, yakalanabilir hatalarla değiştirir.
// lib/errors.ts
export class UnauthorizedError extends Error {
constructor(message = 'Authentication required') {
super(message);
this.name = 'UnauthorizedError';
}
}
export class ForbiddenError extends Error {
constructor(message = 'Access denied') {
super(message);
this.name = 'ForbiddenError';
}
}
export class NotFoundError extends Error {
// Kaynak numaralandırmasını önlemek için genel mesaj
constructor(message = 'Resource not found or access denied') {
super(message);
this.name = 'NotFoundError';
}
}
Tip:
NotFoundErrorkasıtlı olarak genel bir mesaj kullanır. “Document not found” ile “Access denied” döndürmek, kaynağın var olup olmadığını ortaya çıkarır. Hassas kaynaklar için ikisini tek bir mesajda birleştirerek numaralandırma saldırılarını önleyin.
Adım 3: DocumentService
Bu, temel refactoring adımıdır. Post 101’deki dağınık kontrolleri merkezi servis ile karşılaştırın.
Önce — yetkilendirme sunucu eyleminde dağınık (Post 101’den):
// ÖNCE: İzin kontrolü sunucu eyleminde dağınık
'use server';
export async function updateDocument(
documentId: string,
content: string
) {
const session = await getSession();
const document = await db.document.findUnique({
where: { id: documentId },
});
if (document.authorId !== session.userId) {
// Hata: editörlerin de düzenleyebildiğini unutmuş
throw new Error('Unauthorized');
}
// Hata: proje üyeliği kontrolü yok
await db.document.update({
where: { id: documentId },
data: { content },
});
}
Sonra — service layer’da merkezi:
// lib/services/document-service.ts
import 'server-only';
import { requireSession } from '@/lib/auth';
import { ForbiddenError, NotFoundError } from '@/lib/errors';
export async function getDocumentById(documentId: string) {
const session = await requireSession();
const document = await db.document.findUnique({
where: { id: documentId },
include: { project: { include: { members: true } } },
});
if (!document) {
throw new NotFoundError();
}
if (!canAccessDocument(session, document)) {
throw new ForbiddenError();
}
return toDocumentDTO(document, session);
}
export async function updateDocument(
documentId: string,
content: string
) {
const session = await requireSession();
const document = await db.document.findUnique({
where: { id: documentId },
include: { project: { include: { members: true } } },
});
if (!document) {
throw new NotFoundError();
}
if (!canEditDocument(session, document)) {
throw new ForbiddenError();
}
const updated = await db.document.update({
where: { id: documentId },
data: { content },
});
return toDocumentDTO(updated, session);
}
export async function listDocumentsByProject(
projectId: string
) {
const session = await requireSession();
const project = await db.project.findUnique({
where: { id: projectId },
include: { members: true },
});
if (!project) {
throw new NotFoundError();
}
if (!canAccessProject(session, project)) {
throw new ForbiddenError();
}
const documents = await db.document.findMany({
where: { projectId },
});
// Belgeleri kullanıcının erişim seviyesine göre filtrele
return documents
.filter((doc) => canAccessDocument(session, {
...doc,
project,
}))
.map((doc) => toDocumentDTO(
{ ...doc, project },
session
));
}
İzin mantığı, odaklanmış yardımcı fonksiyonlara çıkarıldı:
// document-service.ts içindeki izin yardımcıları
function canAccessDocument(
session: { userId: string; role: string },
document: DocumentWithProject
): boolean {
if (session.role === 'admin') return true;
const project = document.project;
// Public projeler: yayınlanmış belgeler herkese açık
if (
project.visibility === 'public' &&
document.status === 'published'
) {
return true;
}
// Diğer her şey için proje üyesi olmalı
const membership = project.members.find(
(m) => m.userId === session.userId
);
if (!membership) return false;
// Üyeler yayınlanmış belgeleri görebilir
if (document.status === 'published') return true;
// Yazarlar kendi taslaklarını görebilir
if (document.authorId === session.userId) return true;
// Editörler taslaklar dahil tüm belgeleri görebilir
if (membership.role === 'editor') return true;
return false; // Fail closed
}
function canEditDocument(
session: { userId: string; role: string },
document: DocumentWithProject
): boolean {
if (session.role === 'admin') return true;
const membership = document.project.members.find(
(m) => m.userId === session.userId
);
if (!membership) return false;
// Editörler projedeki herhangi bir belgeyi düzenleyebilir
if (membership.role === 'editor') return true;
// Yazarlar kendi belgelerini düzenleyebilir
if (
membership.role === 'author' &&
document.authorId === session.userId
) {
return true;
}
return false; // Fail closed
}
Pattern’e dikkat edin. Her servis metodu aynı yapıyı izler:
- Oturumu doğrula
- Kaynağı ilişkili verilerle getir
- Yetkilendirmeyi kontrol et
- Filtrelenmiş DTO döndür
Adım 4: ProjectService
Aynı pattern projeler için de geçerlidir. Kaynak başına bir servis dosyası, kod tabanını düzenli tutar.
// lib/services/project-service.ts
import 'server-only';
import { requireSession } from '@/lib/auth';
import { ForbiddenError, NotFoundError } from '@/lib/errors';
export async function getProjectById(projectId: string) {
const session = await requireSession();
const project = await db.project.findUnique({
where: { id: projectId },
include: { members: true },
});
if (!project) {
throw new NotFoundError();
}
if (!canAccessProject(session, project)) {
throw new ForbiddenError();
}
return toProjectDTO(project, session);
}
export async function listProjectsForUser() {
const session = await requireSession();
if (session.role === 'admin') {
const projects = await db.project.findMany({
include: { members: true },
});
return projects.map((p) => toProjectDTO(p, session));
}
const projects = await db.project.findMany({
where: {
OR: [
{ visibility: 'public' },
{ members: { some: { userId: session.userId } } },
{ ownerId: session.userId },
],
},
include: { members: true },
});
return projects.map((p) => toProjectDTO(p, session));
}
function canAccessProject(
session: { userId: string; role: string },
project: ProjectWithMembers
): boolean {
if (session.role === 'admin') return true;
if (project.visibility === 'public') return true;
if (project.ownerId === session.userId) return true;
const membership = project.members.find(
(m) => m.userId === session.userId
);
return !!membership; // Fail closed
}
Adım 5: Güvenli Veri Transferi için DTO Pattern
Ham veritabanı nesneleri istemciye ulaşmaması gereken alanlar içerir. Dahili ID’ler, zaman damgaları, soft-delete flag’leri ve diğer rollere yönelik meta veriler, veritabanı modelini doğrudan döndürdüğünüzde sızar.
DTO (Data Transfer Object) pattern’i bunu çözer. Her servis metodu, kullanıcının rolüne göre filtrelenmiş bir nesne döndürür.
// lib/services/document-service.ts
interface DocumentDTO {
id: string;
title: string;
content: string;
status: string;
projectId: string;
authorId: string;
reviewComments?: string[];
}
function toDocumentDTO(
document: DocumentWithProject,
session: { userId: string; role: string }
): DocumentDTO {
const membership = document.project.members.find(
(m) => m.userId === session.userId
);
const base: DocumentDTO = {
id: document.id,
title: document.title,
content: document.content,
status: document.status,
projectId: document.projectId,
authorId: document.authorId,
};
// Editörler ve adminler inceleme meta verilerini görür
if (
session.role === 'admin' ||
membership?.role === 'editor'
) {
return {
...base,
reviewComments: document.reviewComments,
};
}
return base;
}
Bu, Post 101’deki aşırı izin sorununu çözer. Viewer başlık ve içerik alır. Editör başlık, içerik ve inceleme yorumlarını alır. Aynı kaynak, farklı görünümler — tek bir yerde kontrol ediliyor.
Adım 6: İnce Sunucu Eylemleri
Service layer yerindeyken, sunucu eylemleri ince sarmalayıcılar haline gelir. Form verilerini yönetir ve servisi çağırır. Başka bir şey yok.
// app/actions/documents.ts
'use server';
import { updateDocument } from '@/lib/services/document-service';
export async function updateDocumentAction(
documentId: string,
content: string
) {
// Tüm yetkilendirme servis içinde gerçekleşir
return updateDocument(documentId, content);
}
Bunu Post 101’deki hatalı sunucu eylemiyle karşılaştırın. Eylem artık hiçbir izin mantığı içermiyor. İzin hataları olamaz çünkü her şeyi service layer’a devreder.
Route Koruması için Next.js Middleware
Not: Next.js 16’da
middleware.tsdosyasıproxy.tsolarak, export edilen fonksiyon daproxyolarak yeniden adlandırıldı. Bu bölümdeki pattern’ler ve konseptler her ikisi için de geçerlidir. Next.js 15 veya daha eski sürümlerdemiddleware.tskullanın.
Middleware Ne Yapmalı
Middleware, eşleşen her istekten önce çalışır. Hızlıdır ve çerezlere ve header’lara erişimi vardır. Bu onu tek bir iş için uygun kılar: iyimser kimlik doğrulama kontrolleri.
- Oturum çerezinin var olup olmadığını kontrol etme
- Kimliği doğrulanmamış kullanıcıları korunan route’lardan uzaklaştırma
- Oturum token’ından kaba taneli rol iddialarını kontrol etme (“bu kullanıcı admin mi?” gibi)
- İlk savunma katmanı olarak hareket etme — son değil
Middleware Ne YAPMAMALI
- Tek yetkilendirme mekanizması olmamalı (CVE-2025-29927 nedenini gösterdi)
- İzin kontrolleri için veritabanı sorgusu yapmamalı (performans sorunu — her istekte çalışır)
- Kaynak düzeyinde yetkilendirme yapmamalı (middleware kaynak bağlamına sahip değil — kullanıcının hangi belgeye erişmeye çalıştığını bilmez)
Uygulama
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import { decrypt } from '@/lib/session';
const publicRoutes = ['/', '/login', '/signup'];
const adminRoutes = ['/admin'];
export async function middleware(request: NextRequest) {
const path = request.nextUrl.pathname;
// Public route'lara kimlik doğrulama olmadan izin ver
if (publicRoutes.some((route) => path === route)) {
return NextResponse.next();
}
// Oturum çerezini kontrol et (iyimser -- DB çağrısı yok)
const cookie = request.cookies.get('session')?.value;
const session = await decrypt(cookie);
if (!session?.userId) {
return NextResponse.redirect(
new URL('/login', request.url)
);
}
// Admin route'lar: oturum token'ından rolü kontrol et
if (adminRoutes.some((route) => path.startsWith(route))) {
if (session.role !== 'admin') {
return NextResponse.redirect(
new URL('/', request.url)
);
}
}
return NextResponse.next();
}
export const config = {
matcher: [
'/((?!api|_next/static|_next/image|.*\\.png$).*)',
],
};
Bu middleware kasıtlı olarak basittir. Çerez varlığını ve oturum geçerliliğini kontrol eder. Veritabanı sorgusu yapmaz. Kaynak düzeyinde yetkilendirme yapmaz. Bu sorumluluklar service layer’a aittir.
Derinlemesine Savunma
Tek bir katman yeterli değildir. Her katman, diğerlerindeki potansiyel arızaları telafi eder.
- Katman 1 — Middleware: Kimliği doğrulanmamış isteklerin hızlı reddi. Oturum çerezlerine dayalı iyimser kontrol.
- Katman 2 — Service Layer: Tam kaynak bağlamıyla kesin yetkilendirme. Kullanıcı kimliği, rol, proje üyeliği ve belge sahipliğini kontrol eder.
- Katman 3 — Veritabanı: Foreign key’ler ve satır düzeyi güvenlik politikaları (gelecek bir yazıda ele alınacak) gibi kısıtlamalar son bir güvenlik ağı sağlar.
Middleware atlanırsa (CVE-2025-29927’deki gibi), service layer yetkisiz erişimi engellemeye devam eder. Service layer’da bir hata yanlış kaynağa erişim verirse, veritabanı kısıtlamaları tutarsızlığı yakalayabilir. Her katman bağımsız olarak koruma sağlar.
Guard Pattern’leri
Route Düzeyi Guard’lar (Middleware)
Yukarıda ele alındı. Middleware, sayfa render edilmeden önce kimliği doğrulanmamış kullanıcıları yönlendirir. Bu kaba taneldir: “Kullanıcı giriş yapmış mı?” ve “Bu korunan bir route mu?”
Sayfa Düzeyi Guard’lar (Server Component’ler)
Sayfa bileşenleri servis metotlarını doğrudan çağırır. Servis, kimlik doğrulama, yetkilendirme ve veri erişimini tek bir çağrıda yönetir.
// app/projects/[id]/page.tsx
import { getProjectById } from '@/lib/services/project-service';
export default async function ProjectPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
// Servis yönetir: oturum + yetkilendirme + veri erişimi
const project = await getProjectById(id);
return <ProjectView project={project} />;
}
Bunu Post 101’deki sürümle karşılaştırın — sayfa bileşeni tüm izin mantığını satır içi olarak içeriyordu. Sayfa artık ince. Servisi çağırır ve sonucu render eder. Kullanıcı bu projeye erişemiyorsa, servis hata fırlatır — sayfa değil.
Bileşen Düzeyi Guard’lar (Koşullu Render)
Server Component’ler verifySession() çağırabilir ve role göre koşullu render yapabilir. Bu yalnızca UI/UX amaçlıdır — güvenlik değil. Service layer güvenlik sınırıdır.
// components/document-actions.tsx
import { verifySession } from '@/lib/auth';
export async function DocumentActions({
document,
}: {
document: DocumentDTO;
}) {
const session = await verifySession();
if (!session) return null;
const canEdit =
session.role === 'admin' ||
session.role === 'editor' ||
document.authorId === session.userId;
return (
<div>
{canEdit && <EditButton documentId={document.id} />}
{session.role === 'admin' && (
<DeleteButton documentId={document.id} />
)}
</div>
);
}
Warning: Bu bileşen düzeyi kontrolü service layer mantığını kopyalar. Servisteki düzenleme izin kuralı değişirse, burada da değişmelidir. Bu çoğaltma, mevcut yaklaşımın bilinen bir sınırlamasıdır. Post 103 (RBAC), izin kurallarını tek bir kaynaktan sorgulanabilir hale getirerek bu sorunu çözer.
Sunucu Eylemi Guard’ları (Higher-Order Wrapper)
withAuth higher-order fonksiyonu, sunucu eylemlerini kimlik doğrulamayla sarar. Bu, her eylemin yürütülmeden önce oturumu doğrulamasını sağlar.
// lib/action-guard.ts
import { requireSession } from '@/lib/auth';
type ActionFn<TInput, TOutput> = (
session: { userId: string; role: string },
input: TInput
) => Promise<TOutput>;
export function withAuth<TInput, TOutput>(
action: ActionFn<TInput, TOutput>
) {
return async (input: TInput): Promise<TOutput> => {
const session = await requireSession();
return action(session, input);
};
}
Sunucu eyleminde kullanım:
// app/actions/documents.ts
'use server';
import { withAuth } from '@/lib/action-guard';
import { updateDocument } from '@/lib/services/document-service';
export const updateDocumentAction = withAuth(
async (session, input: { documentId: string; content: string }) => {
return updateDocument(input.documentId, input.content);
}
);
withAuth sarmalayıcısı, eylem gövdesi çalışmadan önce oturumun var olduğunu garanti eder. Servis metodu içindeki yetkilendirmeyle birleştiğinde, her sunucu eylemi için iki katmanlı koruma sağlar.
Test Edilebilirlik
Service layer’ın en büyük kazanımlarından biri test edilebilirliktir. İzin mantığı render, routing ve istek yönetiminden izole edilmiştir. Testlerin girdileri ve çıktıları nettir.
// __tests__/services/document-service.test.ts
import { getDocumentById } from '@/lib/services/document-service';
import { ForbiddenError } from '@/lib/errors';
// Farklı kullanıcı rolleri için oturumu mockla
jest.mock('@/lib/auth', () => ({
requireSession: jest.fn(),
}));
describe('getDocumentById', () => {
it('admin herhangi bir belgeye erişebilir', async () => {
mockSession({ userId: 'admin-1', role: 'admin' });
mockDocument({ id: 'doc-1', status: 'draft' });
const result = await getDocumentById('doc-1');
expect(result.id).toBe('doc-1');
});
it('viewer taslak belgelere erişemez', async () => {
mockSession({ userId: 'viewer-1', role: 'viewer' });
mockDocument({
id: 'doc-1',
status: 'draft',
authorId: 'author-1',
});
await expect(
getDocumentById('doc-1')
).rejects.toThrow(ForbiddenError);
});
it('yazar kendi taslağına erişebilir', async () => {
mockSession({ userId: 'author-1', role: 'author' });
mockDocument({
id: 'doc-1',
status: 'draft',
authorId: 'author-1',
});
const result = await getDocumentById('doc-1');
expect(result.id).toBe('doc-1');
});
});
Her test, belirli bir izin kuralını izole olarak doğrular. Sayfa render’ı yok. İstek bağlamı yok. Form yönetimi yok. Sadece: “Kullanıcı X, rol Y ile belge Z’ye eriştiğinde, izin ver veya reddet.”
Dağınık kontrolleri test etmeyle karşılaştırın. Sayfaları render etmeniz, tam istek bağlamlarıyla sunucu eylemlerini çağırmanız ve kimlik doğrulama middleware’ini kurmanız gerekirdi. Service layer, en kritik mantığı — izinleri — test etmesi en kolay kılar.
Faydalar ve Sınırlamalar
Service Layer’ın Çözdükleri
| Post 101 Sorunu | Service Layer Çözümü |
|---|---|
| 3+ konumda dağınık kontroller | Kaynak işlemi başına tek konum |
| Katmanlar arası tutarsız mantık | Tek uygulama, her yerden çağrılır |
| Yeni endpoint’lerde eksik kontroller | Yeni endpoint’ler servis metotlarını çağırmalı |
| Aşırı izin (çok fazla veri döndürme) | DTO pattern’i verileri role göre filtreler |
| İzin mantığını test etme zorluğu | Servis metotları bağımsız test edilebilir |
| Tek doğruluk kaynağı yok | Service layer doğruluk kaynağı OLUR |
Hala Çözülmemiş Olanlar
Service layer, izin kontrollerinin nerede yapıldığını çözer. Ancak izin kurallarının kendisi hala sabit kodlanmış if/else ifadeleridir. Bu birkaç sınırlama oluşturur:
- Sabit kodlanmış rol kontrolleri: İzin mantığı, servis metotları içinde
if (role === 'admin')şeklindedir. Yeni bir rol eklemek her ilgili servis metodunu değiştirmeyi gerektirir. - İzin matrisi yok: “Rol X, kaynak Z üzerinde eylem Y’yi yapabilir” şeklinde bildirimsel, incelenebilir bir liste yoktur. İzin kuralları kod mantığına gömülüdür.
- Bileşen düzeyi çoğaltma: UI guard kontrolleri service layer mantığını kopyalar. Bunları senkronize tutmak manuel ve hataya açıktır.
- Rol değişiklikleri kod değişikliği gerektirir: Yayınlayabilen ama silemeyen bir “moderator” rolü eklemek birden fazla servis metoduna dokunmayı gerektirir.
- Denetim izi yok: Service layer izinleri uygular ama kimin neye ne zaman eriştiğini kaydetmez.
Bu sınırlamalar doğrudan bir sonraki adıma işaret eder: sabit kodlanmış if/else ifadelerini yapılandırılmış bir izin sistemiyle değiştirmek.
Yaygın Tuzaklar
-
“Sadece bu seferlik” veritabanı atlama: Service layer dışındaki her doğrudan veritabanı erişimi potansiyel bir yetkilendirme açığıdır. Pattern’i kod incelemesiyle uygulayın ve servis dosyaları dışındaki doğrudan veritabanı import’larını işaretleyen özel bir ESLint kuralı düşünün.
-
server-onlyeklemeyi unutmak: Bu import olmadan, service layer kodu bir Client Component tarafından import edilebilir. Bu, dahili mantığı ve veritabanı erişim kalıplarını istemci paketine sızdırır. -
Oturum doğrulama için
cache()kullanmamak: Memoizasyon olmadan, bir sayfa bileşeninde ve bir alt sunucu bileşenindeverifySession()çağırmak istek başına iki çerez çözme işlemiyle sonuçlanır. -
Ham veritabanı nesneleri döndürmek: Servis metotlarından her zaman DTO döndürün. Ham nesneler zaman damgaları, soft-delete flag’leri veya istemciye ulaşmaması gereken dahili alanlar içerebilir.
-
Kaynak varlığını sızdıran genel hata mesajları:
throw new Error('Document not found')kaynağın var olmadığını ortaya çıkarır. Hassas kaynaklar için numaralandırmayı önlemek üzere “not found or access denied” kullanın.
Sıradaki
Service layer, yetkilendirme için doğru mimaridir. Tüm veri erişimi izin-farkında servisler üzerinden akar. Middleware ek bir savunma katmanı sağlar. Guard pattern’leri birden fazla seviyede oluşturulur.
Ancak service layer içindeki izin kuralları hala sabit kodlanmış if/else ifadeleridir. Bir kuralı değiştirmek kod değiştirmek demektir. Bir rol eklemek her servis metoduna dokunmak demektir. Tüm izinleri bir bakışta incelemenin yolu yoktur.
Post 103’te Rol Bazlı Erişim Kontrolü’nü (RBAC) tanıtacağız. Sabit kodlanmış koşullar yerine, izin kuralları veri haline gelir — hem service layer’ın hem de UI’ın sorgulayabileceği yapılandırılmış bir matris.
// Ön izleme -- tam uygulama Post 103'te
// Bunun yerine:
if (role === 'admin' || role === 'editor') {
// düzenlemeye izin ver
}
// Şunu kullanacağız:
if (hasPermission(session.role, 'document', 'update')) {
// düzenlemeye izin ver
}
Service layer kalır. Yalnızca izin kontrollerinin iç yapısı değişir.
Kaynaklar
- Martin Fowler - Service Layer - Patterns of Enterprise Application Architecture’dan Service Layer pattern’inin temel tanımı
- Next.js Authentication Guide (Resmi) - Data Access Layer pattern’i,
cache()ileverifySession()ve DTO önerilerini kapsayan resmi Next.js dokümantasyonu - Next.js Data Security Guide (Resmi) -
server-only, DTO pattern’leri, React Taint API’leri ve üç veri getirme yaklaşımı hakkında resmi dokümantasyon - How to Think About Security in Next.js (Resmi Blog) - Server Component’ler ve Server Action’lar için güvenlik modeli hakkında Next.js ekibi rehberi
- CVE-2025-29927: Next.js Middleware Authorization Bypass - Middleware’in neden tek yetkilendirme katmanı olmaması gerektiğini gösteren CVSS 9.1 güvenlik açığının teknik analizi
- OWASP Authorization Cheat Sheet - Merkezi yetkilendirme mantığı ve derinlemesine savunma için en iyi uygulamalar
- OWASP Fail Securely - Güvenlik metotlarının istisna durumunda neden false döndürmesi gerektiği ve fail-closed prensibi
- Defense in Depth (Cloudflare) - Bu yazıda uygulanan katmanlı güvenlik modelinin açıklaması
- Authorization in Next.js - Robin Wieruch - Next.js App Router’da katmanlı yetkilendirme için kapsamlı rehber
- Next.js Server Actions Security - MakerKit - Server Action güvenlik açıklarını ve
next-safe-actionmiddleware bileşim pattern’ini kapsayan rehber - Understanding the Data Access Layer in Next.js -
server-onlykullanımı ve istek tekrarını önleme içincache()dahil DAL pattern’inin pratik açıklaması
Ölçeklenebilir İzin Sistemleri
TypeScript ve Next.js ile ölçeklenebilir izin sistemleri oluşturma rehberi. Basit kontrol mekanizmalarından RBAC ve ABAC'a, oradan multi-tenant yetkilendirme sistemlerine kadar kapsamlı bir seri.
Serideki tüm yazılar
İlgili yazılar
Authentication ve authorization farkı, yaygın izin sistemi tuzakları, fail-closed prensibi ve her izin sisteminin karşılaması gereken hedefler.
TypeScript ile type-safe bir RBAC sistemi oluşturun, birleşik bir can() fonksiyonu yazın, UI ve backend'de izinleri senkronize edin ve RBAC'ın sınırlarını anlayın.
TypeScript'te builder pattern, koşullu izinler ve RBAC'ın sınırlarını aşan type-safe policy değerlendirmesi ile bir ABAC policy engine oluşturun.
ABAC'ı ortam bazlı kurallar, alan seviyesinde okuma ve yazma izinleri ve tekrarlanan izin mantığını ortadan kaldıran otomatik veritabanı sorgusu filtreleme ile genişletin.
İzin sisteminize multi-tenant izolasyonu ekleyin, CASL'ı bir kütüphane alternatifi olarak değerlendirin ve doğru yetkilendirme mimarisini seçmek için karar çerçevelerini kullanın.