Аутентификация и авторизация в ASP.NET Web API / Хабр

Описание основных используемых аннотаций

Controller

Что будет представлять из себя приложение

Сайт со следующими страницам:

Создание нового проекта в IDE

Мы будем использовать систему сборки Maven.

Под GroupId подразумевается уникальный идентификатор компании (или ваше личное доменное имя), которая выпускает проект. ArtefactId – это просто название нашего проекта.

После завершения создания проекта отроется файл pom.xml, Idea предложит включить автоимпорт – не отказывайтесь. В этом файле будут содержаться все зависимости (библиотеки), используемые в проекте.

Создание структуры проекта (пакетов)

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

Теперь коротко о том, что будет храниться в каждом пакете:


Рассмотрим файл

pom.xml

. В этом файле нужно указать ссылку на родительский файл с помощью тега

parent

, т.о. все свойства и зависимости родителя будут добавлены в этот дочерний файл.

Далее добавляем зависимости для работы модулей Spring, драйвер БД PostgreSQL, сервера Tomcat, JSTL.


По умолчанию maven будет использовать старую версию java 1.6, чтобы это исправить указываем версию явно.

Также добавляем плагин, позволяющий упаковывать архивы jar или war и запускать их «на месте»:

Заполним файл

application.properties

1. Добавление сущностей (моделей)

Обязательное требование для всех сущностей: приватные поля, геттеры и сеттеры для всех полей и пустой конструктор (в примерах не представлены). Их не нужно писать вручную, нажмите Alt Insert и Idea сделает это за вас.

Для импорта необходимых классов и библиотек используем комбинацию клавиш Alt Enter.

Аутентификация и авторизация в ASP.NET Web API / Хабр

2. Реализация слоя доступа к данным и сервисного слоя

Spring Data предоставляет набор готовых реализаций для создания слоя, обеспечивающего доступ к БД. Интерфейс

JpaRepository

предоставляет набор стандартных методов (findBy, save, deleteById и др.) для работы с БД.

3. Добавление контроллеров


Для страниц, которые никак не обрабатываются сервером, а просто возвращают страницу, маппинг можно настроить в конфигурации. Страница

login

обрабатывается Spring Security контроллером по умолчанию, поэтому для неё отдельный контроллер не требуется.

@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/login").setViewName("login");
        registry.addViewController("/news").setViewName("news");
    }
}

RegistrationController.

Отдельный контроллер нужен для страницы регистрации. Для обработки GET запроса используется аннотация

@GetMapping(“/registration”)

, для POST –

@PostMapping(“/registration”)


Чтобы что-то добавить или получить со страницы мы обращаемся к model. В GET запросе на страницу добавляется новый пустой объект класса

4. Добавление представлений

index.jsp

Запуск приложения

В main класс Application нужно добавить следующее:

Auth-сервис

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

Пользователи будут храниться в БД. В идеале это должна быть отдельная БД, но в обучающих целях будем использовать ту же, что использовали в первой части. Опишем DAO пользователя:

@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "clients")
public class ClientEntity {
    @Id
    @Column(name = "client_id")
    private String clientId;
    private String hash;
}

Репозиторий будет выглядеть совсем просто:

public interface ClientRepository
        extends CrudRepository<ClientEntity, String> {
}

Т.к. мы будем реализовывать Client Credentials Flow в терминах протокола OAuth, то здесь client — это и есть пользователь. Соответственно clientId, clientSecret — аутентификационные данные пользователя. В открытом виде пароль пользователя хранить нельзя, поэтому будем хранить некий хеш, о котором будет написано ниже.

Опишем сервис, который будет регистрировать нового клиента и проверять его аутентификационные данные:

public interface ClientService {
    void register(String clientId, String clientSecret);
    void checkCredentials(String clientId, String clientSecret);
}

Для правильного хранения паролей в БД будем использовать Bcrypt. Это криптографическая хеш-функция, основанная на шифре Blowfish. Воспользуемся реализацией в библиотеке jBCrypt, добавим зависимость в проект:

