Куки и сессионное хранилище

Куки и сессионное хранилище #

Документация #

Концепция по реализации сессии #

Для реализации сессий используем следующие компоненты:

  • возможностью сохранять данные в куки;
  • возможностью обрабатывать запрос на уровне фильтров;
  • возможностью прикреплять дополнительные данные к контексту запроса.

Процесс аутентификации с помощью пароля будет выглядеть следующим образом:

  1. Пользователь открывает страницу входа в систему.
  2. Пользователь вводит имя пользователя и пароль, отправляет их веб-приложению.
  3. Обработчик HTTP-запроса проверяет переданные данные: для указанного пользователя проверяет корректность введённого пароля. Если пароль корректный, то веб-приложение:
    • Формирует JWT-токен с идентификатором пользователя.
    • Устанавливает токен в куки ответа, чтобы веб-браузер их сохранил.
    • Перенаправляет пользователя на главную страницу.
  4. Веб-браузер запоминает переданный токен в куках и передаёт их при каждом следующем запросе в систему.

Процесс аутентификации с помощью токена будет выглядеть следующим образом:

  1. Веб-браузер посылает запрос серверу.
  2. Сервер на уровне фильтра проверяет наличие токена в куках:
    • Проверяет наличие токена в куках.
    • Проверяет корректность JWT-токена в куках.
    • По идентификатору JWT-токена формирует объект, описывающий пользователя внутри системы.
    • Сохраняет описание пользователя в рамках контекста запроса.
  3. Затем внутри обработчика запроса из контекстного хранилища извлекается информация о пользователе, например для отображения его имени на странице. Извлечённое имя передаётся шаблонизатору для отображения.

Использование контекста запросов #

Библиотека http4k предоставляет классы для сохранения дополнительных данных к конкретным запросам. Это реализуется через классы RequestContext и RequestContexts, а также через линзы RequestContextKey и RequestContextLens. С помощью этих классов можно добавить дополнительные объекты к запросу и использовать их в других частях приложения.

Для реализации этой возможности внутри приложения заводится глобальный объект, который обеспечивает создание хранилища для контекстных данных. Для решения проблем многопоточного доступа конкретные хранилища инициализируются на уровне соответствующего фильтра. Данные внутри хранилища привязаны к конкретному запросу. Обычно состояние хранилища изменяется на уровне фильтров, где происходит запись нужных данных. Затем к этим данным происходит обращение в других фильтрах и в завершающих обработчиках HTTP-запросов.

Для работой над данными http4k предоставляет два варианта:

  1. Использование строк для описания ключа хранения объекта.
  2. Использование линз для модификации данного объекта.

Первый подход может казаться проще, но он не обеспечивает безопасность по типу сохраняемых данных, а также предотвращает случайную перезапись данных по строковому ключу.

Вне зависимости от используемого типа доступа к хранилищу, его необходимо инициализировать с помощью фильтра ServerFilters.InitialiseRequestContext. Инициализировать его необходимо один раз для всего приложения, перед фильтрами и http-обработчиками, использующими данное хранилище. Данный фильтр ответственен также за очистку данных, связанных с конкретным запросом, когда его обработка завершается.

import org.http4k.core.Filter
import org.http4k.core.HttpHandler
import org.http4k.core.Method.GET
import org.http4k.core.Request
import org.http4k.core.RequestContexts
import org.http4k.core.Response
import org.http4k.core.Status.Companion.OK
import org.http4k.core.then
import org.http4k.core.with
import org.http4k.filter.ServerFilters
import org.http4k.lens.RequestContextKey
import org.http4k.lens.RequestContextLens

fun main() {
    // Класс, описывающие данные контекста
    data class SharedState(val message: String)

    // Фильтр для сохранения данных в контекст запроса
    fun addStateFilter(key: RequestContextLens<SharedState>) = Filter { next ->
        { request ->
            // С помощью линзы key сохраняем состояние в хранилище
            next(request.with(key of SharedState("hello there")))
        }
    }

    // Обработчик HTTP-запроса, использующий данные из контекста
    fun printStateHandler(key: RequestContextLens<SharedState>): HttpHandler = { request ->
        // Для получения значения применяем линзу к объекту запроса
        println(key(request))
        Response(OK)
    }

    // Создаём хранилище для всех объектов, описывающих контекст запроса
    val contexts = RequestContexts()

    // Описываем линзу для установки и считывания данных из контекста запроса.
    // Линзы могут быть обязательными (required), опциональными (optional) или
    // со значением по умолчанию (defaulted), как и любые другие линзы
    val key = RequestContextKey.required<SharedState>(contexts)

    // Первый фильтр инициализирует и управляет хранилищем контекста.
    // Второй фильтр записывает состояние в хранилище.
    // Обработчик выводит информацию.
    val app = ServerFilters.InitialiseRequestContext(contexts)
        .then(AddState(key))
        .then(PrintState(key))

    app(Request(GET, "/hello"))
}

Задание. Реализация аутентификации #

Реализуйте процесс аутентификации на основе сессий в приложение по управлению треугольниками.

Приложение должно позволять пользователю ввести имя пользователя и пароль на странице входа в приложение. Если данные введены неправильно, то приложение должно возвращаться назад к форме, сохраняя введённые данные пользователем. Если данные были введены верно:

  • В навигационной панели должно отображаться имя текущего пользователя.
  • При выходе из приложения имя пользователя не отображается.
  • При создании нового треугольника владельцем данного треугольника становится данный пользователь. В форме создания нового треугольника необходимо убрать поле «владелец».

