Использование куков для хранения токенов #
Документация #
Концепция по реализации сессии #
Для реализации сессий используем следующие компоненты:
- возможностью сохранять данные в куки;
- возможностью обрабатывать запрос на уровне фильтров;
- возможностью прикреплять дополнительные данные к контексту запроса.
Процесс аутентификации с помощью пароля будет выглядеть следующим образом:
- Пользователь открывает страницу входа в систему.
- Пользователь вводит имя пользователя и пароль, отправляет их веб-приложению.
- Обработчик HTTP-запроса проверяет переданные данные: для указанного пользователя проверяет корректность введённого пароля. Если пароль корректный, то система:
- Формирует JWT-токен с идентификатором пользователя.
- Устанавливает токен в куки ответа, чтобы веб-браузер их сохранил.
- Перенаправляет пользователя на главную страницу.
 
- Веб-браузер запоминает переданный токен в куках и передаёт их при каждом следующем запросе в систему.
Процесс аутентификации с помощью токена будет выглядеть следующим образом:
- Веб-браузер посылает запрос серверу.
- Сервер на уровне фильтра проверяет наличие токена в куках:
- Проверяет наличие токена в куках.
- Проверяет корректность токена в куках.
- По идентификатору токена формирует описание объекта внутри системы.
- Сохраняет описание пользователя в рамках контекста запроса.
 
- Затем внутри обработчика запроса из контекста извлекается информация о пользователе, например для отображения его имени на странице. Извлечённое имя передаётся шаблонизатору для отображения.
Использование контекста запросов #
Библиотека http4k предоставляет классы для сохранения дополнительных данных к конкретным запросам. Это реализуется через классы RequestContext и RequestContexts, а также через линзы RequestContextKey и RequestContextLens. С помощью этих классов можно добавить дополнительные объекты к запросу и использовать их в других частях приложения.
Для реализации этой возможности внутри приложения заводится глобальный объект, которых содержит в себе необходимые данные. Данные внутри объекта привязаны к конкретному запросу. Обычно состояние объекта изменяется на уровне фильтров, где происходит запись нужных данных. Затем к этим данным происходит обращение в других фильтрах и в завершающих обработчиках HTTP-запросов.
Для работой над данными http4k предоставляет два варианта:
- Использование строк для описания ключа хранения объекта.
- Использование линз для модификации данного объекта.
Первый подход может казаться проще, но он не обеспечивает безопасность по типу сохраняемых данных, а также предотвращает случайную перезапись данных по строковому ключу.
Вне зависимости от используемого типа доступа к хранилищу, его необходимо инициализировать с помощью фильтра ServerFilters.InitialiseRequestContext. Инициализировать его необходимо один раз для всего приложения, перед фильтрами и http-обработчиками, использующими данное хранилище. Данный фильтр ответственен также за очистку данных, связанных с конкретным запросом, когда его обработка завершается.
package guide.howto.attach_context_to_a_request
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"))
}Задание. Реализация аутентификации #
Реализуйте процесс аутентификации на основе сессий в приложение по управлению треугольниками.
Приложение должно позволять пользователю ввести имя пользователя и пароль. Если данные введены неправильно, то приложение должно возвращаться назад к форме. Если данные были введены верно:
- В навигационной панели должно отображаться имя текущего пользователя.
- При выходе из приложения имя пользователя не отображается.
- При создании нового треугольника владельцем данного треугольника становится данный пользователь.
Подход к реализации страницы авторизации #
- Добавьте шаблон страницы авторизации, login.peb. На данной странице должна располагаться форма для ввода имени пользователя и пароля. Данные с формы должны отправляться POST-запросом.
- Добавьте HTTP-обработчик для отображения формы входа в систему. Данный обработчик должен показывать страницу авторизации.
- Свяжите HTTP-обработчик отображения формы с маршрутом /login. Убедитесь, что по данному маршруту отображается форма.
- Добавьте HTTP-обработчик для обработки данных с формы. Данный обработчик должен обрабатывать переданные данные.
- Необходимо проверить, что данные с формы, были введены: имя пользователя и пароль не могут быть пустыми. Если форма не заполнена, то необходимо отобразить нужную информацию.
- Необходимо использовать запрос к хранилищу, который проверяет корректность имени пользователя и пароля.
- Если запрос вернул ошибку, то на странице необходимо отобразить сообщение о том что имя пользователя или пароль были введены неверно.
- Если запрос был указан корректно, то необходимо сформировать JWT-токен, содержащий идентификатор пользователя внутри системы. Данный JWT-токен необходимо установить в куки ответа в качестве идентификатора куки можно использовать имя auth_token. Пользователя необходимо перенаправить на страницу входа.
 