dependencies {
    ...
    implementation group: 'org.mindrot', name: 'jbcrypt', version: '0.4'
}

Реализуем интерфейс ClientService:

Jwt (json web tokens)

JWT — это стандарт, основанный на JSON, для создания токенов доступа. Он состоит из трех частей: заголовка, тела и подписи:

В заголовке указывается тип токена и алгоритм подписи:

{
  "typ": "JWT",
  "alg": "HS256"
}

В тело записывается необходимая пользовательская информация (payload). Для аутентификации и авторизации это может быть id пользователя, его роль, время действия токена:

Авторизация

Нам осталось добавить авторизацию в написанное приложение Bookstore. Откроем проект из предыдущей части и добавим в зависимости библиотеку для работы с JWT (как было показано выше). Также добавим две новых настройки в application.properties:

auth.enabled=true
auth.jwt.secret=30faa058f27f690c7e9a098d54ebcfb3d8725bcb85ee7907a2d84c69622229e2

Одна из них вам уже знакома, а вторую будем использовать для включение/отключения авторизации.

Для проверки полученного токена опишем интерфейс TokenService и его реализацию:

public interface TokenService {
    boolean checkToken(String token);
}

@Service
@Slf4j
public class DefaultTokenService implements TokenService {
    @Value("${auth.jwt.secret}")
    private String secretKey;

    @Override
    public boolean checkToken(String token) {
        Algorithm algorithm = Algorithm.HMAC256(secretKey);
        JWTVerifier verifier = JWT.require(algorithm).build();

        try {
            DecodedJWT decodedJWT = verifier.verify(token);
            if (!decodedJWT.getIssuer().equals("auth-service")) {
                log.error("Issuer is incorrect");
                return false;
            }

            if (!decodedJWT.getAudience().contains("bookstore")) {
                log.error("Audience is incorrect");
                return false;
            }
        } catch (JWTVerificationException e) {
            log.error("Token is invalid: "   e.getMessage());
            return false;
        }

        return true;
    }
}

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

Закроем все эндпоинты и разрешим доступ только при наличии авторизационного токена. Значение авторизационного токена необходимо положить в заголовок следующим образом:

Authorization: Bearer <значение токена>

Осталось написать фильтр, который будет осуществлять чтение заголовка и принимать решение об авторизации запроса:

Авторизация: «проход разрешен»

После аутентификации система разрешит вам читать письма в вашем почтовом ящике. А пропущенного охранником посетителя примут секретари. Если он пришел устраиваться на работу, они пригласят эйчара, если забрать посылку — отдадут посылку, если на встречу — проводят в переговорную и т. д. Это авторизация: предоставление пользователю прав доступа к определенным ресурсам.

Авторизация не просто дает вам возможность зайти в систему, но и разрешает совершать там определенные операции: читать документы, отправлять письма, изменять данные — под тем самым идентификатором, который вы предъявили в самом начале.

Архитектору безопасности на заметку

Задача данного исследования и подготовленного обзора заключается в том, чтобы предоставить Архитектору безопасности лучшие практики для реализации модели АА в разрабатываемом продукте и оставить его наедине с вопросом: «Что из этого лучше подходит к моему приложению?».

При этом, мы не исключаем, что на этот же вопрос «Что из этого лучше подходит к моему приложению?» Архитектор ответит: «Кажется, что ничего не подходит. Придется делать свою модель с блэк-джеком». И в итоге, разработанная им модель окажется в данном списке в качестве новой «best practice» — смело делай pull requests в соответствующую шпаргалку от OWASP.

В полном тексте научной публикации, помимо рекомендаций архитектору по безопасности приложений по выбору подходящего архитектурного шаблона, можно найти полный перечень источников информации (публикации, выступления на конференциях), которые были проанализированы в рамках данного исследования.

Всё хорошо?