Подход к реализации страницы авторизации #

  1. Добавьте шаблон страницы авторизации, login.peb. На данной странице должна располагаться форма для ввода имени пользователя и пароля. Данные с формы должны отправляться POST-запросом. POST-запрос необходим для обеспечения безопасной передачи данных из клиента на сервер.
  2. Добавьте HTTP-обработчик для отображения формы входа в систему. Данный обработчик должен показывать страницу авторизации.
  3. Свяжите HTTP-обработчик отображения формы с маршрутом /login. Убедитесь, что по данному маршруту отображается форма.
  4. Добавьте HTTP-обработчик для обработки данных с формы. Данный обработчик должен обрабатывать переданные данные.
    1. Необходимо проверить, что данные с формы, были введены: имя пользователя и пароль не могут быть пустыми. Если форма не заполнена, то необходимо отобразить нужную информацию.
    2. Необходимо использовать запрос к хранилищу, который проверяет корректность имени пользователя и пароля.
    3. Если запрос вернул ошибку, то на странице необходимо отобразить сообщение о том что имя пользователя или пароль были введены неверно. Нельзя сообщать детальную информацию по проблеме, т.к. злоумышленник сможет использовать сообщения об ошибках для обхода защиты информации.
    4. Если запрос был указан корректно, то необходимо сформировать JWT-токен, содержащий идентификатор пользователя внутри системы.
    5. Данный JWT-токен необходимо установить в куки ответа, в качестве идентификатора куки можно использовать имя auth.
    6. Пользователя необходимо перенаправить на страницу входа.

Реализация отображения имени текущего пользователя #

  1. В функции по созданию веб-приложения создайте объект для сохранения контекста, тип RequestContexts.
  2. В данной функции также определите линзу для взаимодействия с хранилищем, которая позволит сохранять и считывать структуру, описывающую пользователя.
    • В данной структуре необходимо иметь возможность сохранить идентификатор пользователя и его имя.
    • К сожалению линза не может предоставить значение надёжно, поэтому она должна быть опциональной. Линза не сможет предоставить данные, если пользователь не передал аутентификационный токен.
  3. Создайте операцию по формированию структуры по идентификатору пользователя. Данная операция должна принимать в качестве аргумента идентификатор пользователя и возвращать структуру, заполненную необходимой информацией или null-значение, если пользователя с заданным идентификатором не существует.
  4. Создайте фильтр аутентификации. Данный фильтр будет зависеть от созданной на предыдущем шаге линзы, метода по извлечению идентификатора пользователя из JWT-токена и запросу на формирование структуры, описывающей пользователя.
    1. Фильтр должен проверять наличие в куки с наименованием auth.
    2. Из значения данного ключа необходимо извлечь значение, JWT-токен.
    3. Из JWT-токена извлечь идентификатор пользователя.
    4. С помощью запроса по идентификатору необходимо получить структуру пользователя.
    5. Полученную структуру необходимо записать в контекст запроса с помощью переданной линзы. Если какое-то из этих действий сделать невозможно, то не надо производить запись null-значения в качестве значения.
  5. В функции по созданию веб-приложения сформируйте цепочку фильтров.
    1. Первым фильтром должен быть фильтр по инициализации хранилища контекстной информацией.
    2. Следующим фильтром цепочки должен быть созданный фильтр аутентификации.
    3. Затем расположите существующие фильтры вашего приложения или маршрутизатор.
  6. Модифицируйте базовую раскладку страниц, layout.peb, так чтобы в рамках навигационной панели справа располагался блок аутентификации.
    • Если в рамках модели не передаётся структура описания пользователя, то необходимо показывать ссылку на страницу аутентификации. Для проверки на пустоту логично использовать проверку empty шаблонизатора.
    • В противном случае необходимо выводить имя текущего пользователя.
  7. Модифицируйте обработку стартовой страницы внутри приложения. HTTP-обработчику необходимо передать линзу для получения структуры пользователя из контекста запроса. Он должен использовать её при обработке запроса и записывать полученный идентификатор в поле модели.
  8. Проверьте, что после выполнения успешной аутентификации при показе стартовой страницы в навигационной панели показывается имя пользователя.

Реализация функции выхода из приложения #

  1. Добавьте новый HTTP-обработчик запроса на выход из приложения.
  2. При получении запроса он формирует ответ-перенаправление на стартовую страницу приложения. В ответе с помощью метода invalidateCookie установите пустое значение для куки с наименованием auth.
  3. Свяжите данный обработчик в маршрутизаторе с маршрутом /logout и GET-запросом.
  4. Исправьте раскладку layout.peb таким образом, что при наличии информации об активном пользователе, показывалось не только имя пользователя, но и ссылка на выход из приложения.
  5. Проверьте, что после входа в приложение показывается ссылка на выход, а после нажатия на соответствующую ссылку очищается куки auth, содержащая сессионный JWT-токен.

Учёт текущего пользователя при создании треугольника #

Модифицируйте процедуру добавление нового треугольника в приложении.

  1. Уберите поля для ввода имени пользователя и пароля шаблона страницы.
  2. Уберите зависимость от запроса проверку корректности имени пользователя и пароля из HTTP-обработчика.
  3. Добавьте HTTP-обработчику доступ к линзе на получение информации о текущем пользователе.
  4. При показе HTTP-формы необходимо проверить наличие информации о пользователе, т.е. проверить наличие аутентификации. В случае отсутствия информации о пользователе необходимо на HTML-странице вывести соответствующее сообщение.
  5. При обработке HTTP-запроса на добавление треугольника необходимо использовать линзу для получения информации о текущем пользователе. Полученный идентификатор пользователя необходимо использовать при создании нового объекта-треугольника.

© A. M. Васильев, 2024, CC BY-SA 4.0, andrey@crafted.su