Tokenbaserad autentisering är en utbredd teknik för att skydda webbapplikationer och mobilapplikationer mot otillåten åtkomst. I Next.js kan du dra nytta av autentiseringsfunktioner som tillhandahålls av Next-auth.
Ett alternativ är att skapa ett eget tokenbaserat autentiseringssystem med JSON Web Tokens (JWT). Detta ger dig större kontroll över autentiseringslogiken och gör att du kan anpassa systemet exakt efter projektets behov.
Konfigurera ett Next.js-projekt
Börja med att installera Next.js genom att köra följande kommando i din terminal:
npx create-next-app@latest next-auth-jwt --experimental-app
Denna guide använder Next.js 13 som inkluderar appkatalogen.
Installera sedan följande beroenden i ditt projekt med npm (Node Package Manager):
npm install jose universal-cookie
Jose är en JavaScript-modul som erbjuder verktyg för att hantera JSON Web Tokens, medan universal-cookie förenklar hanteringen av webbläsarcookies i både klient- och servermiljöer.
Skapa användargränssnittet för inloggningsformuläret
I katalogen src/app, skapa en ny mapp med namnet ”login”. Lägg till en fil vid namn page.js i mappen och kopiera följande kod:
"use client"; import { useRouter } from "next/navigation"; export default function LoginPage() { return ( <form onSubmit={handleSubmit}> <label> Username: <input type="text" name="username" /> </label> <label> Password: <input type="password" name="password" /> </label> <button type="submit">Login</button> </form> ); }
Koden ovan skapar en komponent för inloggningssidan som renderar ett enkelt inloggningsformulär i webbläsaren där användare kan ange användarnamn och lösenord.
Direktivet ”use client” i koden definierar en gräns mellan server- och klientkod i appkatalogen.
I det här fallet indikerar det att koden på inloggningssidan, särskilt funktionen `handleSubmit`, endast körs på klienten. Annars kommer Next.js att ge ett fel.
Låt oss nu definiera koden för `handleSubmit`-funktionen. Lägg till följande kod inuti den funktionella komponenten:
const router = useRouter(); const handleSubmit = async (event) => { event.preventDefault(); const formData = new FormData(event.target); const username = formData.get("username"); const password = formData.get("password"); const res = await fetch("/api/login", { method: "POST", body: JSON.stringify({ username, password }), }); const { success } = await res.json(); if (success) { router.push("/protected"); router.refresh(); } else { alert("Login failed"); } };
Denna funktion, som hanterar inloggningsautentiseringen, hämtar användaruppgifterna från formuläret. Därefter skickas en POST-förfrågan till en API-slutpunkt som i sin tur validerar användarinformationen.
Om uppgifterna är korrekta, vilket innebär en lyckad inloggning, returnerar API:et en status för lyckad inloggning. Då använder hanteringsfunktionen Next.js-routern för att navigera användaren till en förutbestämd webbadress, i detta fall den skyddade rutten.
Definiera Login API Endpoint
Skapa en ny mapp i src/app-katalogen som du döper till ”api”. Lägg till en fil som heter login/route.js i mappen och lägg in följande kod:
import { SignJWT } from "jose"; import { NextResponse } from "next/server"; import { getJwtSecretKey } from "@/libs/auth"; export async function POST(request) { const body = await request.json(); if (body.username === "admin" && body.password === "admin") { const token = await new SignJWT({ username: body.username, }) .setProtectedHeader({ alg: "HS256" }) .setIssuedAt() .setExpirationTime("30s") .sign(getJwtSecretKey()); const response = NextResponse.json( { success: true }, { status: 200, headers: { "content-type": "application/json" } } ); response.cookies.set({ name: "token", value: token, path: "https://www.makeuseof.com/", }); return response; } return NextResponse.json({ success: false }); }
Huvudsyftet med detta API är att bekräfta inloggningsuppgifterna som skickas i POST-förfrågningar med hjälp av simulerade data.
Efter godkänd validering genereras en krypterad JWT-token som är kopplad till de autentiserade användaruppgifterna. Slutligen skickar API:et en bekräftelse till klienten, med token inkluderad i svarscookies. Om autentiseringen misslyckas returneras ett felmeddelande.
Implementera tokenverifieringslogik
Det första steget i tokenautentisering är att generera token efter en lyckad inloggning. Nästa steg är att implementera logiken för tokenverifiering.
I huvudsak kommer du att använda funktionen `jwtVerify`, som tillhandahålls av Jose-modulen, för att verifiera de JWT-tokens som skickas med efterföljande HTTP-förfrågningar.
Skapa en ny fil som heter `libs/auth.js` i `src`-katalogen och lägg till följande kod:
import { jwtVerify } from "jose"; export function getJwtSecretKey() { const secret = process.env.NEXT_PUBLIC_JWT_SECRET_KEY; if (!secret) { throw new Error("JWT Secret key is not matched"); } return new TextEncoder().encode(secret); } export async function verifyJwtToken(token) { try { const { payload } = await jwtVerify(token, getJwtSecretKey()); return payload; } catch (error) { return null; } }
Den hemliga nyckeln används för att signera och verifiera tokens. Genom att jämföra den avkodade tokensignaturen med den förväntade signaturen kan servern effektivt bekräfta att den angivna token är giltig och slutligen godkänna användarnas förfrågningar.
Skapa en .env-fil i rotkatalogen och lägg till en unik hemlig nyckel enligt följande:
NEXT_PUBLIC_JWT_SECRET_KEY=din_hemliga_nyckel
Skapa en skyddad rutt
Nu måste du skapa en rutt som endast autentiserade användare kan komma åt. För att göra det, skapa en ny fil vid namn `protected/page.js` i `src/app`-katalogen. Lägg till följande kod i filen:
export default function ProtectedPage() { return <h1>Mycket skyddad sida</h1>; }
Skapa en krok för att hantera autentiseringstillståndet
Skapa en ny mapp i src-katalogen som du döper till ”hooks”. Lägg till en ny fil vid namn useAuth/index.js i mappen och inkludera följande kod:
"use client" ; import React from "react"; import Cookies from "universal-cookie"; import { verifyJwtToken } from "@/libs/auth"; export function useAuth() { const [auth, setAuth] = React.useState(null); const getVerifiedtoken = async () => { const cookies = new Cookies(); const token = cookies.get("token") ?? null; const verifiedToken = await verifyJwtToken(token); setAuth(verifiedToken); }; React.useEffect(() => { getVerifiedtoken(); }, []); return auth; }
Denna krok hanterar autentiseringstillståndet på klientsidan. Den hämtar och verifierar giltigheten av JWT-token som finns i cookies med hjälp av funktionen `verifyJwtToken` och sätter sedan in den autentiserade användarinformationen i autentiseringstillståndet.
Genom att göra detta kan andra komponenter komma åt och använda den autentiserade användarens information. Detta är användbart i scenarier som att göra UI-uppdateringar baserat på autentiseringsstatus, göra efterföljande API-förfrågningar eller rendera olika innehåll baserat på användarroller.
I det här fallet kommer du att använda kroken för att visa olika innehåll på startsidan beroende på användarens autentiseringsstatus.
Ett alternativt tillvägagångssätt som du kan överväga är att hantera tillstånd med Redux Toolkit eller använda ett tillståndshanteringsverktyg som Jotai. Detta tillvägagångssätt säkerställer att komponenter kan få global åtkomst till autentiseringstillståndet eller annat definierat tillstånd.
Gå vidare och öppna filen app/page.js, ta bort koden från Next.js och lägg till följande kod.
"use client" ; import { useAuth } from "@/hooks/useAuth"; import Link from "next/link"; export default function Home() { const auth = useAuth(); return <> <h1>Offentlig startsida</h1> <header> <nav> {auth ? ( <p>inloggad</p> ) : ( <Link href="https://wilku.top/login">Logga in</Link> )} </nav> </header> </> }
Koden ovan använder kroken `useAuth` för att hantera autentiseringstillståndet. Detta gör att den visar en offentlig startsida med en länk till inloggningssidan när användaren inte är autentiserad. För en autentiserad användare visas i stället ett stycke med text.
Lägg till ett mellanprogram för att upprätthålla behörig åtkomst till skyddade rutter
Skapa en ny fil som heter middleware.js i src-katalogen och lägg till följande kod:
import { NextResponse } from "next/server"; import { verifyJwtToken } from "@/libs/auth"; const AUTH_PAGES = ["https://wilku.top/login"]; const isAuthPages = (url) => AUTH_PAGES.some((page) => page.startsWith(url)); export async function middleware(request) { const { url, nextUrl, cookies } = request; const { value: token } = cookies.get("token") ?? { value: null }; const hasVerifiedToken = token && (await verifyJwtToken(token)); const isAuthPageRequested = isAuthPages(nextUrl.pathname); if (isAuthPageRequested) { if (!hasVerifiedToken) { const response = NextResponse.next(); response.cookies.delete("token"); return response; } const response = NextResponse.redirect(new URL(`/`, url)); return response; } if (!hasVerifiedToken) { const searchParams = new URLSearchParams(nextUrl.searchParams); searchParams.set("next", nextUrl.pathname); const response = NextResponse.redirect( new URL(`/login?${searchParams}`, url) ); response.cookies.delete("token"); return response; } return NextResponse.next(); } export const config = { matcher: ["https://wilku.top/login", "/protected/:path*"] };
Denna mellanprogramskod fungerar som en väktare. Den kontrollerar att användare som försöker komma åt skyddade sidor är autentiserade och behöriga. Annars omdirigeras obehöriga användare till inloggningssidan.
Säkra Next.js-applikationer
Tokenbaserad autentisering är en kraftfull säkerhetsmekanism, men det är inte den enda strategin som finns tillgänglig för att skydda dina applikationer från otillåten åtkomst.
För att skydda applikationer mot det ständigt föränderliga cybersäkerhetslandskapet är det viktigt att ha en omfattande säkerhetsstrategi som helhetsmässigt hanterar potentiella svagheter och sårbarheter för att säkerställa ett fullständigt skydd.