Почти. Данная система по-прежнему не защищает от кражи хэша из кук при открытом канале и отсутствии SSL. Кроме того, сложно обеспечить своевременную блокировку, если доступ к аккаунту утерян (особенно если взломщик уже сменил пароль). В этом случае очень помог бы сброс пароля через смс-команду с привязанного телефона — но для этого уже нужна возможность принимать и отправлять смс.

А если она есть — то можно сразу внедрить двухфакторную авторизацию, которая снимет массу проблем. Но не проблему с необходимостью в SSL — поскольку всё равно результатом любой авторизации является получение некого токена доступа, работающего ограниченное время. И этого времени может быть достаточно, чтобы сделать от нашего имени что-то, что нам не понравится.

Делегирование полномочий

Давайте попробуем создать отдельный модуль. Назовём его совершенно бесхитростно — security.php, и оформим как отдельный скрипт. Будем подключать его ко всем закрытым страницам нашего проекта в самом начале. Внутри этого файла будем анализировать некие условия, а по итогам его работы выставлять специальный флаг в 0 или 1. Пусть этот флаг будет храниться в переменных сессии (массив $_SESSION в PHP).

Что нам это даёт? Мы можем запихать в этот скрипт сколь угодно хитрую логику, вплоть до анализа последних действий пользователя и добавления его в бан-лист по IP, либо блокировки его аккаунта на тот или иной срок. Но сперва реализуем очень базовую функциональность: будем сверять значение хэша, пришедшего из куки, с тем, что должно было бы получиться, если хэш не был искажён. Сервер знает IP, знает юзер-агент, знает пароль текущего пользователя… Кажется, всё готово!

Децентрализованный шаблон

В этом шаблоне команда разработчиков реализует PDP и PEP непосредственно на уровне кода микросервиса (рис. 3). Все правила контроля доступа, а также атрибуты, необходимые для реализации этого правила, определяются и хранятся в каждом микросервисе (шаг 1). Когда микросервис получает (шаг 2) запрос на доступ вместе с некоторыми метаданными авторизации (например, контекст конечного пользователя), микросервис анализирует его (шаг 3) для того, чтобы сгенерировать решение политики контроля доступа, а затем реализует (enforce) авторизацию (шаг 4).
image
Рисунок 3. Высокоуровневая архитектура децентрализованного шаблона

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

Соответственно, данная модель несет в себе следующие ограничения:

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

Идентификация: «стой! кто идет?»

Когда посетитель приходит в офис компании «Доктор Веб», он представляется охраннику на входе. Таким образом человек идентифицирует себя — сообщает, кто он такой. Охраннику неважно, зачем он пришел — работать, забрать посылку или на встречу. Он должен просто проверить, что этот человек находится в списке допущенных к проходу.

Используемые источники

  1. Registration and Login with Spring Boot, Spring Security, Spring Data JPA, Hibernate, MySQL, JSP, Bootstrap and Docker Compose
  2. Обратная сторона Spring
  3. Spring Security/Технический обзор Spring Security
  4. Официальная документация Spring
  5. Учимся готовить: Spring 3 MVC Spring Security Hibernate

Как быть?

Очевидно, с этим надо что-то делать. Да, можно сразу бежать покупать сертификат и подключать SSL. Но можно сделать кое-что ещё до этого, и существенно снизить тем самым необходимость в нём. В конце концов, в том же ВКонтакте SSL стал принудительным всего полгода назад, а до этого как-то ведь жили.

Первое, что приходит на ум — хранить вместо пароля хэш от него. Это не особо поможет, если кто-то перехватит трафик, и не поможет при краже кук. Но вот в ситуации «кто-то открыл настройки и прошёлся по кукам, пытаясь запомнить значения» — сильно усложнит жизнь злоумышленнику.

Можно пойти дальше, и при создании хэша использовать юзер-агент. Теперь взломщику придётся использовать браузер той же версии, что и у нас (а в случае с IE ещё и с тем же набором плагинов тех же самых версий, что выходит уж очень маловероятно, особенно если взломщик не догадался подсмотреть юзер-агент).

Кастомные фильтр авторизации


