В сегодняшней статье я расскажу о том, как использовать Spring AOP для авторизации запросов API на уровне конечной точки.

Отказ от ответственности. Цель этой статьи — дать вам практический пример использования АОП, а не подробно объяснять все концепции.

Фон

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

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

Для простоты у нас есть только два полномочия: USER и ADMIN, и мы можем считать, что процесс аутентификации уже заполняет Spring Security Context правильными предоставленными полномочиями на основе комбинации имени пользователя и пароля.

Проблема

Некоторым конечным точкам нашего API требуются полномочия USER, а другим — ADMIN (для управления пользователями и других административных требований).

Как мы можем получить авторизацию для наших таможенных органов?

Готовое решение

С Spring мы можем использовать хорошо известную аннотацию @PreAuthorize на уровне конечной точки:

Как видите, мы указываем hasAuthority('USER') как значение для аннотации @PreAuthorize, что означает: аутентифицированный пользователь должен иметь полномочия USER для доступа к этой конечной точке. Если этот орган отсутствует, будет возвращено 403 Forbidden.

Основной проблемой при указании значения авторитета в виде обычного текста является удобство сопровождения и тот факт, что он подвержен опечаткам и критическим изменениям. Представьте, что мы хотим реорганизовать имя органа с USER на CUSTOMER в SecurityAuthorities… это означает, что вам нужно убедиться, что вы нашли все места, где находится строка 'USER', и заменить ее на 'CUSTOMER'; добавьте тот факт, что вам нужно сделать это в 25 разных местах, и это быстро станет болью. Так почему бы вместо этого не использовать наш класс перечисления?

Использовать класс перечисления немного проще, чем обычный текст, потому что во время компиляции вы можете поместить правильное значение после полного имени пакета, и вам не нужно возиться с опечатками. Кроме того, если вы переименуете имя авторитета или переместите перечисление в другой пакет, изменения будут отражены здесь…НО, если вы удалите перечисление, компилятор вообще не будет жаловаться, и это большая проблема, потому что скрывает тот факт, что ваша конечная точка ожидает полномочия или полномочия, которых больше не существует.

Даже если аннотация @PreAuthorize решает процесс авторизации и ее довольно просто использовать, нам по-прежнему нужно более чистое и удобное в сопровождении решение, чтобы использовать наши значения перечисления напрямую, без уточнения имен пакетов или жестко закодированных строк, и в то же время для безопасного времени компиляции. АОП приходит на помощь!

АОП-решение

Что такое АОП

Аспектно-ориентированное программирование (АОП) дополняет объектно-ориентированное программирование (ООП), предлагая другой взгляд на структуру программы. Ключевой единицей модульности в ООП является класс, тогда как в АОП единицей модульности является аспект. Аспекты обеспечивают модульность таких задач, как управление транзакциями, охватывающих несколько типов и объектов. — от Весенних Документов

Я знаю, что определение, данное Spring Docs, может быть немного трудным для понимания, но, если коротко, АОП позволяет вам добавлять дополнительное поведение к существующему коду без изменения самого кода.

Что мы будем делать в основном, так это реализовать метод, который будет вызываться АОП автоматически всякий раз, когда новый запрос поступает к нашей конечной точке, и который получает набор полномочий для проверки аутентифицированного пользователя (Principal), чтобы решить результат авторизация. Имейте в виду, что существующий код для нашей конечной точки претерпит только одно незначительное изменение: замена аннотации @PreAuthorize на пользовательскую. Давайте продолжим…

Пользовательская аннотация

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

Далее нам нужно обновить наши конечные точки следующим образом:

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

Метод АОП

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

Обратите внимание на аннотацию @Aspect на уровне класса и аннотацию @Before на уровне метода. Первый отмечает класс, который будет использоваться Spring AOP, а второй переводится следующим образом: вызывать метод hasAuthorities перед вызовом каждого метода, аннотированного @HasEndpointAuthorities, который является частью класса, аннотированного только @RestController. Поэтому всякий раз, когда на нашу конечную точку /api/v1/expenses поступает запрос, метод hasAuthorities будет автоматически вызываться перед фактическим вызовом метода getExpenses. Если hasAuthorities выдает исключение (403 Forbidden), то getExpenses больше не будет вызываться, и ответ 403 будет возвращен автоматически.

Вот и все! Теперь вы можете контролировать, какие полномочия проверять для каждой конечной точки, просто используя одну строку кода с помощью пользовательской аннотации, и вы также можете в полной мере воспользоваться классом enum.

Заключение

Как всегда, имейте в виду, что этот подход может соответствовать или не соответствовать контексту или потребностям вашего проекта, и я не могу сказать, что нет другого способа сделать это по-другому или лучше. Я очень надеюсь, что вам понравилось, и вы получили удовольствие от чтения.