Васильев Андрей Михайлович, 2024
Версии презентации
Операции над данными должны осуществляться только уполномоченными лицами
Рассмотрим возможности, доступные на информационном портале
HTTP cookie — это небольшой фрагмент данных, отправляемый сервером на браузер пользователя, который может сохранить и отсылать обратно с новым запросом к серверу
Используются в веб-приложениями для:
Set-Cookie
и Cookie
#
Заголовки Set-Cookie
устанавливаются сервером в рамках своего ответа
Простой заголовок может выглядит так:
Set-Cookie: <имя cookie>=<значение cookie>
Таких заголовков в ответе сервера может быть несколько:
Set-Cookie: yummy_cookie=choco
Set-Cookie: tasty_cookie=strawberry
Браузер будет передавать их клиенту в рамках заголовка Cookie
:
Cookie: yummy_cookie=choco; tasty_cookie=strawberry
От клиента на сервер может передаваться несколько куков
Set-Cookie: id=5aoeu; Expires=Wed, 20 Nov 2010 10:15:00 GMT;
HttpOnly
Domain
и Path
Для описания куки используется класс Cookie, включающий поля
val domain: String?
— домен, для которого данная кука действуетval expires: LocalDateTime?
— время истечения действия кукиval httpOnly: Boolean
— ограничение доступа из контекста JavaScriptval maxAge: Long?
— время жизни кукиval name: String
— наименование кукиval path: String?
— путь в домене, который может иметь доступ к кукиval sameSite: SameSite?
— политика передачи куки от браузера к серверуval secure: Boolean
— необходимо ли использовать защищённое соединениеval value: String
— значение кукиДля чтения куков из запросов и записи куков в ответы предлагаются следующие методы
fun Request.cookies(): List<Cookie>
— получить весь набор куковfun Request.cookie(name: String): Cookie?
— получить куки по имениfun Response.cookie(cookie: Cookie): Response
— добавить куки к ответуfun Request.cookie(name: String, value: String): Request
— добавить простую куки по названию и значениюfun Response.invalidateCookie(name: String, domain: String? = null): Response
— установить пустое значение для куки, чтобы клиент её удалилСуществуют и другие методы, но указанных методов должно хватить для решения большинства задач
В подсистеме линз есть объект Cookies, позволяющий сформировать линзы для куков:
val cookieLens: BiDiLens<Request, Cookie> = Cookies.optional("auth_token")
Нет строгого стандарта (например RFC) который определял бы процедуру авторизации с помощью формы, каждый разработчик может реализовать логику самостоятельно
Ключевая задача — сформировать токен аутентификации и сохранить его в куки
В рамках данной схемы на сервере выделяется отдельное веб-приложение, выполняющее задачу аутентификации, а другие приложения делегируют решение задачи первому
Предыдущий процесс можно реализовать в специализированных клиентах, однако при использовании браузера процедура становится более сложной
Существует множество стандартов, среди наиболее популярных: OAuth, OAuth 2.0, OpenID Connect, SAML, WS-Federation
Внутри токена содержится дополнительная информация:
При получении токена его необходимо проверить на соответствие требованиям приложения
Набор пар имя-значение в формате кодирования HTML form, описывает стандартные ключи Issuer, Audience, ExpiresOn и HMACSHA256. Токен подписывается симметричным ключом
Содержит три блока, разделённых точками: заголовок, набор полей и подпись. Первые два блока закодированы в JSON-формате и закодированы в base64. Подпись может быть сформирована как симметричными, так и ассиметричными алгоритмами шифрования
Определяет токены в XML-формате, включающем информацию об эмитенте, субъекте, необходимые условия для проверки токена. Подпись осуществляется при помощи ассиметричной криптографии. Содержат механизм для подтверждения владения токеном
Данные стандарты предназначены в первую очередь для решения задачи по предоставлению доступа одного приложения к другому от имени пользователя
Приложению для своей работы могут потребоваться данные из другой системы:
В рамках стандарта OpenID Connect на основе OAuth разработан слой учётных данных, в рамках которого сервер авторизации предоставляет идентификационный токен
Большие технологические компании и государства предоставляет возможности по аутентификации с помощью данных протоколов: Госуслуги, VK, Яндекс, Сбербанк
Для упрощения данной задачи в промышленных приложениях лучше всего делегировать задачи идентификации и аутентификации внешним приложениям с использованием протоколов OpenID Connect и OAuth 2.0
Для решения задачи аутентификации своими силами необходимо:
По возможности стоит ограничить время жизни токена или реализовать систему отзыва токена при подозрении на их компроментацию (реализация кнопки «выйти из всех браузеров»)
Для проверки сессионных токенов необходимо либо:
Хранение сессионных токенов на стороне сервера несёт ряд сложностей:
Последняя схема имеет ряд проблем:
JSON Web Token — открытый стандарт, который определяет компактный и безопасный способ передачи данных в формате JSON-объекта
JWT включает в себя три части: заголовок, содержимое и подпись. Элементы отделены друг от друга точками
xxxxx.yyyyy.zzzzz
Включает в себя указание типа токена (JWT) и алгоритм подписи:
{
"alg": "HS256",
"typ": "JWT"
}
Данная информация кодируется с помощью кодировки Base64Url:
ew0KICAgICJhbGciOiAiSFMyNTYiLA0KICAgICJ0eXAiOiAiSldUIg0KfQ
Основная часть токена содержит ряд утверждений о том, для кого был сформирован токен, обычно данные о пользователе
Зарегистрированные утверждения не являются обязательными, но рекомендуются к использованию: iss (издатель), exp (время истечения), aud (аудитория) и другие
Публичные утверждения могут быть установлены любыми службами, но их следует зарегистрировать в IANA
Приватные утверждения тоже могут быть использованы для передачи данных, но не включены ни в одну из предыдущих категорий
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
Данная часть тоже кодируется в Base64Url
Подпись строится из следующих элементов:
Например, при использовании алгоритма HMAC SHA256 подпись строится так:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
Полученные элементы объединяются через точки для получения JWT-токена:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Для реализации работы с токеном приложению необходимо выполнять операции:
Для реализации в приложении можно воспользоваться одной из библиотек
В рамках курса рассмотрим библиотеку java-jwt
try { // Необходимо обеспечить хранение секретной строки в настройках
Algorithm algorithm = Algorithm.HMAC512(secret);
String token = JWT.create()
.withIssuer("ru.example")
.sign(algorithm);
} catch (exception: JWTCreationException){
// Неправильная конфигурация или ошибка конвертации утверждений
}
val algorithm = Algorithm.RSA256(rsaPublicKey, rsaPrivateKey);
val verifier: JWTVerifier = JWT.require(algorithm)
.withIssuer("ru.example")
.build(); // Можно создать единожды для приложения
String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpc3MiOiJhdXRoMCJ9.AbIJTDMFc7yUa5MhvcP03nJPyCPzZtQcGEp-zWfOkEE";
try {
val decodedJWT: DecodedJWT = verifier.verify(token);
} catch (JWTVerificationException exception){
// Неправильная подпись или утверждения
}
При разработке приложений на библиотеке http4k можно выполнять:
Внутри приложения стоит выбрать не более двух подходов к решению этой задачи
Предложенная ранее схема должна быть реализована в приложении: несанкционированный доступ к действиям над данными приложения должен быть невозможен
Однако только реализация данного подхода усложняет работу пользователя:
Рекомендуется адаптировать пользовательский интерфейс приложения: не показывать на страницах элементы, которые пользователь не может вызвать
Если мы хотим реализовать проверку прав доступа на всех уровнях приложения, то необходимо описать права в отдельной сущности
Предлагается следующий подход к моделированию прав:
Самый простой способ кодирования — использование классов данных
data class Permissions(
val manageUsers: Boolean = false,
val manageNews: Boolean = false,
) {
companion object {
val USER_EDITOR = Permissions(manageUsers = true)
val NEWS_EDITOR = Permissions(manageNews = true)
val ANONIMOUS = Permissions()
}
}
Использование класса позволяет учесть как базовые возможности роли пользователя, так и возможности, связанные с владенеием данными
Ввиду того, что определение роли необходимо:
Удобно реализовать эту логику на уровне фильтра http-запроса:
Базовый интерфейс фильтров в http4k передаёт данные внутреннему HTTP-обработчику только в формате объекта Request, передача дополнительных данных через этот интерфейс не предусмотрена
Рассмотрим последний вариант
Хранилище контекстных данных позволяет удобным образом хранить данные для одного запроса, удовлетворяя требованиям поточной безопасности
Для записи и извлечения данных из хранилища можно воспользоваться линзами
Данные в хранилище типизированы, т.е. не надо выполнять преобразование данных много раз
По окончании обработки запроса хранилище очищается
Рассмотрим пример
Линзу для взаимодействия с хранилищем необходимо предоставить соответствующим HTTP-обработчикам
Для обеспечения конкуретного доступа из нескольких потоков нужен специальный объект
val contexts = RequestContexts()
Также выполняем инициализацию на соответствующем потоке:
val router: RoutingHttpHandler = ... // Приложение
val baseApp = // Приложение с фильтрами
ErrorFilter(htmlView)
.then(router)
val app = //
ServerFilters.InitialiseRequestContext(contexts)
.then(AddState(contexts))
В хранилище можно помещать любые объекты:
data class SharedState(val message: String)
val key = RequestContextKey.required<SharedState>(contexts)
Далле линзу можно применить к запросу, чтобы установить:
val initialRequest: Request
val request = request.with(key of SharedState("hello there"))
Или считать значение:
val request: Request
val storedData: SharedState = key(request)
Настроенную линзу необходимо передать в фильтры или обработчики, которым нужен доступ
На уровне вида приложение может захотеть иметь информацию:
Для всех страниц, отображаемых пользователю (для отображения информации в навигационной панели)
На настоящий момент подсистема вида не поддерживает возможности прямого доступа к хранилищу контекста, поэтому предлагается собственный подход к формированию слоя отображения