Предыстория
Мы разрабатываем небольшой портал на 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.*;
<hh user=Target>({ElementType.METHOD})
<hh user=Retention>(RetentionPolicy.SOURCE)
<hh user=GroovyASTTransformationClass>("com.example.SuperSecuredTransformation")
public <hh user=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;
<hh user=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.
Ссылки
Автор: jreznot