Пошук уроків, статей та іншого контенту
Налаштуєте створення, оновлення, перевірку та завершення сесій користувача в Next.js.
Сесія пов’язує HTTP-запити з конкретним користувачем. Браузер зберігає ідентифікатор сесії в cookie та автоматично надсилає його з наступними запитами.
Типовий життєвий цикл сесії:
Користувач успішно проходить автентифікацію.
Сервер створює сесію та встановлює cookie.
Сервер перевіряє cookie під час доступу до захищених сторінок.
Сервер оновлює сесію, наприклад продовжує термін дії.
Після виходу сервер завершує сесію та видаляє cookie.
У Next.js сесію зручно реалізовувати на сервері за допомогою:
cookies() для читання та встановлення cookie;
Server Actions або Route Handlers для операцій, які змінюють cookie;
підписаного токена, щоб користувач не міг змінити його вміст непомітно для сервера.
У цьому уроці використаємо підписаний JWT у httpOnly cookie. У реальному застосунку дані сесії також часто зберігають у базі даних або Redis.
Для підписування та перевірки JWT встановимо пакет jose:
npm install joseСтворіть файл .env.local:
SESSION_SECRET=replace-this-with-a-long-random-secretСекрет не можна зберігати в репозиторії. Для генерації випадкового значення можна використати команду:
openssl rand -base64 32Скопіюйте отримане значення у SESSION_SECRET.
JWT підписаний, але не зашифрований. Його вміст можна прочитати на клієнті, тому не зберігайте в ньому пароль, платіжні дані чи іншу конфіденційну інформацію.
Створимо файл lib/session.ts. У ньому будуть функції для створення, перевірки, оновлення та завершення сесії.
import "server-only";
import { jwtVerify, SignJWT } from "jose";
import { cookies } from "next/headers";
const SESSION_COOKIE = "session";
const SESSION_DURATION = 60 * 60 * 24 * 7; // сім днів
const secretValue = process.env.SESSION_SECRET;
if (!secretValue) {
throw new Error("SESSION_SECRET is not defined");
}
const secret = new TextEncoder().encode(secretValue);
type SessionPayload = {
userId: string;
};
export type Session = SessionPayload & {
expiresAt: Date;
};
function cookieOptions() {
return {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax" as const,
path: "/",
maxAge: SESSION_DURATION,
};
}
async function createToken(userId: string) {
return new SignJWT({ userId })
.setProtectedHeader({ alg: "HS256", typ: "JWT" })
.setSubject(userId)
.setIssuer("fullstack-example")
.setAudience("fullstack-users")
.setIssuedAt()
.setExpirationTime(`${SESSION_DURATION}s`)
.sign(secret);
}
export async function createSession(userId: string) {
const token = await createToken(userId);
const cookieStore = await cookies();
cookieStore.set(SESSION_COOKIE, token, cookieOptions());
}
export async function getSession(): Promise<Session | null> {
const cookieStore = await cookies();
const token = cookieStore.get(SESSION_COOKIE)?.value;
if (!token) {
return null;
}
try {
const { payload } = await jwtVerify(token, secret, {
algorithms: ["HS256"],
issuer: "fullstack-example",
audience: "fullstack-users",
});
if (typeof payload.userId !== "string") {
return null;
}
if (!payload.exp) {
return null;
}
return {
userId: payload.userId,
expiresAt: new Date(payload.exp * 1000),
};
} catch {
// Неправильний або прострочений токен вважаємо недійсним.
return null;
}
}
export async function updateSession(): Promise<Session | null> {
const currentSession = await getSession();
if (!currentSession) {
return null;
}
// Створюємо новий токен із новим терміном дії.
await createSession(currentSession.userId);
return getSession();
}
export async function deleteSession() {
const cookieStore = await cookies();
cookieStore.set(SESSION_COOKIE, "", {
...cookieOptions(),
maxAge: 0,
expires: new Date(0),
});
}httpOnlyОпція httpOnly забороняє JavaScript у браузері читати cookie через document.cookie. Це зменшує ризик викрадення сесії під час XSS-атаки.
Також важливі такі параметри:
secure — передавати cookie лише через HTTPS у production;
sameSite: "lax" — обмежити надсилання cookie у кроссайтових запитах;
path: "/" — надсилати cookie для всіх маршрутів;
maxAge — час життя cookie в секундах.
Після перевірки email і пароля потрібно викликати createSession. Для прикладу використаємо Server Action.
Файл app/login/actions.ts:
"use server";
import { redirect } from "next/navigation";
import { createSession } from "@/lib/session";
export async function loginAction(formData: FormData) {
const email = String(formData.get("email") ?? "");
const password = String(formData.get("password") ?? "");
// У реальному застосунку тут має бути пошук користувача в базі даних
// і перевірка хешу пароля.
const isValidUser =
email === "user@example.com" && password === "password123";
if (!isValidUser) {
return {
error: "Неправильна електронна пошта або пароль",
};
}
// У реальному застосунку це значення надходить із бази даних.
const userId = "user-123";
await createSession(userId);
redirect("/account");
}Server Action виконується на сервері, тому вона може встановити httpOnly cookie.
Файл app/login/page.tsx:
import { loginAction } from "./actions";
export default function LoginPage() {
return (
<main>
<h1>Вхід</h1>
<form action={loginAction}>
<div>
<label htmlFor="email">Електронна пошта</label>
<input
id="email"
name="email"
type="email"
defaultValue="user@example.com"
required
/>
</div>
<div>
<label htmlFor="password">Пароль</label>
<input
id="password"
name="password"
type="password"
defaultValue="password123"
required
/>
</div>
<button type="submit">Увійти</button>
</form>
</main>
);
}Після успішного входу:
loginAction перевіряє облікові дані.
createSession створює JWT.
Сервер встановлює cookie session.
Користувач перенаправляється на /account.
Для перевірки сесії сервер читає cookie та викликає jwtVerify. Функція getSession повертає:
об’єкт сесії, якщо токен коректний і не прострочений;
null, якщо cookie відсутня, токен змінений або термін його дії завершився.
Створимо захищену сторінку app/account/page.tsx:
import { redirect } from "next/navigation";
import { getSession } from "@/lib/session";
export default async function AccountPage() {
const session = await getSession();
if (!session) {
redirect("/login");
}
return (
<main>
<h1>Особистий кабінет</h1>
<p>Ідентифікатор користувача: {session.userId}</p>
<p>
Сесія дійсна до: {session.expiresAt.toLocaleString("uk-UA")}
</p>
</main>
);
}getSession можна використовувати в Server Components, оскільки вони виконуються на сервері.
Перевірка сесії повинна виконуватися не лише на сторінці, а й у серверних операціях, які працюють із приватними даними:
"use server";
import { redirect } from "next/navigation";
import { getSession } from "@/lib/session";
export async function updateProfileAction(formData: FormData) {
const session = await getSession();
if (!session) {
redirect("/login");
}
const displayName = String(formData.get("displayName") ?? "");
// Оновлюємо профіль саме для session.userId.
// У реальному застосунку тут буде запит до бази даних.
console.log({
userId: session.userId,
displayName,
});
}Не варто довіряти userId, отриманому з форми або URL. Ідентифікатор авторизованого користувача потрібно брати з перевіреної сесії.
Оновлення сесії зазвичай означає продовження її терміну дії. Для цього створюється новий токен із тим самим userId, але новим часом завершення.
Створимо Server Action:
"use server";
import { redirect } from "next/navigation";
import { updateSession } from "@/lib/session";
export async function refreshSessionAction() {
const session = await updateSession();
if (!session) {
redirect("/login");
}
}Її можна викликати з форми:
import { refreshSessionAction } from "./actions";
export function RefreshSessionForm() {
return (
<form action={refreshSessionAction}>
<button type="submit">Продовжити сесію</button>
</form>
);
}Оновлювати сесію під час кожного запиту не обов’язково. Частіше використовують один із підходів:
фіксований термін дії — сесія завершується через заданий час;
ковзний термін дії — сесія продовжується після активності користувача;
продовження лише тоді, коли до завершення залишилося мало часу.
Обережно використовуйте ковзний термін дії: активна вкладка може продовжувати сесію необмежено довго.
Під час виходу потрібно видалити cookie. Створимо Server Action у файлі app/account/actions.ts:
"use server";
import { redirect } from "next/navigation";
import { deleteSession } from "@/lib/session";
export async function logoutAction() {
await deleteSession();
redirect("/login");
}Додамо кнопку виходу до сторінки кабінету:
import { redirect } from "next/navigation";
import { getSession } from "@/lib/session";
import { logoutAction } from "./actions";
export default async function AccountPage() {
const session = await getSession();
if (!session) {
redirect("/login");
}
return (
<main>
<h1>Особистий кабінет</h1>
<p>Ідентифікатор користувача: {session.userId}</p>
<form action={logoutAction}>
<button type="submit">Вийти</button>
</form>
</main>
);
}Після виклику deleteSession браузер отримує cookie з минулим терміном дії та видаляє її. Наступний виклик getSession поверне null.
У наведеному прикладі вся інформація про сесію міститься в підписаному JWT. Такий підхід має переваги:
не потрібен запит до бази даних для кожної перевірки;
легко працює в різних середовищах виконання;
сервер може перевірити токен самостійно.
Однак у нього є важливе обмеження: сервер не може миттєво відкликати вже виданий токен. Після deleteSession cookie видаляється лише в поточному браузері. Якщо токен було скопійовано, він залишатиметься дійсним до завершення терміну дії.
Для можливості негайного відкликання використовують непрозорий ідентифікатор сесії:
Сервер генерує випадковий токен.
У cookie зберігається лише цей токен.
У базі даних зберігаються токен, userId і expiresAt.
Під час кожного запиту сервер шукає токен у базі даних.
Для виходу сервер видаляє запис сесії з бази даних.
Такий підхід зручний для:
виходу з усіх пристроїв;
перегляду активних сесій;
примусового завершення сесії адміністратором;
негайного блокування викраденого токена.
Незалежно від способу зберігання не записуйте пароль у cookie або в JWT.
Дотримуйтеся таких правил:
зберігайте секрет у змінних середовища;
використовуйте довгий випадковий секрет;
встановлюйте httpOnly;
вмикайте secure у production;
обмежуйте термін життя сесії;
перевіряйте підпис, алгоритм, видавця та аудиторію токена;
не зберігайте в токені конфіденційні дані;
перевіряйте сесію на сервері перед кожною операцією з приватними даними;
після зміни пароля або критичних налаштувань розгляньте завершення активних сесій.
httpOnly cookie на клієнтіhttpOnly cookie недоступна через document.cookie. Це очікувана поведінка. Читайте її в Server Component, Server Action або Route Handler.
Server Component може читати cookie, але зміна cookie має відбуватися в Server Action або Route Handler.
Не декодуйте JWT простим split(".") і не використовуйте його payload без перевірки підпису. Для перевірки використовуйте jwtVerify.
Підпис не приховує дані. Користувач може прочитати payload токена, навіть якщо не може його змінити.
Захист сторінки не захищає автоматично всі дії. Кожна Server Action, яка змінює приватні дані, повинна самостійно викликати getSession.
Не записуйте SESSION_SECRET безпосередньо у файлі TypeScript і не додавайте .env.local до репозиторію.
Довгоживуча сесія зручна для користувача, але збільшує наслідки викрадення cookie. Обирайте термін відповідно до чутливості застосунку.
Сесія зберігає зв’язок між користувачем і наступними HTTP-запитами.
У Next.js cookie читають за допомогою cookies() з next/headers.
Створення та зміна cookie виконуються на сервері через Server Actions або Route Handlers.
Підписаний JWT захищає дані від непомітної зміни, але не шифрує їх.
getSession має повертати null для відсутньої, недійсної або простроченої сесії.
Оновлення сесії створює новий токен із продовженим терміном дії.
Завершення сесії видаляє cookie.
Для негайного відкликання сесій краще зберігати сесії на сервері за допомогою бази даних.