Авторизация действий пользователя #
Васильев Андрей Михайлович, 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;
Безопасность HTTP куков #
- Данные передаются в открытом виде, для защиты нужно использовать SSL-шифрование
- Для защиты данных куки от доступа из JavaScript, необходимо использовать атрибут
HttpOnly
- Можно явно указать домены, с которых будет доступна информация с помощью атрибутов
Domain
иPath
- «Раздражающее» сообщение о куках необходимо по законодательству сообщать пользователю об их использовании веб-приложением и их назначением. Многие JavaScript-библиотеки добавляют свои куки
Работа с куками в http4k #
Для описания куки используется класс 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) который определял бы процедуру авторизации с помощью формы, каждый разработчик может реализовать логику самостоятельно
Ключевая задача — сформировать токен аутентификации и сохранить его в куки
Аутентификация по токенам #
В рамках данной схемы на сервере выделяется отдельное веб-приложение, выполняющее задачу аутентификации, а другие приложения делегируют решение задачи первому
- Веб-приложение, обеспечивающее аутентификацию, называется identity provider или authentication service (в случае, если происходит ещё и авторизация)
- Другие приложения в рамках данной схемы называются service provider
Аутентификация браузера в провайдере #
Предыдущий процесс можно реализовать в специализированных клиентах, однако при использовании браузера процедура становится более сложной
Варианты аутентификации по токену #
Существует множество стандартов, среди наиболее популярных: OAuth, OAuth 2.0, OpenID Connect, SAML, WS-Federation
Внутри токена содержится дополнительная информация:
- кто сгенерировал токен
- кто может быть получателем токена
- срок действия
- набор сведений об аутентифицированной сущности
При получении токена его необходимо проверить на соответствие требованиям приложения
Форматы токенов #
Simple Web Token #
Набор пар имя-значение в формате кодирования HTML form, описывает стандартные ключи Issuer, Audience, ExpiresOn и HMACSHA256. Токен подписывается симметричным ключом
JSON Web Token, JWT #
Содержит три блока, разделённых точками: заголовок, набор полей и подпись. Первые два блока закодированы в JSON-формате и закодированы в base64. Подпись может быть сформирована как симметричными, так и ассиметричными алгоритмами шифрования
Security Assertion Markup Language (SAML) #
Определяет токены в XML-формате, включающем информацию об эмитенте, субъекте, необходимые условия для проверки токена. Подпись осуществляется при помощи ассиметричной криптографии. Содержат механизм для подтверждения владения токеном
Стандарты OAuth и OpenID Connect #
Данные стандарты предназначены в первую очередь для решения задачи по предоставлению доступа одного приложения к другому от имени пользователя
Приложению для своей работы могут потребоваться данные из другой системы:
- Список контактов данного пользователя
- Список файлов данного пользователя
- Почтовый адрес данного пользователя
В рамках стандарта OpenID Connect на основе OAuth разработан слой учётных данных, в рамках которого сервер авторизации предоставляет идентификационный токен
Большие технологические компании и государства предоставляет возможности по аутентификации с помощью данных протоколов: Госуслуги, VK, Яндекс, Сбербанк
Реализация аутентификации по паролю #
Для упрощения данной задачи в промышленных приложениях лучше всего делегировать задачи идентификации и аутентификации внешним приложениям с использованием протоколов OpenID Connect и OAuth 2.0
Для решения задачи аутентификации своими силами необходимо:
- Безопасно сохранять данные для идентификации и аутентификации
- Формировать средства быстрой проверки аутентификации, токены
- Сохранять токены в куках HTTP-протокола
- Реализовать аутентификацию пользователя посредством проверки токена
По возможности стоит ограничить время жизни токена или реализовать систему отзыва токена при подозрении на их компроментацию (реализация кнопки «выйти из всех браузеров»)
Хранение и проверка сессионных токенов #
Для проверки сессионных токенов необходимо либо:
- доверять данным, пришедшим от пользователя, т.е. отказаться от проверки данных
- хранить сессионный токен на стороне сервера для сверки
- применять схемы с шифрованием данных или подписью данных в токене
Хранение сессионных токенов на стороне сервера несёт ряд сложностей:
- сколько токенов может быть выдано конкретному пользователю?
- сколько пространства потребуется для хранения всех выданных токенов?
- сколько времени уйдёт на выявление их корректности?
Последняя схема имеет ряд проблем:
- надо корректно реализовать криптографические схемы
- сложно отозвать скомпрометированные токены
JWT-токены #
JSON Web Token — открытый стандарт, который определяет компактный и безопасный способ передачи данных в формате JSON-объекта
- Безопасность достигается благодаря использованию цифровой подписи
- Наличие подписи позволяет верифицировать данные внутри подписи
- Злоумышленнику чрезвычайно трудно сфабриковать подпись
- Данные JWT-токена передаются открыто, нельзя помещать туда конфиденциальные данные
Структура JWT-токена #
JWT включает в себя три части: заголовок, содержимое и подпись. Элементы отделены друг от друга точками
xxxxx.yyyyy.zzzzz
Элементы токена, заголовок #
Включает в себя указание типа токена (JWT) и алгоритм подписи:
{
"alg": "HS256",
"typ": "JWT"
}
Данная информация кодируется с помощью кодировки Base64Url:
ew0KICAgICJhbGciOiAiSFMyNTYiLA0KICAgICJ0eXAiOiAiSldUIg0KfQ
Элементы токена, содержимое #
Основная часть токена содержит ряд утверждений о том, для кого был сформирован токен, обычно данные о пользователе
Зарегистрированные утверждения не являются обязательными, но рекомендуются к использованию: iss (издатель), exp (время истечения), aud (аудитория) и другие
Публичные утверждения могут быть установлены любыми службами, но их следует зарегистрировать в IANA
Приватные утверждения тоже могут быть использованы для передачи данных, но не включены ни в одну из предыдущих категорий
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
Данная часть тоже кодируется в Base64Url
Элементы токена, подпись #
Подпись строится из следующих элементов:
- Берётся Base64-строка заголовка
- Берётся Base64-строка содержимого
- Объединяется и выполняется подпись
Например, при использовании алгоритма HMAC SHA256 подпись строится так:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
Полученные элементы объединяются через точки для получения JWT-токена:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Операции над токеном #
Для реализации работы с токеном приложению необходимо выполнять операции:
- Создание нового токена в случае успешной аутентификации пользователя
- Проверять переданный от пользователя токен
- Извлекать данные из переданного пользователем токена
Для реализации в приложении можно воспользоваться одной из библиотек
В рамках курса рассмотрим библиотеку java-jwt
- Данную библиотеку можно использовать для создания коммерческих решений
- Можно использовать на любой JVM-платформе
- Поддерживает много стандартов шифрования токена
Библиотека 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){
// Неправильная подпись или утверждения
}
Авторизация действий в приложении #
- Каждый запрос в протоколе HTTP выполняется независимо друг от друга
- Веб-браузер посылает с каждым запросом данные с помощью механизма куков
- В куках может храниться JWT-токен с аутентификационными данными
- При получении запроса приложению необходимо:
- Извлечь JWT-токен из целевой куки
- Верифицировать подпись JWT-токена
- Извлечь идентификационные данные для JWT-токена
- На основании идентификационных данных выносится решение о доступе к запросу
- Шаги № 1-3 являются общими для множества запросов, их следует расположить в фильтре для всех обработчиков, которые
- Шаг № 4 может быть выполнен на любом уровне обработки запросов
Уровни авторизации действий #
При разработке приложений на библиотеке 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-запроса:
- На этапе обработки JWT-токена можно получить уникальный идентификатор пользователя
- На этапе определения роли по полученному идентификатору можно определить роль пользователя
- На уровне обработчика HTTP-запроса можно учесть фактор владения данными
Передача данных из фильтра #
Базовый интерфейс фильтров в http4k передаёт данные внутреннему HTTP-обработчику только в формате объекта Request, передача дополнительных данных через этот интерфейс не предусмотрена
- Передавать данные строками через Request
- Придумывать своё собственное дополнительное хранилище
- Воспользоваться хранилищем контекстных данных в http4k
Рассмотрим последний вариант
Работа хранилища контекста запросов #
Хранилище контекстных данных позволяет удобным образом хранить данные для одного запроса, удовлетворяя требованиям поточной безопасности
Для записи и извлечения данных из хранилища можно воспользоваться линзами
Данные в хранилище типизированы, т.е. не надо выполнять преобразование данных много раз
По окончании обработки запроса хранилище очищается
Особенности реализации #
- Объект хранилища запросов необходимо создать при запуске приложения
- Можно определить ряд линз, позволяющих сохранять и считывать данные из контекста
- Добавить в начало цепочек фильтров фильтр, инициализирующий работу хранилища для запроса
Рассмотрим пример
Линзу для взаимодействия с хранилищем необходимо предоставить соответствующим 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)
Настроенную линзу необходимо передать в фильтры или обработчики, которым нужен доступ
Доступ к контексту на уровне вида #
На уровне вида приложение может захотеть иметь информацию:
- Об имени пользователя
- О статусе авторизации пользователя
Для всех страниц, отображаемых пользователю (для отображения информации в навигационной панели)
На настоящий момент подсистема вида не поддерживает возможности прямого доступа к хранилищу контекста, поэтому предлагается собственный подход к формированию слоя отображения
- Создать собственный компонент отображения Pebble-шаблонов
- Ассоциировать с данным компонентом линзы для доступа к контексту запроса
- Получить данные из контекста для передачи в вид
- Использовать данные внутри вида