Johdanto
Next.js 16 toi mukanaan yhden niistä muutoksista, joka saa kehittäjät heti tarkistamaan projektinsa: tuttu middleware.ts-tiedosto korvattiin uudella proxy.ts-tiedostolla. Ja ei, kyseessä ei ole pelkkä nimenmuutos — vaikka se ensi silmäyksellä siltä saattaa näyttää.
Se heijastaa syvempää arkkitehtuurista linjausta siitä, miten Next.js käsittelee pyyntöjä ennen reititystä.
Mutta miksi tämä muutos oikeasti tehtiin? Ja miten rakennat turvallisen, suorituskykyisen autentikoinnin ja reittien suojauksen uudella proxy-mallilla? Tässä oppaassa käymme läpi kaiken käytännön koodiesimerkein — mukaan lukien sen kriittisen CVE-2025-29927-haavoittuvuuden, jonka jokaisen Next.js-kehittäjän tulisi tuntea.
Miksi middleware nimettiin uudelleen proxyksi?
Termi "middleware" aiheutti jatkuvasti sekaannusta. Oikeasti aika paljon sekaannusta. Express.js-taustaiset kehittäjät odottivat sen toimivan samalla tavalla kuin Express-middlewaren — eli pystyvänsä ketjuttamaan useita middleware-funktioita tai käsittelemään pyynnön bodya vapaasti. Next.js:n middleware ei kuitenkaan koskaan toiminut näin.
Termi "proxy" kuvaa paljon paremmin sitä, mitä tiedosto oikeasti tekee: se toimii verkkokerroksena sovelluksen edessä. Se voi uudelleenohjata, kirjoittaa URL-osoitteita uudelleen, muokata otsakkeita ja vastata suoraan — mutta se ei ole osa sovelluksen renderöintilogiikkaa.
Toinen iso muutos on ajonaikaympäristö. Vanha middleware.ts ajettiin oletuksena Edge Runtimessa, mikä rajoitti käytettävissä olevia Node.js-API:ita merkittävästi. Uusi proxy.ts sen sijaan käyttää Node.js-ajonaikaa, eli käytössäsi on täysi Node.js-ympäristö — tiedostojärjestelmä, kryptokirjastot ja kaikki muu. Tämä yksinään on iso parannus.
Siirtymä middleware.ts:stä proxy.ts:ään
Hyvä uutinen: siirtymä on yllättävän suoraviivainen. Perusmuutos vaatii käytännössä kaksi asiaa — tiedoston uudelleennimeämisen ja funktionimen muuttamisen. Siinä se.
Automaattinen siirtymä codemodilla
Next.js tarjoaa valmiin codemod-työkalun, joka hoitaa siirtymän automaattisesti:
npx @next/codemod@latest middleware-to-proxy .
Codemod nimeää tiedoston uudelleen ja muuttaa funktion nimen middleware:sta proxy:ksi. Asetukset ja logiikka säilyvät ennallaan. Rehellisesti sanottuna, tämä on yksi nopeimmista siirtymistä mitä Next.js on tarjonnut.
Manuaalinen siirtymä
Jos haluat tehdä muutoksen käsin (tai haluat ymmärtää mitä tapahtuu konepellin alla), tässä konkreettinen esimerkki:
Vanha tapa (middleware.ts):
// middleware.ts — VANHENTUNUT
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
const token = request.cookies.get("session-token");
if (!token && request.nextUrl.pathname.startsWith("/dashboard")) {
return NextResponse.redirect(new URL("/login", request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard/:path*"],
};
Uusi tapa (proxy.ts):
// proxy.ts — Next.js 16+
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function proxy(request: NextRequest) {
const token = request.cookies.get("session-token");
if (!token && request.nextUrl.pathname.startsWith("/dashboard")) {
return NextResponse.redirect(new URL("/login", request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard/:path*"],
};
Huomasitko? Ainoa muutos on funktion nimi. Logiikka, config-objekti ja matcher toimivat identtisesti. Vanha middleware.ts toimii edelleen, mutta tuottaa varoituksen — ja se poistetaan tulevissa versioissa. Joten kannattaa siirtyä mieluummin nyt kuin myöhemmin.
Autentikoinnin toteuttaminen proxy.ts:llä
Reittien suojaaminen on ylivoimaisesti yleisin proxy-käyttötapaus. Käydään läpi kolme eri lähestymistapaa — yksinkertaisesta evästepohjaisesta tarkistuksesta JWT-validointiin ja roolipohjaiseen pääsynhallintaan.
Evästepohjainen sessiotarkistus
Yksinkertaisin malli on tarkistaa, onko käyttäjällä sessioevästettä. Ei mitään hienoa, mutta toimii monessa tilanteessa yllättävän hyvin:
// proxy.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
const protectedRoutes = ["/dashboard", "/profile", "/settings"];
const publicRoutes = ["/login", "/register", "/"];
export function proxy(request: NextRequest) {
const { pathname } = request.nextUrl;
const sessionToken = request.cookies.get("session-token")?.value;
// Tarkista, onko reitti suojattu
const isProtected = protectedRoutes.some((route) =>
pathname.startsWith(route)
);
if (isProtected && !sessionToken) {
const loginUrl = new URL("/login", request.url);
loginUrl.searchParams.set("callbackUrl", pathname);
return NextResponse.redirect(loginUrl);
}
// Estä kirjautuneiden käyttäjien pääsy kirjautumissivulle
const isPublicAuth = publicRoutes.includes(pathname);
if (isPublicAuth && sessionToken) {
return NextResponse.redirect(new URL("/dashboard", request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico|api/public).*)"],
};
Tässä on pari tärkeää yksityiskohtaa. callbackUrl-parametri tallennetaan, jotta käyttäjä voidaan ohjata takaisin alkuperäiselle sivulle kirjautumisen jälkeen — pieni yksityiskohta, mutta käyttäjäkokemuksen kannalta todella merkittävä. Matcher-konfiguraatio jättää staattiset tiedostot ja julkiset API-reitit proxyn ulkopuolelle, mikä pitää asiat suorituskykyisinä.
JWT-tokenin validointi
Nyt päästään mielenkiintoisempaan osioon. Koska proxy.ts käyttää Node.js-ajonaikaa, voit käyttää täyttä kryptografiaa suoraan — toisin kuin vanhassa Edge Runtime -ympäristössä:
// proxy.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { jwtVerify } from "jose";
const JWT_SECRET = new TextEncoder().encode(
process.env.JWT_SECRET!
);
async function verifyToken(token: string) {
try {
const { payload } = await jwtVerify(token, JWT_SECRET);
return payload;
} catch {
return null;
}
}
export async function proxy(request: NextRequest) {
const token = request.cookies.get("auth-token")?.value;
if (!token) {
return NextResponse.redirect(new URL("/login", request.url));
}
const payload = await verifyToken(token);
if (!payload) {
// Token on vanhentunut tai virheellinen
const response = NextResponse.redirect(
new URL("/login", request.url)
);
response.cookies.delete("auth-token");
return response;
}
// Välitä käyttäjätiedot sovellukselle otsakkeilla
const requestHeaders = new Headers(request.headers);
requestHeaders.set("x-user-id", payload.sub as string);
requestHeaders.set("x-user-role", payload.role as string);
return NextResponse.next({
request: { headers: requestHeaders },
});
}
export const config = {
matcher: ["/dashboard/:path*", "/api/protected/:path*"],
};
Tärkeä huomio tässä: käyttäjätiedot välitetään sovellukselle HTTP-otsakkeilla. Proxy toimii verkkokerroksessa erillään renderöintikoodista, joten tiedon välitys tapahtuu otsakkeilla, evästeillä tai URL-uudelleenkirjoituksilla. Tämä voi tuntua aluksi vieraalta, mutta siihen tottuu nopeasti.
Roolipohjainen pääsynhallinta
JWT-payload sisältää usein käyttäjän roolin, ja sitä kannattaa hyödyntää. Tässä esimerkki, joka rajoittaa admin-sivut vain ylläpitäjille:
// proxy.ts — roolipohjainen suojaus
const roleRoutes: Record<string, string[]> = {
"/admin": ["admin"],
"/dashboard": ["admin", "user"],
"/editor": ["admin", "editor"],
};
export async function proxy(request: NextRequest) {
const token = request.cookies.get("auth-token")?.value;
const payload = token ? await verifyToken(token) : null;
if (!payload) {
return NextResponse.redirect(new URL("/login", request.url));
}
const { pathname } = request.nextUrl;
const userRole = payload.role as string;
for (const [route, allowedRoles] of Object.entries(roleRoutes)) {
if (pathname.startsWith(route) && !allowedRoles.includes(userRole)) {
return NextResponse.redirect(new URL("/unauthorized", request.url));
}
}
return NextResponse.next();
}
Tietoturvan vahvistaminen proxylla
Turvallisuusotsakkeet
Proxy on erinomainen paikka lisätä turvallisuusotsakkeet kaikkiin vastauksiin. Tämä on yksi niistä asioista, jotka kannattaa tehdä heti projektin alussa — myöhemmin se helposti unohtuu:
export function proxy(request: NextRequest) {
const response = NextResponse.next();
response.headers.set("X-Frame-Options", "DENY");
response.headers.set("X-Content-Type-Options", "nosniff");
response.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
response.headers.set("X-XSS-Protection", "1; mode=block");
response.headers.set(
"Strict-Transport-Security",
"max-age=63072000; includeSubDomains; preload"
);
return response;
}
Pyyntöjen rajoittaminen (Rate Limiting)
API-reittien suojaaminen liiallisilta pyynnöiltä on kriittistä tuotantoympäristöissä. Jokainen, joka on joskus nähnyt API-laskun räjähtävän käsiin, tietää mistä puhun. Tässä yksinkertainen muistipohjainen ratkaisu:
// proxy.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
const rateLimitMap = new Map<string, { count: number; lastReset: number }>();
const WINDOW_MS = 60 * 1000; // 1 minuutti
const MAX_REQUESTS = 60;
function checkRateLimit(ip: string): boolean {
const now = Date.now();
const record = rateLimitMap.get(ip);
if (!record || now - record.lastReset > WINDOW_MS) {
rateLimitMap.set(ip, { count: 1, lastReset: now });
return true;
}
if (record.count >= MAX_REQUESTS) {
return false;
}
record.count++;
return true;
}
export function proxy(request: NextRequest) {
if (request.nextUrl.pathname.startsWith("/api/")) {
const ip = request.headers.get("x-forwarded-for") ?? "unknown";
if (!checkRateLimit(ip)) {
return NextResponse.json(
{ error: "Liian monta pyyntöä. Yritä myöhemmin uudelleen." },
{
status: 429,
headers: { "Retry-After": "60" },
}
);
}
}
return NextResponse.next();
}
export const config = {
matcher: ["/api/:path*"],
};
Pieni varoitus: tuotantoympäristössä kannattaa käyttää Upstash Redis -palvelua tämän muistipohjaisen ratkaisun sijaan. Serverless-ympäristössä muistipohjainen Map ei säily instanssien välillä, joten rate limiting voi vuotaa. Kehitysympäristössä tämä kuitenkin toimii mainiosti.
CVE-2025-29927: Tärkeät opetukset
Tämä on se osio, johon kannattaa oikeasti keskittyä.
Maaliskuussa 2025 paljastui kriittinen haavoittuvuus, joka sai CVSS-pisteytykseksi 9.1 — eli erittäin vakava. CVE-2025-29927 osoitti, miten hyökkääjä pystyi ohittamaan middlewaren kokonaan yksinkertaisella HTTP-otsakemanipulaatiolla.
Miten hyökkäys toimi?
Next.js käytti sisäistä x-middleware-subrequest-otsaketta estääkseen rekursiiviset pyynnöt aiheuttamasta ikuisia silmukoita. Ongelma? Hyökkääjä pystyi lisäämään tämän otsakkeen ulkoiseen pyyntöön ja ohittamaan middlewaren kokonaan — mukaan lukien kaikki autentikointitarkistukset.
Käytännössä tämä tarkoitti sitä, että suojatut reitit olivat täysin avoimia kenelle tahansa, joka tiesi lisätä oikean otsakkeen pyyntöön. Aika pelottava ajatus.
Mitä tästä opittiin?
Tämä haavoittuvuus opettaa kaksi kriittistä asiaa:
- Syvyyssuuntainen puolustus (Defense-in-Depth): Proxy ei saa koskaan olla ainoa autentikointikerros. Tarkista käyttäjän oikeudet myös Server Componenteissa, Server Actioneissa ja API-reittihandlereissa — mahdollisimman lähellä datalähdettä. Tämä ei ole ylimääräistä varovaisuutta, vaan perusedellytys.
- Pidä Next.js ajan tasalla: Päivitä aina vähintään versioon 15.2.3 (tai 14.2.25, 13.5.9, 12.3.5 vanhemmissa versioissa). Vercel-alustalla haavoittuvuus korjattiin automaattisesti, mutta itse hostatuissa ympäristöissä korjaus vaati manuaalisen päivityksen.
Syvyyssuuntainen puolustus käytännössä
Näin toteutat autentikoinnin useassa kerroksessa — tämä on se malli, jota suosittelen kaikille Next.js-projekteille:
// app/dashboard/page.tsx — Server Component
import { cookies } from "next/headers";
import { verifySession } from "@/lib/auth";
import { redirect } from "next/navigation";
export default async function DashboardPage() {
const cookieStore = await cookies();
const token = cookieStore.get("auth-token")?.value;
// Tarkista autentikointi MYÖS täällä — älä luota pelkästään proxyyn
const session = token ? await verifySession(token) : null;
if (!session) {
redirect("/login");
}
return (
<div>
<h1>Tervetuloa, {session.user.name}</h1>
{/* Dashboard-sisältö */}
</div>
);
}
// app/actions/posts.ts — Server Action
"use server";
import { cookies } from "next/headers";
import { verifySession } from "@/lib/auth";
export async function createPost(formData: FormData) {
const cookieStore = await cookies();
const token = cookieStore.get("auth-token")?.value;
const session = token ? await verifySession(token) : null;
if (!session) {
throw new Error("Unauthorized");
}
// Suorita toimenpide vasta autentikoinnin jälkeen
const title = formData.get("title") as string;
// ...tietokantatoiminnot
}
Proxy-tiedoston parhaat käytännöt
Näiden periaatteiden noudattaminen säästää sinut monelta päänsäryltä pitkällä aikavälillä:
- Pidä logiikka kevyenä. Proxy ajetaan jokaisessa pyynnössä, joten vältä raskaita tietokantakyselyitä tai ulkoisia API-kutsuja. Tarkista sessio evästeestä optimistisesti ja jätä täysi validointi renderöintikerrokseen.
- Käytä matcheria rajaamaan ajoa. Älä aja proxyä jokaiselle pyynnölle — rajaa se vain suojattuihin reitteihin. Jätä staattiset resurssit, kuvat ja favicon aina matcherin ulkopuolelle. Tämä tekee yllättävän ison eron suorituskyvyssä.
- Vältä ikuisia uudelleenohjakussilmukoita. Tarkista aina, onko pyyntö jo kohdesivulla, ennen kuin uudelleenohjaat. Olen nähnyt tämän bugin useammin kuin haluaisin myöntää — login-sivulle uudelleenohjaus voi aiheuttaa ikuisen silmukan jos et ole tarkkana.
- Välitä data otsakkeilla. Proxy on erillinen kerros — käytä
request.headers-otsakeita tiedon välittämiseen renderöintikoodille. Älä yritä asettaa muuttujia tai jakaa tilaa suoraan. - Yksi tiedosto riittää. Next.js tukee vain yhtä
proxy.ts-tiedostoa projektissa. Organisoi logiikka importtaamalla apufunktioita erillisistä moduuleista sen sijaan, että yrität luoda useita proxy-tiedostoja.
Usein kysytyt kysymykset
Toimiiko vanha middleware.ts vielä Next.js 16:ssa?
Kyllä, middleware.ts toimii edelleen, mutta se tuottaa varoituksen konsoliin. Next.js-tiimi on ilmoittanut, että se poistetaan tulevissa versioissa. Suosittelen siirtymistä proxy.ts-tiedostoon mahdollisimman pian — codemod-työkalu tekee siirtymästä käytännössä automaattisen.
Miksi proxy.ts käyttää Node.js-ajonaikaa Edge Runtimen sijaan?
Next.js 16:ssa proxy.ts siirtyi Node.js-ajonaikaan, koska Edge Runtime rajoitti merkittävästi käytettävissä olevia API:ita. Tiedostojärjestelmää, tiettyjä kryptokirjastoja ja monia npm-paketteja ei yksinkertaisesti voinut käyttää. Node.js-ajonaikaympäristö poistaa nämä rajoitukset ja tekee proxysta huomattavasti monipuolisemman.
Riittääkö proxy.ts yksinään autentikointiin?
Ei, ja tämä on tärkeä pointti. Proxy on ensimmäinen puolustuslinja, mutta se ei saa olla ainoa. CVE-2025-29927 osoitti konkreettisesti, että middleware (ja sitä kautta proxy) voidaan ohittaa. Tarkista autentikointi aina myös Server Componenteissa, Server Actioneissa ja API-reittihandlereissa — mahdollisimman lähellä datalähdettä.
Miten välitän käyttäjätietoja proxysta sovellukseen?
Proxy toimii verkkokerroksessa erillään renderöintikoodista. Tiedon välittämiseen käytetään HTTP-otsakkeita, evästeitä, URL-uudelleenkirjoituksia tai uudelleenohjauksia. Yleisin (ja mielestäni selkein) tapa on asettaa mukautettuja otsakkeita NextResponse.next({ request: { headers } }) -metodilla.
Voinko käyttää useita proxy-tiedostoja eri reiteille?
Ei. Next.js tukee vain yhtä proxy.ts-tiedostoa projektissa, joka sijaitsee juuressa tai src-kansiossa. Voit kuitenkin organisoida logiikan importtaamalla apufunktioita erillisistä moduuleista ja käyttämällä matcher-konfiguraatiota reittien rajaamiseen.