Реализация отображения имени текущего пользователя #
- В функции по созданию веб-приложения создайте объект для сохранения контекста, тип RequestContexts.
- В данной функции также определите линзу для взаимодействия с хранилищем, которая позволит сохранять и считывать структуру, описывающую пользователя. В данной структуре необходимо иметь возможность сохранить идентификатор пользователя и его имя. К сожалению линза не может предоставить значение надёжно, поэтому она должна быть опциональной.
- Создайте операцию по формированию структуры по идентификатору пользователя. Данная операция должна принимать в качестве аргумента идентификатор пользователя и возвращать структуру, заполненную необходимой информацией или null-значение, если пользователя с заданным идентификатором не существует.
- Создайте фильтр аутентификации. Данный фильтр будет зависеть от созданной на предыдущем шаге линзы, метода по извлечению идентификатора пользователя из JWT-токена и запросу на формирование структуры, описывающей пользователя.
- Фильтр должен проверять наличие в куках ключа auth_token.
- Из значения данного ключа необходимо извлечь значение, JWT-токен.
- Из JWT-токена извлечь идентификатор пользователя.
- С помощью запроса по идентификатору необходимо получить структуру пользователя.
- Полученную структуру необходимо записать в контекст запроса с помощью переданной линзы. Если какое-то из этих действий сделать невозможно, то не надо производить запись null-значения в качестве значения.
 
- Фильтр должен проверять наличие в куках ключа 
- В функции по созданию веб-приложения сформируйте цепочку фильтров.
- Первым фильтром должен быть фильтр по инициализации хранилища контекстной информацией.
- Следующим фильтром цепочки должен быть созданный фильтр аутентификации.
- Затем расположите существующие фильтры вашего приложения или маршрутизатор.
 
- Модифицируйте базовую раскладку страниц, layout.peb, так чтобы в рамках навигационной панели справа располагался блок аутентификации.- Если в рамках модели не передаётся структура описания пользователя, то необходимо показывать ссылку на страницу аутентификации. Для проверки на пустоту логично использовать проверку emptyшаблонизатора.
- В противном случае необходимо выводить имя текущего пользователя.
 
- Если в рамках модели не передаётся структура описания пользователя, то необходимо показывать ссылку на страницу аутентификации. Для проверки на пустоту логично использовать проверку 
- Модифицируйте обработку стартовой страницы внутри приложения. HTTP-обработчику необходимо передать линзу для получения структуры пользователя из контекста запроса. Он должен использовать её при обработке запроса и записывать полученный идентификатор в поле модели.
- Проверьте, что после выполнения успешной аутентификации при показе стартовой страницы в навигационной панели показывается имя пользователя.
Реализация функции выхода из приложения #
- Добавьте новы HTTP-обработчик запроса на выход из приложения.
- При получении запроса он формирует ответ-перенаправление на стартовую страницу приложения. В ответе с помощью метода removeCookieвыполняется удаление кукиauth_token.
- Свяжите данный обработчик в маршрутизаторе с маршрутом /logoutи GET-запросом.
- Исправьте раскладку layout.pebтаким образом, что при наличии информации об активном пользователе, показывалось не только имя пользователя, но и ссылка на выход из приложения.
- Проверьте, что после входа в приложение показывается ссылка на выход, а после нажатия на соответствующую ссылку очищается сессионный JWT-токен.
Учёт текущего пользователя при создании треугольника #
Модифицируйте процедуру добавление нового треугольника в приложении.
- Уберите поля для ввода имени пользователя и пароля шаблона страницы.
- Уберите зависимость от запроса проверку корректности имени пользователя и пароля из HTTP-обработчика.
- Добавьте HTTP-обработчику доступ к линзе на получение информации о текущем пользователе.
- При показе HTTP-формы необходимо проверить наличие информации о пользователе, т.е. проверить наличие аутентификации. В случае отсутствия информации о пользователе необходимо на HTML-странице вывести соответствующее сообщение.
- При обработке HTTP-запроса на добавление треугольника необходимо использовать линзу для получения информации о текущем пользователе. Полученный идентификатор пользователя необходимо использовать при создании нового объекта-треугольника.