Предыстория
Мы разрабатываем небольшой портал на Grails и используем Spring Security для управления безопасностью. Плагин spring-security для Grails достаточно удобен и до последнего момента от него не требовалось сложной функциональности.
Недавно был обнаружен неприятный момент в использовании аннотаций @Secured для методов контроллеров Grails. Проблема заключается в том, что аннотации обрабатываются во время исполнения и преобразуются в набор правил для адресов «Адрес -> Набор требуемых ролей». Такой подход порождает ряд проблем в Grails-контроллерах в действиях сохранения/удаления данных, поскольку они отправляют данные на основной URL контроллера, то приходиться во-первых аннотировать контроллер, во вторых — невозможно задать различные ограничения для таких запросов.
Речь пойдёт о том, как решить проблему и приобрести хороший инструмент для управления правилами безопасности.
Возможные решения
Я не понимаю, почему разработчики плагина для Grails поступили так халатно в отношении пользователей, наверное им просто было так проще.
Альтернативные решения:
- AOP для контроллеров (сложно конфигурировать большое количество правил)
- Генерация байт-кода по аннотациям во время компиляции
Второй подход неприятно реализовывать в Java, поскольку требуется организовывать этапы сборки. Но только не в Groovy. В Groovy для таких целей принято использовать Мета-программирование или Трансформации AST (Abastract Syntax Tree).
Рассматривать мета-программирование для фильтрации запросов к контроллерам мы не будем, поскольку это больше похоже на хак, чем на стабильное решение.
Трансформации
Трансформации широко применяются в Grails, например для добавления полей id и version в классы модели. Мы будем использовать их для фильтрации обращений к методам контроллеров.
Трансформация представляет собой простой Java класс реализующий интерфейс ASTTransformation и аннотированный @GroovyASTTransformation, который содержит всего один метод visit — и по сути является типичным представителем паттерна Посетитель. Для трансформации можно задать фазу компиляции на которой она применяется. А для выбора узлов, поступающих посетителю необходим класс аннотации, аннотированный GroovyASTTransformationClass. В итоге трансформация изменяет синтаксическое дерево, может добавлять/изменять/удалять узлы, влияя на получающийся байт-код.
Итого нам потребуется некоторая аннотация и класс транформации. Для простоты назовём их @SuperSecured и SuperSecuredTransformation.
Аннотация содержит в значении массив строк — необходимых ролей для доступа к методу.
@Retention(RetentionPolicy.SOURCE) — указывает на то, что в итоговом байт-коде аннотация будет отсутствовать.
package com.example; import org.codehaus.groovy.transform.GroovyASTTransformationClass; import java.lang.annotation.*; @Target({ElementType.METHOD}) @Retention(RetentionPolicy.SOURCE) @GroovyASTTransformationClass("com.example.SuperSecuredTransformation") public @interface SuperSecured { String[] value() default {}; }
Трансформация:
package com.example; import org.codehaus.groovy.ast.*; import org.codehaus.groovy.ast.expr.*; import org.codehaus.groovy.ast.stmt.*; import org.codehaus.groovy.control.*; import org.codehaus.groovy.transform.*; import java.util.List; @GroovyASTTransformation(phase = CompilePhase.SEMANTIC_ANALYSIS) public class SuperSecuredTransformation implements ASTTransformation { @Override public void visit(ASTNode[] astNodes, SourceUnit sourceUnit) { if (astNodes != null) { for (ASTNode node : astNodes) { if (node instanceof MethodNode) { MethodNode methodNode = (MethodNode) node; List<AnnotationNode> annotations = methodNode.getAnnotations(new ClassNode(SuperSecured.class)); if (annotations != null && !annotations.isEmpty()) { injectRolesCheck(methodNode, annotations); } } } } } private void injectRolesCheck(MethodNode method, List<AnnotationNode> annotations) { for (AnnotationNode annotationNode : annotations) { BlockStatement code = (BlockStatement) method.getCode(); Expression rolesValue = annotationNode.getMember("value"); Expression checkRolesExpression = new StaticMethodCallExpression( new ClassNode(SuperSecuredInspector.class), "rejectByRoles", new ArgumentListExpression( rolesValue ) ); code.getStatements().add(0, new ExpressionStatement(checkRolesExpression)); } } }
Получилось следующее: трансформация принимает на вход узлы синтаксического дерева, аннотированные как @SuperSecured и, если это метод, добавляет в начало вызов статического метода SuperSecuredInspector.rejectByRoles со списком ролей в значении аннотации. Этот метод выбрасывает исключение AccessDeniedException, если текущий пользователь не удовлетворяет условиям безопасности.
Пользоваться такими аннотациями в итоге — одно удовольствие.
Заключение
Такой подход позволяет выделить сложные правила доступа к объектам и не дублировать их в коде. Аннотации достаточно выразительны, а кодогенерация статически типизирована, что позволяет избежать ошибок во время исполнения.
Трансформации — достойная альтернатива AOP в Groovy.
Ссылки
P.S. Хабра-девелоперы, почините пожалуйста отображения знака @ в теге source.