Кастомный фильтр должен быть унаследован от одного из следующих типов:

Следующая диаграмма показывает иерархию классов фильтров:
Аутентификация и авторизация в ASP.NET Web API / Хабр

Межсервисная аутентификация

Существует два общих способа реализации межсервисной аутентификации:

В подходе mTLS каждый микросервис может уверенно определить, с кем он общается, в дополнение к достижению конфиденциальности и целостности передаваемых данных. Каждый микросервис при старте должен получать свою пару открытый/закрытый ключей и открытый ключ доверенного центра сертификации, которые в дальнейшем используются для аутентификации микросервиса получателя через mTLS.

Немного информации о spring security


Самым фундаментальным объектом является

SecurityContextHolder

. В нем хранится информация о текущем контексте безопасности приложения, который включает в себя подробную информацию о пользователе (принципале), работающим с приложением. Spring Security использует объект

Authentication

, пользователя авторизованной сессии.

Предыстория

Осень 2022-ого. Примерно полтора года назад, когда мне случилось стать участником разработки проекта, где пользователей существенно больше пары десятков человек, я наконец-то впервые в своей жизни задумался о надёжности авторизации.

По сути, авторизация — это то, с чего начинается процесс взаимодействия зарегистрированного пользователя с системой (для незарегистрированного пользователя всё начинается с регистрации, и эти два процесса, как вы уже догадались, очень сильно взаимосвязаны). Я внезапно осознал, что во всех проектах, что я делал до этого, с безопасностью всё очень плохо.

Стандартная реализация

Итак, сессия в PHP по умолчанию хранится в файле. Её id сохраняется в cookie (если куки отключены — задействуется механизм передачи через адресную строку, если это не отключено в конфигурации). Время жизни такой сессионной куки по умолчанию — до момента закрытия браузера (и обычно его никто не меняет).

Поэтому более продвинутые программисты реализуют галочку «запомнить меня», либо реализуют её функционал по умолчанию, без возможности отключить. Что они делают? Просто сохраняют в собственной куке айди пользователя. Но поскольку просто айди хранить как-то уж слишком стрёмно (любой может поставить любое число и получить доступ к произвольному аккаунту), то часто вместе с айди за компанию сохраняют и пароль. В открытом виде.

Если кто-то не понимает, чем это плохо — представьте себе, что у нас пользователь пользуется очень старым, либо очень плохо реализованным браузером. Мы ведь не можем гарантировать, что браузер надёжно шифрует куки? Да и вирусы всякие могут быть у пользователя на компьютере, и в случае особо серьёзной малвари может не спасти и шифровка — зловред может попытаться считать значения прямо из памяти, когда они расшифрованы.

Человек, случайно оказавшийся у вашего компьютера в ваше отсутствие — сможет просто скопировать себе все интересующие куки, и в новых браузерах этот процесс стал ещё проще. Дыры в безопасности браузера могут потенциально привести к тому, что ваша кука станет доступна стороннему сайту (да, сейчас это крайней маловероятно, но чем чёрт не шутит).

Ну и самое главное — если у нас SSL используется только при авторизации (а на остальных страницах его решили отключить ради выигрыша в скорости, либо чтобы лучше работало промежуточное кэширование)… То наш пароль всё время передаётся открытым текстом.

Теория

Аутентификация и авторизация

Понятия “аутентификация” и “авторизация” часто употребляются как синонимы, но на самом деле они обозначают два разных процесса.

Если кратко, то аутентификация — это проверка того, кем является пользователь, т.е. определение того, что пользователь зарегистрирован в нашей системе, о чем, в частности, может свидетельствовать наличие данных пользователя в нашей базе данных. Авторизация же — это процесс определения полномочий пользователя, т.е. того, к чему пользователь имеет доступ, какие данные нашего приложения он может просматривать, редактировать, удалять и т.д.

В качестве примера сравнения аутентификации и авторизации из реальной жизни можно привести систему безопасности аэропорта. Сначала мы предъявляем паспорт для подтверждения нашей личности (аутентификация, идентификация), затем посадочный талон для подтверждения нашего доступа на самолет (авторизация).

