Autentificarea bazată pe jetoane reprezintă o metodă frecvent utilizată pentru a securiza aplicațiile web și mobile împotriva accesului neautorizat. În contextul Next.js, poți utiliza mecanismele de autentificare oferite de Next-auth.
O alternativă este dezvoltarea unui sistem de autentificare personalizat, folosind jetoane web JSON (JWT). Această abordare îți oferă un control sporit asupra logicii de autentificare, permițându-ți să adaptezi sistemul la cerințele specifice ale proiectului tău.
Inițializarea unui proiect Next.js
Pentru început, instalează Next.js folosind următoarea comandă în terminal:
npx create-next-app@latest next-auth-jwt --experimental-app
Acest ghid va folosi Next.js versiunea 13, care include directorul „app”.
Ulterior, adaugă dependențele necesare în proiect prin intermediul npm, managerul de pachete Node:
npm install jose universal-cookie
Jose este un modul JavaScript ce oferă instrumente pentru manipularea jetoanelor web JSON, iar universal-cookie facilitează lucrul cu cookie-urile browserului, atât în medii client, cât și server.
Crearea interfeței pentru formularul de autentificare
Accesează directorul `src/app`, creează un nou folder numit `login`. În interiorul acestui folder, adaugă un fișier `page.js` și include următorul cod:
"use client";
import { useRouter } from "next/navigation";export default function LoginPage() {
return (
<form onSubmit={handleSubmit}>
<label>
Nume utilizator:
<input type="text" name="username" />
</label>
<label>
Parola:
<input type="password" name="password" />
</label>
<button type="submit">Autentificare</button>
</form>
);
}
Acest cod implementează o componentă functională pentru pagina de autentificare, care afișează un formular simplu ce permite utilizatorilor să introducă numele de utilizator și parola.
Declarația `”use client”` specifică o graniță între codul destinat exclusiv serverului și cel executat pe client în cadrul directorului „app”.
În acest caz, este folosită pentru a defini că funcția `handleSubmit` din pagina de autentificare va fi executată exclusiv pe client, evitând astfel erorile generate de Next.js.
Acum, să definim logica pentru funcția `handleSubmit`. În interiorul componentei, adaugă următorul cod:
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("Autentificare eșuată");
}
};
Pentru a gestiona logica de autentificare, această funcție preia datele de autentificare din formular. Apoi, trimite o cerere POST către un endpoint API care procesează datele pentru verificare.
Dacă datele de autentificare sunt valide, API-ul returnează un răspuns de succes. Funcția folosește apoi `router`-ul Next.js pentru a redirecționa utilizatorul către un URL specific, în acest caz ruta protejată.
Definirea endpoint-ului API pentru autentificare
În directorul `src/app`, creează un folder nou numit `api`. În acest folder, adaugă un nou fișier `login/route.js` și include următorul cod:
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 });
}
Acest API are rolul de a verifica datele de autentificare primite prin cereri POST, folosind date simulate.
Dacă datele sunt valide, generează un token JWT criptat, asociat cu detaliile utilizatorului autentificat. Trimite apoi un răspuns de succes clientului, incluzând tokenul în cookie-urile de răspuns. În caz contrar, returnează un răspuns de eșec.
Implementarea logicii de verificare a jetoanelor
Primul pas în autentificarea bazată pe jetoane este generarea acestuia după autentificarea cu succes. Următorul pas este implementarea logicii pentru verificarea jetonului.
În esență, funcția `jwtVerify` din modulul Jose va fi folosită pentru a verifica jetoanele JWT transmise prin cereri HTTP ulterioare.
În directorul `src`, creează un nou fișier `libs/auth.js` și adaugă următorul cod:
import { jwtVerify } from "jose";export function getJwtSecretKey() {
const secret = process.env.NEXT_PUBLIC_JWT_SECRET_KEY;
if (!secret) {
throw new Error("Cheia secretă JWT nu a fost găsită");
}
return new TextEncoder().encode(secret);
}
export async function verifyJwtToken(token) {
try {
const { payload } = await jwtVerify(token, getJwtSecretKey());
return payload;
} catch (error) {
return null;
}
}
Cheia secretă este folosită pentru semnarea și verificarea jetoanelor. Comparând semnătura jetonului decodificat cu semnătura așteptată, serverul poate verifica dacă jetonul primit este valid și poate autoriza cererile utilizatorilor.
Creează un fișier `.env` în directorul rădăcină și adaugă o cheie secretă unică, după cum urmează:
NEXT_PUBLIC_JWT_SECRET_KEY=cheia_ta_secreta
Crearea unei rute protejate
Acum, trebuie să creezi o rută la care pot accesa doar utilizatorii autentificați. Pentru aceasta, creează un nou fișier `protected/page.js` în directorul `src/app`. În interiorul acestui fișier, adaugă următorul cod:
export default function ProtectedPage() {
return <h1>Pagină strict protejată</h1>;
}
Crearea unui hook pentru gestionarea stării de autentificare
Creează un nou folder în directorul `src`, numit `hooks`. În acest folder, adaugă un nou fișier `useAuth/index.js` și include următorul cod:
"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;
}
Acest hook gestionează starea de autentificare pe partea clientului. El preia și verifică validitatea jetonului JWT din cookie-uri folosind funcția `verifyJwtToken`, apoi setează detaliile utilizatorului autentificat în starea de autentificare.
În acest fel, permite altor componente să acceseze și să utilizeze informațiile utilizatorului autentificat. Acest lucru este crucial pentru acțiuni ca actualizarea interfeței în funcție de starea de autentificare, efectuarea de cereri API ulterioare sau redarea unui conținut diferit bazat pe rolurile utilizatorului.
În acest caz, hook-ul va fi folosit pentru a reda conținut diferit pe ruta principală, în funcție de starea de autentificare a utilizatorului.
O altă abordare pe care o poți lua în considerare este gestionarea stării folosind Redux Toolkit sau un instrument de gestionare a stării ca Jotai. Această metodă garantează că componentele pot avea acces global la starea de autentificare sau la orice altă stare definită.
Deschide fișierul `app/page.js`, șterge codul boilerplate Next.js și adaugă următorul cod:
"use client" ;import { useAuth } from "@/hooks/useAuth";
import Link from "next/link";
export default function Home() {
const auth = useAuth();
return <>
<h1>Pagină principală publică</h1>
<header>
<nav>
{auth ? (
<p>Autentificat</p>
) : (
<Link href="https://wilku.top/login">Autentificare</Link>
)}
</nav>
</header>
</>
}
Codul de mai sus utilizează hook-ul `useAuth` pentru a gestiona starea de autentificare. În acest fel, redă condiționat o pagină principală publică cu un link către pagina de autentificare când utilizatorul nu este autentificat și afișează un paragraf pentru un utilizator autentificat.
Adăugarea unui middleware pentru impunerea accesului autorizat la rutele protejate
În directorul `src`, creează un nou fișier `middleware.js` și adaugă următorul cod:
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*"] };
Acest cod middleware acționează ca un paznic. El verifică dacă, atunci când utilizatorii încearcă să acceseze pagini protejate, aceștia sunt autentificați și autorizați să acceseze rutele, redirecționând utilizatorii neautorizați către pagina de autentificare.
Securizarea aplicațiilor Next.js
Autentificarea prin jetoane este un mecanism de securitate eficient. Totuși, nu este singura metodă disponibilă pentru a proteja aplicațiile de acces neautorizat.
Pentru a consolida securitatea aplicațiilor împotriva amenințărilor cibernetice dinamice, este esențial să adopți o abordare complexă de securitate, care să abordeze în mod holistic lacunele și vulnerabilitățile potențiale, garantând o protecție completă.