Make your Spring Security @Secured annotations more DRY

本文介绍了一种通过使用自定义注解@Authorities和AST转换来减少@Secured注解中重复代码的方法,并展示了如何利用属性文件配置角色权限。
Recently a user on the Grails User mailing list wanted to know  how to reduce repetition when defining @Secured annotations . The rules for specifying attributes in Java annotations are pretty restrictive, so I couldn’t see a direct way to do what he was asking.

Using Groovy doesn’t really help here since for the most part annotations in a Groovy class are pretty much the same as in Java (except for the syntax for array values). Of course Groovy now supports closures in annotations, but this would require a code change in the plugin. But then I thought about some work Jeff Brown did recently in the cache plugin.

Spring’s cache abstraction API includes three annotations; @Cacheable,@CacheEvict, and @CachePut. We were thinking ahead about supporting more configuration options than these annotations allow, but since you can’t subclass annotations we decided to use an AST transformation to find our versions of these annotations (currently with the same attributes as the Spring annotations) and convert them to valid Spring annotations. So I looked at Jeff’s code and it ended up being the basis for a fix for this problem.

It’s not possible to use code to externalize the authority lists because you can’t control the compilation order. So I ended up with a solution that isn’t perfect but works – I look for a properties file in the project root (roles.properties). The format is simple – the keys are names for each authority list and the values are the lists of authority names, comma-delimited. Here’s an example:

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

These keys are the values you use for the new @Authorities annotation:

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

For example here’s a controller using the new annotation:

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

This is the equivalent of this controller (and if you decompile the one with @Authorities you’ll see both annotations):

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

The AST transformation class looks for @Authorities annotations, loads the properties file, and adds a new @Securedannotation (the @Authorities annotation isn’t removed) using the role names specified in the properties file:

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

I’ll probably include this in the plugin at some point – I created a JIRA issue as a reminder – but for now you can just copy these two classes into your application’s src/java folder and create a roles.properties file in the project root. Any time you want to add or remove an entry or add or remove a role name from an entry, update the properties file, rungrails clean and grails compile to be sure that the latest values are used.

Reference: Make your Spring Security @Secured annotations more DRY from our JCG partner Burt Beckwith at the An Army of Solipsists blog.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值