Признаки аутентификации:

Признаки авторизации:

Таким образом, для доступа к приложению требуется как аутентификация, так и авторизация. Если пользователь не может подтвердить свою личность (идентичность, identity), он не будет иметь доступа. И даже если пользователь подтвердил свою личность, но не авторизовался, в доступе ему будет отказано.

Токен

Аутентификация и авторизация в ASP.NET Web API / Хабр

Под токеном в рамках настоящей статьи подразумевается JSON Web Token.

JWT — это открытый стандарт (RFC 7519), определяющий компактный и автономный способ безопасной передачи данных между сторонами в виде объекта формата JSON. JWT — это стандарт, т.е. все JWT являются токенами, но не все токены являются JWT.

Установка контекста безопасности


Если ваше приложение выполняет какую-либо логику связанную с аутентификацией, вы должны задать контекст безопасности в двух свойствах:

Следующий код показывает как задать контекст безопасности:

Цели исследования

Архитектура микросервисов все чаще используется для проектирования и внедрения прикладных систем как в облачных, так и в локальных инфраструктурах, крупномасштабных приложениях и сервисах. Достаточно изучить Хабр на предмет различных практик, связанных с разработкой распределенных и отказоустойчивых систем, чтобы убедиться в повсеместном внедрении микросервисной модели.

На этапах разработки и внедрения программного продукта необходимо решить множество проблем безопасности. Основополагающими требованиями, над которыми ломает голову aрхитектор безопасности (Application Security Architect – роль в крупной продуктовой компании, которая еще не избавляет от технических задач Security Engineer’a, но которая уже добавляет высокоуровневые проблемы, связанные с архитектурой и процессами) и которые должны быть решены на этапе проектирования, являются аутентификация и авторизация (для краткости, АА).

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

Исследование проводится с учетом трех ключевых вопросов:

В ходе исследования мы провели обзор основных электронных библиотек научных статей, мы также просмотрели стандарты безопасности, презентации на крупных конференциях по безопасности и локальных тематических митапах (привет, OWASP Moscow Meetup).

Итогом стали следующие результаты:

Централизованная модель со встроенной точкой принятия решения

В этой модели правила контроля доступа определяются централизованно, но хранятся и оцениваются на уровне микросервисов (рисунок 5). Правила контроля доступа определяются с помощью PAP (шаг 1) и доставляются во встроенную PDP вместе с атрибутами субъектов и объектов доступа, которые необходимы для использования этих правил (шаг 2). Когда субъект вызывает микросервиса (шаг 3), код микросервиса вызывает PDP, а PDP генерирует решение политики контроля доступа, оценивая входной запрос по правилам и атрибутам контроля доступа (шаг 4). На основе решения PDP микросервис применяет авторизацию (шаг 5).
image
Рисунок 5 Высокоуровневая архитектура централизованной модели со встроенной PDP

PDP код в этом случае может быть реализован как встроенная библиотека микросервиса или «sidecar-прокси». Sidecar обрабатывают коммуникации между сервисами, производят мониторинг и устраняют проблемы безопасности, то есть все, что может быть абстрагировано от отдельных сервисов, подробнее можно почитать тут.

В связи с возможными отказами сети или хоста и задержками в сети рекомендуется реализовать встроенную PDP в виде микросервисной встроенной библиотеки или sidecar на одном хосте с микросервисом. Встроенная PDP обычно хранит политику авторизации и связанные с политикой данные в памяти, чтобы уменьшить количество внешних зависимостей во время авторизации и для получения низкой задержки.

Основное отличие от “централизованного шаблона с единой точкой принятия решения” с подходом к кэшированию заключается в том, что решения по авторизации не хранятся на стороне микросервиса, вместо этого на стороне микросервиса хранится актуальная политика авторизации.

В выступлении “How Netflix Is Solving Authorization Across Their Cloud” представлен практический пример применения шаблона “Централизованную модель со встроенной PDP” для реализации авторизации на уровне микросервисов (рисунок 6):

