Сессия и владелец истории читателя
Формы «Маршрута» готовы отправлять проверяемые команды, но серверу ещё нужен доверенный владелец. Добавим учебную сессию: после входа браузер получает случайный непрозрачный токен, а сервер связывает его с идентификатором читателя. Загрузка и каждое действие используют locals.reader, а не значение из полей формы.
Результат урока — связанный локальный кабинет. Это ограниченный учебный механизм с данными и сессиями в памяти процесса; после перезапуска они исчезнут. Он не является подключением к аккаунтам ProfessorWeb и не включает регистрацию, восстановление доступа или промышленное управление сессиями. Полные исходники приложены к серии, но запуск, вход и запись здесь не выполнялись.
Токен не равен идентификатору
Если cookie содержит просто reader=alina, человек способен заменить её на другое известное имя. Серверу нужен токен, который трудно угадать и который он сам выдал после проверки входа. В учебном модуле используем randomBytes из Node.js, а серверный Map связывает токен с владельцем и сроком действия.
Фрагмент src/lib/server/session.ts показывает основной контракт. Полная реализация в исходниках также содержит удаление токена и проверку пароля локального учебного пользователя.
import { randomBytes } from 'node:crypto';
type Session = { readerId: string; expires: number };
const sessions = new Map<string, Session>();
export function createSession(readerId: string) {
const token = randomBytes(32).toString('hex');
sessions.set(token, { readerId, expires: Date.now() + 60 * 60 * 1000 });
return token;
}
export function findSession(token: string | undefined) {
const session = token ? sessions.get(token) : undefined;
if (!session || session.expires <= Date.now()) return null;
return session;
}
Map находится на сервере и служит учебным хранилищем. Это не глобальный реактивный объект, отданный компонентам всех посетителей. Однако память всё равно общая для процесса, поэтому каждый метод обязан выбирать запись по доверенному readerId. Название модуля само по себе не гарантирует изоляцию. Многопроцессное размещение потребовало бы общего серверного хранилища сессий.
Личность в контексте запроса
Hook выполняется перед обработкой страницы и заполняет контекст текущего запроса. Полный src/hooks.server.ts использует токен из cookie; имя пользователя не читается из URL. getReader принадлежит серверному репозиторию и возвращает только краткую проекцию владельца.
import type { Handle } from '@sveltejs/kit';
import { findSession } from '$lib/server/session';
import { getReader } from '$lib/server/repository';
export const handle: Handle = async ({ event, resolve }) => {
const session = findSession(event.cookies.get('route-session'));
event.locals.reader = session ? getReader(session.readerId) : null;
return resolve(event);
};
В src/app.d.ts объявите App.Locals.reader как объект { id: string; displayName: string } | null. TypeScript после этого помогает не забыть отсутствие сессии. Он не выполняет проверку вместо hook: фактический пользователь всё равно определяется на сервере для каждого запроса.
Серверная загрузка кабинета сначала проверяет locals.reader, затем читает репозиторий по его id. Без сессии используйте redirect(303, '/login'). Действия тоже проверяют сессию, как уже показано в девятом уроке. Пользователь способен отправить POST напрямую, поэтому защита только загрузки была бы неполной.
Вход и атрибуты cookie
Учебные учётные записи задаются серверными переменными окружения, а пароли не попадают в клиентский каталог. Вход проверяет пароль, создаёт токен и устанавливает cookie. В SvelteKit 2 при установке cookie нужен явный path; правила API приведены в руководстве миграции.
cookies.set('route-session', createSession(reader.id), {
path: '/', httpOnly: true, sameSite: 'lax',
secure: !dev, maxAge: 3600
});
redirect(303, '/account');
dev импортируется из $app/environment. Локальная разработка допускает HTTP, рабочее размещение требует HTTPS и защищённую cookie. httpOnly не позволяет обычному браузерному JavaScript читать токен. sameSite уменьшает часть нежелательных межсайтовых отправок, однако вся политика запросов и защиты формы должна соответствовать среде SvelteKit.
Токен не возвращается в load, не выводится в HTML и не помещается в query string. Выход удаляет серверную сессию и cookie с тем же path. Одно только скрытие кнопки кабинета не завершает действующий токен. В финальном архиве маршрут выхода реализован POST-формой, потому что это изменение состояния сессии.
Репозиторий и два читателя
Начальное состояние каждого учебного читателя содержит пустые наборы закладок и завершённых уроков. Методы setBookmark и setComplete получают владельца первым параметром. Они устанавливают нужное значение, а не переворачивают его, сохраняют уникальность идентификаторов и увеличивают версию после фактического изменения.
Ожидаемый пример: Алина сохраняет css-layout, Борис открывает свой кабинет и не видит этой закладки. Борис отмечает html-start завершённым, история Алины остаётся прежней. Такая пара наблюдений показывает смысл владельца лучше, чем одна успешная запись. Здесь эти действия описаны как будущая ручная проверка, а не проведённый сценарий.
Данные в памяти исчезают после перезапуска и не разделяются между несколькими серверными процессами. Поэтому результат подходит локальному изучению пути запроса, но не постоянной истории посетителей. Замена репозитория на базу должна сохранить контракт владельца, уникальность пары читатель–урок и ясный результат повторной команды.
Границы учебного входа
Пароли примера нельзя считать готовой системой регистрации. Для внешнего продукта понадобятся защищённое хранение учётных данных, ограничение попыток, восстановление доступа, отзыв сессий и управление жизненным циклом аккаунта. Конечный договор передачи назовёт эти отсутствующие возможности, чтобы кабинет не оказался опубликованным с учебными допущениями.
Жизнь сессии после изменения профиля
Токен сессии хранит идентификатор владельца, а имя берётся из репозитория при текущем запросе. Поэтому сохранение новой подписи не требует выдавать новый токен или передавать имя в cookie. Идентичность остаётся прежней, изменяется отображаемое свойство.
Когда срок истёк, findSession возвращает null. В полном модуле полезно также удалить истёкшую запись, чтобы память не росла от давно неиспользуемых токенов. Для внешней системы понадобятся управляемая очистка и возможность отзыва конкретной сессии. Учебный Map не решает эксплуатационный жизненный цикл автоматически.
Два входа одного читателя могут иметь разные токены и одну историю. Выход текущего браузера удаляет только его токен в выбранном примере. Кнопка «Выйти на всех устройствах» потребовала бы отдельного поиска и отзыва всех сессий владельца; её нельзя обещать по существующему маршруту выхода.
После выдачи cookie перенаправление ведёт только на известный адрес кабинета. Если позже появится returnTo, сервер обязан ограничить его допустимыми внутренними путями, чтобы вход не стал открытым перенаправлением на чужой сайт. Удобная навигационная возможность тоже требует своего контракта, отдельного от правильности пароля.
Тем не менее основные границы уже полезны: публичный каталог не знает пароля; hook получает личность из серверной сессии; загрузка отдаёт безопасную проекцию; действие снова проверяет владельца; браузер отправляет только намерение. Следующий урок разберёт отказ хранилища и доступность изменяющегося списка, сохраняя эти границы даже в неудачном запросе.