W poprzednich modułach poznaliśmy podstawy uwierzytelniania, podłączanie zewnętrznych dostawców, zarządzanie sesjami i implementację systemu ról i uprawnień. Teraz skupimy się na middleware - potężnym mechanizmie Next.js, który pozwala na wykonanie kodu przed dotarciem do żądania docelowego, co jest idealnym miejscem do implementacji ochrony tras.
Middleware w Next.js działa na poziomie serwera, ale jest wykonywane przed renderowaniem strony lub obsługą żądania API. Daje to możliwość centralnego zarządzania dostępem, przekierowywania, modyfikacji nagłówków i wielu innych operacji.
Zacznijmy od zrozumienia, jak działa middleware w Next.js:
middleware.ts (lub middleware.js) w głównym katalogu projektu Next.js.Prosty przykład middleware:
1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function middleware(request: NextRequest) {
6 console.log('Middleware uruchomione dla:', request.nextUrl.pathname);
7
8 // Kontynuuj normalne przetwarzanie żądania
9 return NextResponse.next();
10}
11
12// Zastosuj middleware tylko do określonych ścieżek
13export const config = {
14 matcher: ['/dashboard/:path*', '/api/:path*'],
15};Teraz przejdźmy do pełnej implementacji middleware do ochrony tras w aplikacji Next.js.
Zacznijmy od prostego middleware, które sprawdza, czy użytkownik jest zalogowany, i chroni określone ścieżki:
1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4import { getToken } from 'next-auth/jwt';
5
6export async function middleware(request: NextRequest) {
7 // Pobierz token sesji z ciasteczka
8 const token = await getToken({ req: request });
9
10 // Sprawdź, czy użytkownik jest zalogowany
11 const isLoggedIn = !!token;
12
13 // Aktualna ścieżka
14 const { pathname } = request.nextUrl;
15
16 // Ścieżki chronione - wymagające zalogowania
17 const protectedPaths = [
18 '/dashboard',
19 '/profile',
20 '/settings',
21 ];
22
23 // Ścieżki tylko dla niezalogowanych (np. strona logowania)
24 const authPaths = ['/auth/login', '/auth/register'];
25
26 // Sprawdź, czy aktualna ścieżka jest chroniona
27 const isProtectedPath = protectedPaths.some(path =>
28 pathname === path || pathname.startsWith(`${path}/`)
29 );
30
31 // Sprawdź, czy aktualna ścieżka jest stroną uwierzytelniania
32 const isAuthPath = authPaths.some(path => pathname === path);
33
34 // Jeśli użytkownik próbuje dostać się do chronionej ścieżki, ale nie jest zalogowany
35 if (isProtectedPath && !isLoggedIn) {
36 // Przekieruj do strony logowania z informacją o docelowej ścieżce
37 const url = new URL('/auth/login', request.url);
38 url.searchParams.set('callbackUrl', encodeURI(pathname));
39 return NextResponse.redirect(url);
40 }
41
42 // Jeśli użytkownik jest zalogowany i próbuje dostać się do strony logowania/rejestracji
43 if (isAuthPath && isLoggedIn) {
44 // Przekieruj na dashboard
45 return NextResponse.redirect(new URL('/dashboard', request.url));
46 }
47
48 // W innych przypadkach kontynuuj normalne przetwarzanie
49 return NextResponse.next();
50}
51
52// Określ, dla których ścieżek middleware powinno być uruchamiane
53export const config = {
54 matcher: [
55 /*
56 * Dopasuj wszystkie ścieżki z wyjątkiem:
57 * 1. Plików statycznych (np. /favicon.ico, /images/*, itd.)
58 * 2. API Route next-auth (/api/auth/*, które obsługuje samo uwierzytelnianie)
59 */
60 '/((?!api/auth|_next/static|_next/image|favicon.ico).*)',
61 ],
62};Rozbudujmy nasze middleware, aby uwzględniało również role i uprawnienia użytkowników:
1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4import { getToken } from 'next-auth/jwt';
5
6// Struktura opisująca wymagania dostępu dla ścieżki
7interface PathConfig {
8 requireAuth: boolean; // Czy wymagane jest zalogowanie
9 roles?: string[]; // Dozwolone role (jeśli puste, wszystkie role mają dostęp)
10 permissions?: string[]; // Wymagane uprawnienia
11}
12
13// Konfiguracja dostępu do ścieżek
14const pathAccessConfig: Record<string, PathConfig> = {
15 // Ścieżki administracyjne
16 '/admin': {
17 requireAuth: true,
18 roles: ['admin'],
19 },
20
21 // Ścieżki moderatorów
22 '/moderator': {
23 requireAuth: true,
24 roles: ['admin', 'moderator'],
25 },
26
27 // Ścieżki dashboard - wymagają zalogowania
28 '/dashboard': {
29 requireAuth: true,
30 },
31
32 // Strona zarządzania użytkownikami - wymaga uprawnienia user:manage
33 '/admin/users': {
34 requireAuth: true,
35 roles: ['admin'],
36 permissions: ['user:manage'],
37 },
38
39 // Ścieżki publiczne - dostępne dla wszystkich
40 '/': {
41 requireAuth: false,
42 },
43 '/about': {
44 requireAuth: false,
45 },
46 '/contact': {
47 requireAuth: false,
48 },
49
50 // Ścieżki uwierzytelniania - dostępne tylko dla niezalogowanych
51 '/auth/login': {
52 requireAuth: false,
53 },
54 '/auth/register': {
55 requireAuth: false,
56 },
57};
58
59// Funkcja pomocnicza do sprawdzania, czy ścieżka pasuje do konfiguracji
60function matchPathConfig(pathname: string): [string, PathConfig] | null {
61 // Sprawdź dokładne dopasowanie
62 if (pathAccessConfig[pathname]) {
63 return [pathname, pathAccessConfig[pathname]];
64 }
65
66 // Sprawdź dopasowanie prefiksu
67 for (const configPath in pathAccessConfig) {
68 // Ignoruj dokładne dopasowania, ponieważ już je sprawdziliśmy
69 if (configPath === pathname) continue;
70
71 // Sprawdź, czy aktualna ścieżka zaczyna się od konfigurowanej ścieżki + /
72 if (pathname.startsWith(`${configPath}/`)) {
73 return [configPath, pathAccessConfig[configPath]];
74 }
75 }
76
77 // Brak dopasowania
78 return null;
79}
80
81export async function middleware(request: NextRequest) {
82 // Pobierz token sesji z ciasteczka
83 const token = await getToken({ req: request });
84
85 // Aktualna ścieżka
86 const { pathname } = request.nextUrl;
87
88 // Znajdź konfigurację dostępu dla aktualnej ścieżki
89 const pathMatch = matchPathConfig(pathname);
90
91 // Jeśli nie znaleziono konfiguracji dla tej ścieżki, kontynuuj normalne przetwarzanie
92 if (!pathMatch) {
93 return NextResponse.next();
94 }
95
96 const [configPath, config] = pathMatch;
97
98 // Strony uwierzytelniania - przekieruj do dashboard, jeśli użytkownik jest już zalogowany
99 if (
100 !config.requireAuth &&
101 (pathname === '/auth/login' || pathname === '/auth/register') &&
102 token
103 ) {
104 return NextResponse.redirect(new URL('/dashboard', request.url));
105 }
106
107 // Jeśli ścieżka wymaga uwierzytelnienia, a użytkownik nie jest zalogowany
108 if (config.requireAuth && !token) {
109 // Przekieruj do strony logowania z informacją o docelowej ścieżce
110 const url = new URL('/auth/login', request.url);
111 url.searchParams.set('callbackUrl', pathname);
112 return NextResponse.redirect(url);
113 }
114
115 // Jeśli ścieżka wymaga określonych ról, sprawdź uprawnienia użytkownika
116 if (token && config.roles && config.roles.length > 0) {
117 const userRole = token.role as string;
118
119 if (!config.roles.includes(userRole)) {
120 // Użytkownik nie ma wymaganej roli - przekieruj do strony braku uprawnień
121 return NextResponse.redirect(new URL('/auth/unauthorized', request.url));
122 }
123 }
124
125 // Jeśli ścieżka wymaga określonych uprawnień, sprawdź uprawnienia użytkownika
126 if (token && config.permissions && config.permissions.length > 0) {
127 const userPermissions = token.permissions as string[] || [];
128
129 // Sprawdź, czy użytkownik ma wszystkie wymagane uprawnienia
130 const hasAllPermissions = config.permissions.every(permission =>
131 userPermissions.includes(permission)
132 );
133
134 if (!hasAllPermissions) {
135 // Użytkownik nie ma wymaganych uprawnień - przekieruj do strony braku uprawnień
136 return NextResponse.redirect(new URL('/auth/unauthorized', request.url));
137 }
138 }
139
140 // Wszystkie warunki spełnione - kontynuuj
141 return NextResponse.next();
142}
143
144// Dopasowanie dla wszystkich tras z wyjątkiem plików statycznych i API uwierzytelniania
145export const config = {
146 matcher: [
147 '/((?!api/auth|_next/static|_next/image|images|favicon.ico).*)',
148 ],
149};Jak widać, opisane middleware działa dla stron, przekierowując użytkownika w przypadku braku uprawnień. W przypadku API routes powinniśmy zwrócić odpowiednie kody błędów, a nie przekierowywać użytkownika. Oto implementacja middleware specjalnie dla API:
1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4import { getToken } from 'next-auth/jwt';
5
6// Konfiguracja dostępu do tras API
7const apiAccessConfig: Record<string, {
8 roles?: string[];
9 permissions?: string[];
10}> = {
11 // API administratora
12 '/api/admin': {
13 roles: ['admin'],
14 },
15
16 // API moderatora
17 '/api/moderator': {
18 roles: ['admin', 'moderator'],
19 },
20
21 // API zarządzania użytkownikami
22 '/api/users': {
23 permissions: ['user:read'],
24 },
25 '/api/users/create': {
26 permissions: ['user:create'],
27 },
28 '/api/users/update': {
29 permissions: ['user:update'],
30 },
31 '/api/users/delete': {
32 permissions: ['user:delete'],
33 },
34};
35
36// Funkcja pomocnicza do sprawdzania, czy ścieżka API pasuje do konfiguracji
37function matchApiConfig(pathname: string) {
38 // Sprawdź dokładne dopasowanie
39 if (apiAccessConfig[pathname]) {
40 return [pathname, apiAccessConfig[pathname]];
41 }
42
43 // Sprawdź dopasowanie prefiksu
44 for (const configPath in apiAccessConfig) {
45 if (pathname.startsWith(`${configPath}/`)) {
46 return [configPath, apiAccessConfig[configPath]];
47 }
48 }
49
50 return null;
51}
52
53export async function middleware(request: NextRequest) {
54 // Pobierz token sesji
55 const token = await getToken({ req: request });
56
57 // Aktualna ścieżka
58 const { pathname } = request.nextUrl;
59
60 // Sprawdź, czy to żądanie API
61 const isApiRequest = pathname.startsWith('/api/');
62
63 // Jeśli to nie jest API, przejdź do następnego middleware
64 if (!isApiRequest) {
65 return NextResponse.next();
66 }
67
68 // Pomiń routes uwierzytelniania
69 if (pathname.startsWith('/api/auth/')) {
70 return NextResponse.next();
71 }
72
73 // Jeśli to API i użytkownik nie jest zalogowany
74 if (!token) {
75 return NextResponse.json(
76 { error: 'Unauthorized - Authentication required' },
77 { status: 401 }
78 );
79 }
80
81 // Znajdź konfigurację dostępu dla tego endpointu API
82 const apiMatch = matchApiConfig(pathname);
83
84 // Jeśli brak konfiguracji, zakładamy, że wystarczy być zalogowanym
85 if (!apiMatch) {
86 return NextResponse.next();
87 }
88
89 const [configPath, config] = apiMatch;
90
91 // Sprawdź rolę użytkownika
92 if (config.roles && config.roles.length > 0) {
93 const userRole = token.role as string;
94
95 if (!config.roles.includes(userRole)) {
96 return NextResponse.json(
97 { error: 'Forbidden - Insufficient role permissions' },
98 { status: 403 }
99 );
100 }
101 }
102
103 // Sprawdź uprawnienia użytkownika
104 if (config.permissions && config.permissions.length > 0) {
105 const userPermissions = token.permissions as string[] || [];
106
107 const hasAllPermissions = config.permissions.every(permission =>
108 userPermissions.includes(permission)
109 );
110
111 if (!hasAllPermissions) {
112 return NextResponse.json(
113 { error: 'Forbidden - Insufficient permissions' },
114 { status: 403 }
115 );
116 }
117 }
118
119 // Wszystkie warunki spełnione
120 return NextResponse.next();
121}
122
123// Dopasowanie dla wszystkich tras API
124export const config = {
125 matcher: ['/api/:path*'],
126};Możemy połączyć oba podejścia w jednym middleware:
1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4import { getToken } from 'next-auth/jwt';
5
6// Konfiguracja dostępu do stron
7const pageAccessConfig: Record<string, {
8 requireAuth: boolean;
9 roles?: string[];
10 permissions?: string[];
11}> = {
12 '/admin': { requireAuth: true, roles: ['admin'] },
13 '/moderator': { requireAuth: true, roles: ['admin', 'moderator'] },
14 '/dashboard': { requireAuth: true },
15 '/profile': { requireAuth: true },
16 '/settings': { requireAuth: true },
17 '/': { requireAuth: false },
18 '/about': { requireAuth: false },
19 '/auth/login': { requireAuth: false },
20 '/auth/register': { requireAuth: false },
21};
22
23// Konfiguracja dostępu do API
24const apiAccessConfig: Record<string, {
25 roles?: string[];
26 permissions?: string[];
27}> = {
28 '/api/admin': { roles: ['admin'] },
29 '/api/moderator': { roles: ['admin', 'moderator'] },
30 '/api/users': { permissions: ['user:read'] },
31 '/api/users/create': { permissions: ['user:create'] },
32};
33
34// Funkcje pomocnicze do dopasowywania ścieżek (pominięte dla zwięzłości)
35//
36export async function middleware(request: NextRequest) {
37 // Pobierz token sesji
38 const token = await getToken({ req: request });
39 const { pathname } = request.nextUrl;
40
41 // Sprawdź, czy to żądanie API
42 const isApiRequest = pathname.startsWith('/api/');
43
44 // Pomiń routes uwierzytelniania
45 if (pathname.startsWith('/api/auth/')) {
46 return NextResponse.next();
47 }
48
49 // Obsługa żądań API
50 if (isApiRequest) {
51 // Jeśli to API i użytkownik nie jest zalogowany (dla chronionych endpointów)
52 if (!token && !pathname.startsWith('/api/public/')) {
53 return NextResponse.json(
54 { error: 'Unauthorized - Authentication required' },
55 { status: 401 }
56 );
57 }
58
59 // Dopasuj konfigurację API
60 const apiMatch = matchApiConfig(pathname);
61 if (apiMatch && token) {
62 const [configPath, config] = apiMatch;
63
64 // Sprawdź role i uprawnienia
65 if (!hasRequiredRole(token, config.roles) ||
66 !hasRequiredPermissions(token, config.permissions)) {
67 return NextResponse.json(
68 { error: 'Forbidden - Insufficient permissions' },
69 { status: 403 }
70 );
71 }
72 }
73 }
74 // Obsługa stron
75 else {
76 // Dopasuj konfigurację strony
77 const pageMatch = matchPageConfig(pathname);
78 if (pageMatch) {
79 const [configPath, config] = pageMatch;
80
81 // Strony uwierzytelniania - przekieruj, jeśli użytkownik jest zalogowany
82 if (
83 !config.requireAuth &&
84 (pathname === '/auth/login' || pathname === '/auth/register') &&
85 token
86 ) {
87 return NextResponse.redirect(new URL('/dashboard', request.url));
88 }
89
90 // Jeśli strona wymaga uwierzytelnienia, a użytkownik nie jest zalogowany
91 if (config.requireAuth && !token) {
92 const url = new URL('/auth/login', request.url);
93 url.searchParams.set('callbackUrl', pathname);
94 return NextResponse.redirect(url);
95 }
96
97 // Jeśli użytkownik jest zalogowany, sprawdź role i uprawnienia
98 if (token && config.requireAuth) {
99 if (!hasRequiredRole(token, config.roles) ||
100 !hasRequiredPermissions(token, config.permissions)) {
101 return NextResponse.redirect(new URL('/auth/unauthorized', request.url));
102 }
103 }
104 }
105 }
106
107 // Wszystkie warunki spełnione
108 return NextResponse.next();
109}
110
111// Funkcje pomocnicze do sprawdzania ról i uprawnień
112function hasRequiredRole(token: any, requiredRoles?: string[]): boolean {
113 if (!requiredRoles || requiredRoles.length === 0) return true;
114
115 const userRole = token.role as string;
116 return requiredRoles.includes(userRole);
117}
118
119function hasRequiredPermissions(token: any, requiredPermissions?: string[]): boolean {
120 if (!requiredPermissions || requiredPermissions.length === 0) return true;
121
122 const userPermissions = token.permissions as string[] || [];
123 return requiredPermissions.every(permission => userPermissions.includes(permission));
124}
125
126// Funkcje dopasowujące konfigurację ścieżek (implementacja pominięta dla zwięzłości)
127function matchPageConfig(pathname: string) { /* ... */ }
128function matchApiConfig(pathname: string) { /* ... */ }
129
130export const config = {
131 matcher: [
132 '/((?!_next/static|_next/image|images|favicon.ico).*)',
133 ],
134};Często potrzebujemy middleware, które obsługuje konkretne przypadki użycia. Przyjrzyjmy się niektórym przykładom:
1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5// Prosta implementacja limitu żądań w pamięci
6// W produkcji należy użyć Redis lub podobnego rozwiązania
7const REQUESTS_PER_MINUTE = 60;
8const WINDOW_SIZE_MS = 60 * 1000; // 1 minuta
9
10interface RateLimitState {
11 requests: number;
12 timestamp: number;
13}
14
15const ipRequestCounts = new Map<string, RateLimitState>();
16
17export function middleware(request: NextRequest) {
18 // Pobierz adres IP użytkownika
19 const ip = request.ip || 'unknown';
20
21 // Sprawdź, czy to jest API
22 if (!request.nextUrl.pathname.startsWith('/api/')) {
23 return NextResponse.next();
24 }
25
26 // Pobierz aktualny stan limitów dla tego IP
27 const now = Date.now();
28 const state = ipRequestCounts.get(ip) || { requests: 0, timestamp: now };
29
30 // Jeśli upłynęło okno czasowe, zresetuj licznik
31 if (now - state.timestamp > WINDOW_SIZE_MS) {
32 state.requests = 0;
33 state.timestamp = now;
34 }
35
36 // Zwiększ licznik żądań
37 state.requests++;
38 ipRequestCounts.set(ip, state);
39
40 // Jeśli przekroczono limit, zwróć błąd 429 (Too Many Requests)
41 if (state.requests > REQUESTS_PER_MINUTE) {
42 const response = NextResponse.json(
43 { error: 'Rate limit exceeded. Please try again later.' },
44 { status: 429 }
45 );
46
47 // Ustaw nagłówki informujące o limitach
48 response.headers.set('X-RateLimit-Limit', REQUESTS_PER_MINUTE.toString());
49 response.headers.set('X-RateLimit-Remaining', '0');
50 response.headers.set('Retry-After',
51 Math.ceil((state.timestamp + WINDOW_SIZE_MS - now) / 1000).toString()
52 );
53
54 return response;
55 }
56
57 // W granicach limitu - kontynuuj
58 const response = NextResponse.next();
59
60 // Ustaw nagłówki informujące o limitach
61 response.headers.set('X-RateLimit-Limit', REQUESTS_PER_MINUTE.toString());
62 response.headers.set(
63 'X-RateLimit-Remaining',
64 Math.max(0, REQUESTS_PER_MINUTE - state.requests).toString()
65 );
66
67 return response;
68}
69
70export const config = {
71 matcher: ['/api/:path*'],
72};1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5// Lista dozwolonych kodów krajów
6const ALLOWED_COUNTRIES = ['US', 'CA', 'GB', 'DE', 'FR', 'PL'];
7
8// Lista stron objętych restrykcjami geograficznymi
9const GEO_RESTRICTED_PATHS = [
10 '/premium-content',
11 '/members-only',
12];
13
14export function middleware(request: NextRequest) {
15 // Sprawdź, czy ścieżka podlega restrykcjom geograficznym
16 const isGeoRestrictedPath = GEO_RESTRICTED_PATHS.some(path =>
17 request.nextUrl.pathname === path ||
18 request.nextUrl.pathname.startsWith(`${path}/`)
19 );
20
21 if (isGeoRestrictedPath) {
22 // Pobierz informacje o kraju użytkownika z nagłówków (dostępne na platformie Vercel)
23 const country = request.geo?.country || 'Unknown';
24
25 // Jeśli kraj użytkownika nie jest na liście dozwolonych
26 if (!ALLOWED_COUNTRIES.includes(country)) {
27 return NextResponse.redirect(new URL('/geo-restricted', request.url));
28 }
29 }
30
31 return NextResponse.next();
32}
33
34export const config = {
35 matcher: [
36 '/premium-content/:path*',
37 '/members-only/:path*',
38 ],
39};1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function middleware(request: NextRequest) {
6 // Pobierz oryginalną odpowiedź
7 const response = NextResponse.next();
8
9 // Dodaj nagłówki bezpieczeństwa
10
11 // Chroń przed atakami XSS
12 response.headers.set('X-XSS-Protection', '1; mode=block');
13
14 // Zapobiegaj "sniffing" MIME typów
15 response.headers.set('X-Content-Type-Options', 'nosniff');
16
17 // Kontroluj, jak strona może być osadzona w iframe
18 response.headers.set('X-Frame-Options', 'SAMEORIGIN');
19
20 // Content Security Policy - bardziej zaawansowana ochrona przed XSS
21 response.headers.set(
22 'Content-Security-Policy',
23 "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' https://api.example.com;"
24 );
25
26 // HTTP Strict Transport Security - wymusza użycie HTTPS
27 response.headers.set(
28 'Strict-Transport-Security',
29 'max-age=31536000; includeSubDomains; preload'
30 );
31
32 // Permissions Policy - ogłoś, które funkcje przeglądarki są dozwolone
33 response.headers.set(
34 'Permissions-Policy',
35 'camera=(), microphone=(), geolocation=(self), interest-cohort=()'
36 );
37
38 return response;
39}
40
41export const config = {
42 matcher: [
43 '/((?!_next/static|_next/image|images|favicon.ico).*)',
44 ],
45};1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5// Konfiguracja eksperymentów A/B
6const AB_TESTS = {
7 'new-landing-page': {
8 variants: ['control', 'variant-a', 'variant-b'],
9 weights: [0.34, 0.33, 0.33], // Podział ruchu
10 paths: ['/'], // Ścieżki objęte testem
11 cookieName: 'ab-new-landing',
12 },
13 'checkout-flow': {
14 variants: ['original', 'simplified'],
15 weights: [0.5, 0.5],
16 paths: ['/checkout', '/cart'],
17 cookieName: 'ab-checkout',
18 },
19};
20
21export function middleware(request: NextRequest) {
22 const url = request.nextUrl;
23 const response = NextResponse.next();
24
25 // Iteruj przez zdefiniowane testy
26 for (const [testName, test] of Object.entries(AB_TESTS)) {
27 // Sprawdź, czy aktualna ścieżka jest objęta testem
28 const isTestPath = test.paths.some(path =>
29 url.pathname === path || url.pathname.startsWith(`${path}/`)
30 );
31
32 if (isTestPath) {
33 // Sprawdź, czy użytkownik ma już przypisany wariant testu
34 const existingVariant = request.cookies.get(test.cookieName)?.value;
35 let variant = existingVariant;
36
37 // Jeśli użytkownik nie ma przypisanego wariantu, przydziel nowy
38 if (!variant) {
39 variant = assignVariant(test.variants, test.weights);
40
41 // Ustaw ciasteczko z przypisanym wariantem
42 response.cookies.set(test.cookieName, variant, {
43 maxAge: 60 * 60 * 24 * 30, // 30 dni
44 path: '/',
45 });
46 }
47
48 // Dodaj nagłówek z informacją o wariancie testu
49 response.headers.set(`X-AB-Test-${testName}`, variant);
50
51 // Możesz również ustawić rewrite do innej strony dla różnych wariantów
52 if (testName === 'new-landing-page' && url.pathname === '/') {
53 if (variant === 'variant-a') {
54 // Przekierowanie wewnętrzne (niewidoczne dla użytkownika)
55 return NextResponse.rewrite(new URL('/landing-a', request.url));
56 } else if (variant === 'variant-b') {
57 return NextResponse.rewrite(new URL('/landing-b', request.url));
58 }
59 }
60 }
61 }
62
63 return response;
64}
65
66// Funkcja przydzielająca wariant na podstawie wag
67function assignVariant(variants: string[], weights: number[]): string {
68 const randomNum = Math.random();
69 let cumulativeWeight = 0;
70
71 for (let i = 0; i < variants.length; i++) {
72 cumulativeWeight += weights[i];
73 if (randomNum < cumulativeWeight) {
74 return variants[i];
75 }
76 }
77
78 // Fallback na pierwszy wariant
79 return variants[0];
80}
81
82export const config = {
83 matcher: [
84 '/', '/checkout', '/cart',
85 '/landing-a', '/landing-b',
86 ],
87};Testy są bardzo ważne dla sprawdzenia poprawności działania middleware, zwłaszcza że obsługuje ono kluczowe aspekty bezpieczeństwa aplikacji. Oto prosty przykład testów jednostkowych:
1// __tests__/middleware.test.ts
2import { NextRequest } from 'next/server';
3import { middleware } from '../middleware';
4import { getToken } from 'next-auth/jwt';
5
6// Mockujemy getToken z next-auth/jwt
7jest.mock('next-auth/jwt', () => ({
8 getToken: jest.fn(),
9}));
10
11describe('Auth Middleware', () => {
12 // Setup i reset przed każdym testem
13 beforeEach(() => {
14 jest.clearAllMocks();
15 });
16
17 it('should redirect unauthenticated user from protected route to login page', async () => {
18 // Mockuj getToken zwracający null (niezalogowany użytkownik)
19 (getToken as jest.Mock).mockResolvedValueOnce(null);
20
21 // Utwórz testowy request do chronionej ścieżki
22 const req = new NextRequest(new URL('http://localhost:3000/dashboard'));
23
24 // Uruchom middleware
25 const response = await middleware(req);
26
27 // Sprawdź, czy przekierowuje do strony logowania
28 expect(response).not.toBeNull();
29 expect(response.status).toBe(307); // Kod przekierowania
30 expect(response.headers.get('location')).toBe('http://localhost:3000/auth/login?callbackUrl=%2Fdashboard');
31 });
32
33 it('should allow authenticated user to access protected route', async () => {
34 // Mockuj getToken zwracający token (zalogowany użytkownik)
35 (getToken as jest.Mock).mockResolvedValueOnce({
36 name: 'Test User',
37 email: 'test@example.com',
38 role: 'user',
39 permissions: ['content:read']
40 });
41
42 // Utwórz testowy request do chronionej ścieżki
43 const req = new NextRequest(new URL('http://localhost:3000/dashboard'));
44
45 // Uruchom middleware
46 const response = await middleware(req);
47
48 // Sprawdź, czy zezwala na dostęp
49 expect(response).not.toBeNull();
50 expect(response.status).toBe(200);
51 });
52
53 it('should redirect authenticated user from login page to dashboard', async () => {
54 // Mockuj getToken zwracający token (zalogowany użytkownik)
55 (getToken as jest.Mock).mockResolvedValueOnce({
56 name: 'Test User',
57 email: 'test@example.com',
58 });
59
60 // Utwórz testowy request do strony logowania
61 const req = new NextRequest(new URL('http://localhost:3000/auth/login'));
62
63 // Uruchom middleware
64 const response = await middleware(req);
65
66 // Sprawdź, czy przekierowuje do dashboardu
67 expect(response).not.toBeNull();
68 expect(response.status).toBe(307);
69 expect(response.headers.get('location')).toBe('http://localhost:3000/dashboard');
70 });
71
72 it('should redirect user without admin role from admin page', async () => {
73 // Mockuj getToken zwracający token z rolą user
74 (getToken as jest.Mock).mockResolvedValueOnce({
75 name: 'Test User',
76 email: 'test@example.com',
77 role: 'user',
78 permissions: ['content:read']
79 });
80
81 // Utwórz testowy request do strony administratora
82 const req = new NextRequest(new URL('http://localhost:3000/admin/dashboard'));
83
84 // Uruchom middleware
85 const response = await middleware(req);
86
87 // Sprawdź, czy przekierowuje do strony braku uprawnień
88 expect(response).not.toBeNull();
89 expect(response.status).toBe(307);
90 expect(response.headers.get('location')).toBe('http://localhost:3000/auth/unauthorized');
91 });
92});Middleware jest wykonywane przy każdym żądaniu, więc ważne jest, aby było jak najbardziej wydajne:
Debugging middleware może być trudny, ponieważ działa "za kulisami":
console.log() do śledzenia działania middleware.1// Przykład dodawania nagłówków debugowania
2const response = NextResponse.next();
3response.headers.set('X-Debug-Path', request.nextUrl.pathname);
4response.headers.set('X-Debug-Auth', token ? 'Authenticated' : 'Unauthenticated');
5return response;Dla złożonych aplikacji warto rozważyć modularyzację middleware:
1// middleware/auth.ts
2import { NextRequest, NextResponse } from 'next/server';
3import { getToken } from 'next-auth/jwt';
4
5export async function authMiddleware(req: NextRequest) {
6 // Implementacja middleware uwierzytelniania
7 //
8}
9
10// middleware/security.ts
11import { NextRequest, NextResponse } from 'next/server';
12
13export function securityHeadersMiddleware(req: NextRequest) {
14 // Implementacja middleware nagłówków bezpieczeństwa
15 //
16}
17
18// middleware.ts
19import { NextRequest, NextResponse } from 'next/server';
20import { authMiddleware } from './middleware/auth';
21import { securityHeadersMiddleware } from './middleware/security';
22
23export async function middleware(req: NextRequest) {
24 // Sprawdź, czy ścieżka wymaga uwierzytelniania
25 if (req.nextUrl.pathname.startsWith('/dashboard') ||
26 req.nextUrl.pathname.startsWith('/admin')) {
27 return await authMiddleware(req);
28 }
29
30 // Dodaj nagłówki bezpieczeństwa do wszystkich odpowiedzi
31 return securityHeadersMiddleware(req);
32}
33
34export const config = { /* ... */ };Middleware w Next.js oferuje potężny mechanizm do implementacji ochrony tras i wielu innych funkcji na poziomie serwera. W tym module poznaliśmy:
Dobra implementacja middleware jest kluczowym elementem bezpieczeństwa i wydajności aplikacji Next.js. Dzięki technikom przedstawionym w tym module, możesz tworzyć zaawansowane, wielowarstwowe zabezpieczenia dla swoich aplikacji.
W następnym module przejdziemy do implementacji Context Provider dla uwierzytelniania, co umożliwi łatwy dostęp do danych uwierzytelniania w całej aplikacji.