Spring Security — must-have компонент в Spring-приложениях, так как он отвечает за аутентификацию пользователя, а также за авторизацию тех или иных его действий в системе. Одним из методов авторизации в Spring Security является использование аннотации @PreAuthorize, в которой с помощью выражений можно наглядно описать правила, следуя которым модуль авторизации решает, разрешить ли проведение операции или запретить. В моём REST-сервисе возникла необходимость предоставить точку доступа к описанию правил авторизации для всех методов контроллеров сервиса. Причём, по возможности, избежать раскрытия специфики именно SpEL-выражений (т.е., вместо permitAll нужно что-то вроде anybody, а principal избегать вовсе как избыточное выражение), но возвращать свои выражения, с которыми уже можно делать что угодно. Давайте представим небольшой сервис и правила доступа к нему. Имея в наличии только результат логических выражений, нельзя получить описания самого правила. Нужно каким-то образом определить, какие правила отвечают за конкретное действие. Допустим, мы решаем использовать не boolean, а объект, описывающий правило на более высоком уровне. К такому объекту у нас было бы всего два требования: Исходя из требований, нам достаточно иметь в распоряжение что-то типа: И слегка изменить компонент авторизации: К сожалению, в обычном режиме @PreAuthorize работает так, что его выражения могут возвращать только булевские значения. Поэтому при обращении к методам сервиса получится следующее исключение: В первую очередь для устранения проблемы с небулевыми значениями нужно настроить GlobalMethodSecurityConfiguration, потому как он позволяет настроить контекст вычисления выражений в @PreAuthorize. За это отвечает т.н. TypeConverter, к которому довольно просто добраться: Здесь есть несколько моментов. Во-первых, мы будем использовать стандартный DefaultMethodSecurityExpressionHandler, который сделает большую часть за нас, просто переопределив возвращаемый им контекст. Во-вторых, предок DefaultMethodSecurityExpressionHandler, а именно AbstractSecurityExpressionHandler, запрещает создание своего контекста, но мы можем создать т.н. внутренний контекст, в котором и переопределим TypeConverter. В-третьих, нам нужно написать forwarding-декоратор для StandardEvaluationContext, чтобы не сломать поведение оригинального контекста: Да, Java здесь не лучше выглядит, и было бы здорово, если бы Java умела by как Kotlin, чтобы не нужно было писать так много. Или, например, @Delegate из Lombok. Можно, конечно, было использовать и поле, а не абстрактный метод, но абстрактный метод мне кажется чуточку гибче (впрочем, я не знаю, умеют ли Kotlin и Lombok делегировать к методу, возвращающему декорируемый объект). Два класса выше я бы отнёс с “библиотечному” слою, т.е. его можно использовать в нескольких приложениях отдельно и настраивать в каждом приложении под свои нужны. И вот уже в “слое приложения” теперь можно без проблем добавить свой конвертер из IAuthorizationExpression в boolean: Теперь IAuthorizationExpression работает как boolean и может участвовать в логических операциях напрямую. Поскольку у нас уже есть в наличии объект-выражение, мы можем расширить его базовый функционал и создать выражения, сложнее чем SimpleAuthorizationExpression. Это позволит нам комбинировать выражения любой сложности, при этом соблюдая требования, указанные выше. Мне всегда нравились fluent-интерфейсы, методы которых можно объединять удобным образом. Например, Mockito и Hamcrest используют такой подход вовсю. И Java теперь тоже для базовых интерфейсов типа Function, Supplier, Consumer, Comparator и т.д. Java 8 привнесла в язык кучу хороших возможностей, и одну из них, а именно методы по умолчанию, можно использовать для расширения базового функционала выражений. Например, можно добавить в IAuthorizationExpression простой предикат AND: Как видно, операция AND строится очень просто. В методе mayProceed можно просто получить результат композиции выражений с помощью &&, а в toHumanReadableExpression — сформировать строку именно для этого выражения. Теперь можно комбинировать выражение, используя операцию AND, например, так: И в тот же момент строковое представление для такого выражения будет: Весьма неплохо. Так же без проблем можно добавить поддержку операции OR или унарной операции NOT. Кроме того, можно создавать и более сложные выражения без тривиальных операций, поскольку SimpleAuthorizationExpression не имеет большого смысла. Например, выражение, определяющее, является ли пользователь root-ом: Или является ли строка, представленная переменной name, запрещённой: Теперь правила авторизации могут быть представлены иначе: Код вполне читаем и прекрасно понятно, что именно делают эти выражения. А вот как выглядят сами строковые представления: Аналогия на лицо, верно? Теперь осталось только получить эти выражения во время выполнения приложения. Допустим есть отдельная возможность получить список всех методов, которые нам нужны (там может быть много специфики, и она нас сейчас не очень интересует). Имея такой набор методов, остаётся просто “виртуально” выполнить выражения из @PreAuthorize, заменив недостающие переменные какимо-либо значениями. Например: Этот код немного сложнее, но в действительности в нём нет ничего сложного. Трудности могут возникнуть разве с нахождением недостающих переменных в для выражений (например, #name из примеров выше). Вместо <...> нужно подставить свою реализацию для подстановки аргументов в параметры. На самом деле, здесь часто можно обойтись и просто null-ом, но в некоторых случаях такое решение не работает из-за понятных причин. И ещё одна не очень приятная особенность: нужно вручную ещё дополнительно создать ForwardingEvaluationContext за тем же принципом, что и ForwardingStandardEvaluationContext выше: Вот и всё: мы добавили поддержку произвольных типов в @PreAuthorize в качестве результата вычисления выражения или в качестве операндов, а также извлекать удобные для прочтения человеком представления этих выражений. На самом деле, как говорилось выше, в моём приложении правилами авторизации обвешаны методы в контроллерах, а не в сервисах. Поскольку я использую Springmvc-router, получение списка методов не составляет для меня большого труда. Насколько мне известно, просто это сделать и стандартными средствами, что позволяет исследовать не только контроллеры, но и сервисы и вообще разные компоненты. Поэтому способ получения всех методов, проаннотированных @PreAuthorize, остаётся на личное усмотрение. Я также не люблю открытые конструкторы и всегда им предпочитаю статические фабричные методы из-за возможности: “Effective Java” — замечательная книга. А наличие final и nullability-аннотаций — это дело привычки, и как бы мне хотелось считать — привычки хорошего тона. И последнее. В @PreAuthorize я предпочитаю использовать просто вызовы методов авторизирующих компонентов вместо сложных выражений. Во-первых, это позволяет дать некоторое имя правилу авторизации, а также избежать в некоторых случаях строковых констант, которые наверняка захочется использовать (но лично я считаю, что не надо). Во-вторых, это позволяет собирать правила авторизации в определённые группы и соответствующе к ним обращаться. В-третьих, правила авторизации могут быть слишком длинными, что вряд ли упростит сопровождение таких выражений в @PreAuthorize. К тому же, компилятор скорее сам отловит имеющиеся ошибки, не оставляя их на время выполнения. Да и любимая IDE лучше раскрасит такие выражения.