Аутентифікація з використанням токенів є поширеним методом, який забезпечує захист веб-застосунків та мобільних додатків від несанкціонованого проникнення. У середовищі Next.js, для реалізації автентифікації можна використовувати функціонал, який пропонує Next-auth.
Також, можна створити власну систему аутентифікації на основі токенів, використовуючи JSON Web Tokens (JWT). Такий підхід дозволяє отримати більш гнучкий контроль над логікою автентифікації, а саме, можливість адаптувати систему під індивідуальні вимоги проекту.
Налаштування проекту Next.js
Для початку, встановіть Next.js, використовуючи команду в терміналі:
npx create-next-app@latest next-auth-jwt --experimental-app
У цьому керівництві ми будемо використовувати Next.js версії 13, який включає каталог додатків.
Далі, встановіть необхідні залежності у ваш проект за допомогою npm, менеджера пакетів Node.
npm install jose universal-cookie
Jose — це JavaScript модуль, що надає інструменти для роботи з JSON Web Tokens, а universal-cookie дозволяє легко керувати файлами cookie браузера як на стороні клієнта, так і на сервері.
Створення інтерфейсу користувача для форми входу
Відкрийте каталог src/app, створіть нову папку з назвою login. У цій папці створіть файл page.js та додайте наступний код:
"use client";
import { useRouter } from "next/navigation";export default function LoginPage() {
return (
<form onSubmit={handleSubmit}>
<label>
Ім'я користувача:
<input type="text" name="username" />
</label>
<label>
Пароль:
<input type="password" name="password" />
</label>
<button type="submit">Увійти</button>
</form>
);
}
Цей код створює функціональний компонент сторінки входу, де користувачі можуть ввести ім’я користувача та пароль.
Інструкція “use client” вказує, що код виконується на стороні клієнта, а не на сервері. Це необхідно, оскільки функція handleSubmit має виконуватися на клієнті, інакше Next.js покаже помилку.
Тепер давайте додамо код для функції handleSubmit. Вставте наступний блок коду в компонент:
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("Помилка входу");
}
};
Функція обробляє введені користувачем дані з форми. Вона надсилає POST-запит до API, передаючи дані користувача для їх перевірки. Якщо аутентифікація успішна, API повертає статус успіху, а обробник перенаправляє користувача на захищений маршрут.
Створення кінцевої точки API для входу
У каталозі src/app, створіть папку api, а в ній папку login. У папці login створіть файл route.js та додайте наступний код:
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 });
}
Цей API перевіряє облікові дані, передані в POST-запиті. У разі успішної перевірки, він створює JWT токен з інформацією про користувача та відправляє клієнту успішну відповідь, включаючи токен у cookies, інакше повертає статус помилки.
Реалізація логіки перевірки токенів
Після генерації токена, наступним кроком є реалізація логіки його перевірки.
Тут буде використовуватись функція jwtVerify, надана модулем Jose, для перевірки JWT токенів, переданих в HTTP-запитах.
У каталозі src, створіть файл libs/auth.js та додайте наступний код:
import { jwtVerify } from "jose";export function getJwtSecretKey() {
const secret = process.env.NEXT_PUBLIC_JWT_SECRET_KEY;
if (!secret) {
throw new Error("Ключ JWT не знайдено");
}
return new TextEncoder().encode(secret);
}export async function verifyJwtToken(token) {
try {
const { payload } = await jwtVerify(token, getJwtSecretKey());
return payload;
} catch (error) {
return null;
}
}
Секретний ключ використовується для підписання та перевірки токенів. Порівнюючи декодований підпис токена з очікуваним, сервер може перевірити його дійсність та авторизувати запит.
Створіть файл .env в кореневому каталозі та додайте унікальний секретний ключ:
NEXT_PUBLIC_JWT_SECRET_KEY=your_secret_key
Створення захищеного маршруту
Необхідно створити маршрут, до якого можуть отримати доступ лише автентифіковані користувачі. У каталозі src/app створіть файл protected/page.js та додайте наступний код:
export default function ProtectedPage() {
return <h1>Захищена сторінка</h1>;
}
Створення хука для керування станом аутентифікації
Створіть папку hooks у каталозі src. У ній створіть файл useAuth/index.js та додайте наступний код:
"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;
}
Цей хук керує станом аутентифікації на стороні клієнта. Він отримує та перевіряє дійсність JWT токена, використовуючи функцію verifyJwtToken, та встановлює дані користувача в стан авторизації.
Це дозволяє іншим компонентам отримувати доступ та використовувати інформацію про автентифікованого користувача, необхідну для оновлення інтерфейсу, виконання запитів API або рендерингу контенту на основі ролей користувача.
У цьому випадку хук використовується для відображення різного вмісту на головному маршруті, в залежності від стану аутентифікації користувача.
Можна розглянути альтернативний підхід – керування станом за допомогою Redux Toolkit або Jotai, що забезпечить глобальний доступ до стану аутентифікації.
Відкрийте файл app/page.js, видаліть стандартний код Next.js та додайте наступний код:
"use client" ;
import { useAuth } from "@/hooks/useAuth";
import Link from "next/link";
export default function Home() {
const auth = useAuth();
return <>
<h1>Головна сторінка</h1>
<header>
<nav>
{auth ? (
<p>Ви авторизовані</p>
) : (
<Link href="https://wilku.top/login">Вхід</Link>
)}
</nav>
</header>
</>
}
Цей код використовує хук useAuth для керування станом аутентифікації. Він відображає головну сторінку, посилання на сторінку входу, якщо користувач не авторизований, або повідомлення про авторизацію, якщо авторизований.
Додавання проміжного ПЗ для обмеження доступу до захищених маршрутів
У каталозі src створіть файл middleware.js та додайте наступний код:
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*"] };
Цей код проміжного ПЗ перевіряє, чи авторизований користувач для доступу до захищених сторінок, та перенаправляє неавторизованих користувачів на сторінку входу.
Захист додатків Next.js
Аутентифікація за маркерами є ефективним інструментом для забезпечення безпеки, але не єдиним. Для ефективного захисту від кіберзагроз, необхідно застосовувати комплексний підхід до безпеки, що охоплює усі вразливості та потенційні загрози.