Преимущества этих шаблонов такие же, как и для “Централизованных шаблонов с одним PDP”. Кроме того, шаблон не сильно влияет на задержки в обслуживании сетевых запросов из-за встраивания PDP на уровне микросервисов.

Существует несколько проблем, которые необходимо учитывать при применении этой модели:

  • этот шаблон опирается на ручные или полуручные правила политики доступа, разработанные командой безопасности, которые могут быть подвержены ошибкам — во избежание уязвимостей конфигурации необходимо применять методы тестирования и верификации безопасности;
  • архитекторы безопасности приложений должны комбинировать его с другими шаблонами (например, авторизация на пограничном уровне), чтобы избежать наличия единой точки принятия решений и обеспечить соблюдение принципа “эшелонированной обороны”;
  • может случиться так, что некоторые специфические для бизнеса правила контроля доступа не могут быть реализованы таким образом — архитекторы безопасности приложений должны комбинировать этот шаблон с “Децентрализованным шаблоном”;
  • архитекторы безопасности приложений должны выбрать подход, как получать обновления политик авторизации из централизованных PAP (например, опрос PAP или механизм публикации-подписки);
  • команда разработчиков должна безопасно использовать сторонние компоненты авторизации и описывать политику контроля доступа, используя некий формальный язык, который в некоторых случаях может быть накладным — “Децентрализованный шаблон” может быть достаточным для реализации некоторой простой политики контроля доступа.

Централизованный шаблон с единой точкой принятия решения

В этой модели правила контроля доступа определяются, хранятся и оцениваются централизованно (рисунок 4). Правила контроля доступа определяются с помощью PAP (шаг 1) и доставляются в централизованную PDP вместе с атрибутами субъектов и объектов доступа, которые необходимы для использования этих правил (шаг 2).

Когда субъект вызывает микросервис (шаг 3), код микросервиса вызывает централизованный PDP через сетевой вызов, а PDP генерирует решение политики контроля доступа, оценивая входной запрос по правилам и атрибутам контроля доступа (шаг 4). На основе решения PDP микросервис реализует авторизацию (шаг 5).

image
Рисунок 4. Высокоуровневая архитектура централизованного шаблона с единой PDP

Некоторыми преимуществами этой схемы являются:

  • команды разработчиков и продуктовой безопасности могут обновлять правила контроля доступа без изменения исходного кода, это позволяет централизованно управлять политиками, изменения в политиках могут быть развернуты отдельно от микросервисов;
  • определения правил контроля доступа можно оставить для реализации команде разработчиков, но при этом вынести за пределы основной части исходного кода, чтобы политики были доступны для периодического аудита, более того, некоторые правила контроля доступа являются универсальными для системы и могут совместно использоваться в микросервисах всей организации;
  • правила контроля доступа могут использоваться в среде эксплуатации для обнаружения аномалий безопасности, например, аномальное поведение микросервисов, основанное на вызовах API, при обнаружении угроз можно динамически воссоздавать и применять новые правила контроля доступа для снижения риска безопасности.

Для определения правил контроля доступа команда разработчиков и DevOps-инженеров должна использовать язык или нотацию. Примером может служить язык разметки Extensible Access Control Markup Language (XACML) и Next Generation Access Control (NGAC), который является стандартом для реализации описания правил политики.

Этот шаблон плохо влияет на время обслуживания запросов из-за дополнительных сетевых вызовов PDP; использование кэширования решений политики авторизации на стороне микросервиса позволяет уменьшить сетевые задержки.

Следует отметить, что PDP должен работать в режиме высокой доступности из-за возможных проблем с отказоустойчивостью. Архитекторы безопасности приложений должны комбинировать его с другими шаблонами (например, авторизация на уровне шлюза API), чтобы избежать наличия единой точки принятия решений и обеспечить соблюдение принципа “эшелонированной обороны”.

Похожее:  Сеть автодорог

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *