Статьи

Сделайте ваши аннотации Spring Security @Secured более сухими

Недавно пользователь в списке рассылки Grails User захотел узнать, как уменьшить количество повторений при определении аннотаций @Secured . Правила указания атрибутов в аннотациях Java довольно ограничительны, поэтому я не мог найти прямой способ сделать то, что он просил.

Использование Groovy здесь не очень помогает, поскольку аннотации в классе Groovy по большей части такие же, как в Java (за исключением синтаксиса для значений массива). Конечно, Groovy теперь поддерживает замыкания в аннотациях, но для этого потребуется изменение кода в плагине. Но потом я подумал о некоторой работе, которую Джефф Браун сделал недавно в плагине кеша .

API абстракции кэша Spring включает три аннотации; @Cacheable , @CacheEvict и @CachePut . Мы думали заранее о поддержке большего количества параметров конфигурации, чем позволяют эти аннотации, но, поскольку вы не можете создавать подклассы аннотаций, мы решили использовать преобразование AST, чтобы найти наши версии этих аннотаций (в настоящее время с такими же атрибутами, как у аннотаций Spring) и преобразовать их. к действующим аннотациям Spring. Поэтому я посмотрел на код Джеффа, и он стал основой для решения этой проблемы.

Невозможно использовать код для экстернализации списков прав доступа, поскольку вы не можете контролировать порядок компиляции. Таким образом, я получил решение, которое не идеально, но работает — я ищу файл свойств в корне проекта ( roles.properties ). Формат прост — ключи — это имена для каждого списка полномочий, а значения — это списки имен полномочий, разделенные запятыми. Вот пример:

1
2
3
admins=ROLE_ADMIN, ROLE_SUPERADMIN
switchUser=ROLE_SWITCH_USER
editors=ROLE_EDITOR, ROLE_ADMIN

Эти ключи являются значениями, которые вы используете для новой аннотации @Authorities :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package grails.plugins.springsecurity.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.codehaus.groovy.transform.GroovyASTTransformationClass;
/**
 * @author Burt Beckwith
 */
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@GroovyASTTransformationClass(
    "grails.plugins.springsecurity.annotation.AuthoritiesTransformation")
public @interface Authorities {
   /**
    * The property file key; the property value will be a
    * comma-delimited list of role names.
    * @return the key
    */
   String value();
}

Например, вот контроллер, использующий новую аннотацию:

1
2
3
4
5
6
7
8
@Authorities('admins')
class SecureController {
   @Authorities('editors')
   def someAction() {
      ...
   }
}

Это эквивалент этого контроллера (и если вы декомпилируете тот с помощью @Authorities вы увидите обе аннотации):

1
2
3
4
5
6
7
8
@Secured(['ROLE_ADMIN', 'ROLE_SUPERADMIN'])
class SecureController {
   @Secured(['ROLE_EDITOR', 'ROLE_ADMIN'])
   def someAction() {
      ...
   }
}

Класс преобразования AST ищет аннотации @Authorities , загружает файл свойств и добавляет новую аннотацию @Secured (аннотация @Authorities не удаляется), используя имена ролей, указанные в файле свойств:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
package grails.plugins.springsecurity.annotation;
import grails.plugins.springsecurity.Secured;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.AnnotatedNode;
import org.codehaus.groovy.ast.AnnotationNode;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.expr.ConstantExpression;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.ListExpression;
import org.codehaus.groovy.control.CompilePhase;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.transform.ASTTransformation;
import org.codehaus.groovy.transform.GroovyASTTransformation;
import org.springframework.util.StringUtils;
/**
 * @author Burt Beckwith
 */
@GroovyASTTransformation(phase=CompilePhase.CANONICALIZATION)
public class AuthoritiesTransformation implements ASTTransformation {
  protected static final ClassNode SECURED =
       new ClassNode(Secured.class);
  public void visit(ASTNode[] astNodes, SourceUnit sourceUnit) {
    try {
      ASTNode firstNode = astNodes[0];
      ASTNode secondNode = astNodes[1];
      if (!(firstNode instanceof AnnotationNode) ||
          !(secondNode instanceof AnnotatedNode)) {
        throw new RuntimeException("Internal error: wrong types: " +
            firstNode.getClass().getName() +
            " / " + secondNode.getClass().getName());
      }
      AnnotationNode rolesAnnotationNode = (AnnotationNode) firstNode;
      AnnotatedNode annotatedNode = (AnnotatedNode) secondNode;
      AnnotationNode secured = createAnnotation(rolesAnnotationNode);
      if (secured != null) {
        annotatedNode.addAnnotation(secured);
      }
    }
    catch (Exception e) {
      // TODO
      e.printStackTrace();
    }
  }
  protected AnnotationNode createAnnotation(AnnotationNode rolesNode)
        throws IOException {
    Expression value = rolesNode.getMembers().get("value");
    if (!(value instanceof ConstantExpression)) {
      // TODO
      System.out.println(
         "annotation @Authorities value isn't a ConstantExpression: " +
         value);
      return null;
    }
    String fieldName = value.getText();
    String[] authorityNames = getAuthorityNames(fieldName);
    if (authorityNames == null) {
      return null;
    }
    return buildAnnotationNode(authorityNames);
  }
  protected AnnotationNode buildAnnotationNode(String[] names) {
    AnnotationNode securedAnnotationNode = new AnnotationNode(SECURED);
    List<Expression> nameExpressions = new ArrayList<Expression>();
    for (String authorityName : names) {
      nameExpressions.add(new ConstantExpression(authorityName));
    }
    securedAnnotationNode.addMember("value",
              new ListExpression(nameExpressions));
    return securedAnnotationNode;
  }
  protected String[] getAuthorityNames(String fieldName)
       throws IOException {
    Properties properties = new Properties();
    File propertyFile = new File("roles.properties");
    if (!propertyFile.exists()) {
      // TODO
      System.out.println("Property file roles.properties not found");
      return null;
    }
    properties.load(new FileReader(propertyFile));
    Object value = properties.getProperty(fieldName);
    if (value == null) {
      // TODO
      System.out.println("No value for property '" + fieldName + "'");
      return null;
    }
    List<String> names = new ArrayList<String>();
    String[] nameArray = StringUtils.commaDelimitedListToStringArray(
        value.toString())
    for (String auth : nameArray) {
      auth = auth.trim();
      if (auth.length() > 0) {
        names.add(auth);
      }
    }
    return names.toArray(new String[names.size()]);
  }
}

Возможно, я когда-нибудь включу это в плагин — я создал проблему JIRA в качестве напоминания — но сейчас вы можете просто скопировать эти два класса в папку src / java вашего приложения и создать файл roles.properties в корне проекта , Каждый раз, когда вы хотите добавить или удалить запись или добавить или удалить имя роли из записи, обновите файл свойств, запустите grails clean и grails compile чтобы убедиться, что используются последние значения.

Ссылка: Сделайте аннотации Spring Security @Secured более СУХОЙ от нашего партнера JCG Берта Беквита в блоге « Армия солипсистов» .