将枚举类型用作@RolesAllowed注释的值参数

70

我正在开发一个Java企业应用程序,目前正在进行Java EE安全性方面的工作,以限制特定用户对特定功能的访问。我已经配置了应用程序服务器和所有内容,现在我正在使用RolesAllowed注释来保护方法:

@Documented
@Retention (RUNTIME)
@Target({TYPE, METHOD})
public @interface RolesAllowed {
    String[] value();
}

当我使用如下注释时,它能很好地工作:

@RolesAllowed("STUDENT")
public void update(User p) { ... }

但这不是我想要的,因为我必须在这里使用一个字符串,重构变得困难,并且可能会出现拼写错误。因此,我想使用枚举值作为此注释的参数。该枚举如下:

public enum RoleType {
    STUDENT("STUDENT"),
    TEACHER("TEACHER"),
    DEANERY("DEANERY");

    private final String label;

    private RoleType(String label) {
        this.label = label;
    }

    public String toString() {
        return this.label;
    }
}

所以我尝试将枚举作为参数使用,就像这样:

@RolesAllowed(RoleType.DEANERY.name())
public void update(User p) { ... }

但是,我得到了以下编译器错误,尽管Enum.name只返回一个字符串(这个字符串总是常量,不是吗?)。
注释属性RolesAllowed.value的值必须是一个常量表达式。
接下来,我尝试向我的枚举类中添加一个额外的final字符串。
public enum RoleType {
    ...
    public static final String STUDENT_ROLE = STUDENT.toString();
    ...
}

但是这也不能作为参数,会导致相同的编译错误:

// The value for annotation attribute RolesAllowed.value must be a constant expression
@RolesAllowed(RoleType.STUDENT_ROLE)

我应该如何实现我想要的行为?我甚至实现了自己的拦截器来使用我的注解,这很美妙,但对于这样一个小问题而言,代码行数太多了。

免责声明

这个问题最初是一个Scala问题。我发现Scala不是问题的源头,所以我首先尝试在Java中解决它。


2
抱歉,这与解决您的问题有些无关,但我想提醒您,如果您只是将枚举构造函数设置为与名称相同,那么可以省略String参数;您可以通过在枚举上调用.name()访问该值。 我认为toString()方法无论如何都会委托给name()。 - I82Much
可能是如何在Java中从常量向注释提供枚举值的重复问题。 - Martin Schröder
5个回答

43

这个怎么样?

public enum RoleType {
    STUDENT(Names.STUDENT),
    TEACHER(Names.TEACHER),
    DEANERY(Names.DEANERY);

    public class Names{
        public static final String STUDENT = "Student";
        public static final String TEACHER = "Teacher";
        public static final String DEANERY = "Deanery";
    }

    private final String label;

    private RoleType(String label) {
        this.label = label;
    }

    public String toString() {
        return this.label;
    }
}

你可以在注释中像这样使用它

@RolesAllowed(RoleType.Names.DEANERY)
public void update(User p) { ... }

有一个小问题,就是我们需要在两个地方进行修改。但由于它们在同一个文件中,很难遗漏。相反,我们的好处是不使用原始字符串,并避免复杂的机制。

或者这听起来完全愚蠢?:)


2
谢谢,我很喜欢这个,我会使用它。我最初来到这个问题是因为我在寻找一种更清晰的方式来指定 Jackson 中 @JsonSubTypes.Type 注释中的“名称”属性,以这样的方式,定义这些逻辑名称的枚举可以在注释中使用,并可用于应用程序的其他部分。 - Trevor
很高兴你喜欢@Trevor。 - Samiron
请参见:https://dev59.com/H2Yr5IYBdhLWcg3w8-zA - alexander
我认为内部的“Names”应该是一个接口。 - Muhammad Ali
可能将Names类设为静态更加方便。 - Viacheslav Plekhanov

37

我认为你使用枚举的方法不会奏效。我发现,如果将你最后一个例子中的STUDENT_ROLE字段更改为常量字符串而不是表达式,则编译器错误会消失:

public enum RoleType { 
  ...
  public static final String STUDENT_ROLE = "STUDENT";
  ...
}

然而,这意味着枚举值将不会在任何地方使用,因为您将在注释中使用字符串常量。看起来,如果您的RoleType类只包含一堆静态final字符串常量,那么您会更好。
为了找出您的代码为什么无法编译,我查阅了Java Language Specification(JLS)。有关注释的JLS指出,对于具有类型T和值V的参数的注释,

如果T是原始类型或String,则V是常量表达式。

常量表达式包括以下内容之一:

形如TypeName. Identifier的限定名称,用于引用常量变量

常量变量定义为

一个变量,原始类型或类型String,是final并使用编译时常量表达式初始化


6
感谢你的努力!有趣的事实,特别是最后一个。我一直认为决赛已经是常数了。好吧,这就是为什么它不能工作。对我来说,这似乎已经是答案了。尽管我并不真的很满意 ;) (顺便说一句,我确实需要 Enum,不仅仅是为了注释) - Wolkenarchitekt
我确实遇到了一个例子,它声明了 public interface Roles {String REGISTERED = "registered"; },然后使用了 @RolesAllowed({Roles.REGISTERED})。当然,一个没有使用 enum 的例子并不意味着 enum 会产生问题,但是嘛 ;-) - Arjan

13

我通过使用Lombok注解FieldNameConstants解决了这个问题:

@FieldNameConstants(onlyExplicitlyIncluded = true)
public enum EnumBasedRole {
    @FieldNameConstants.Include ADMIN,
    @FieldNameConstants.Include EDITOR,
    @FieldNameConstants.Include READER;
}

接下来您可以按以下方式使用它:

@RestController
@RequestMapping("admin")
@RolesAllowed(EnumBasedRole.Fields.ADMIN)
public class MySecuredController {

   @PostMapping("user")
   public void deleteUser(...) {
       ...
   }
}

谢谢,完美且简洁的解决方案 :) - Ahmed Ghoneim
聪明的点子,我会向团队提议这个。 - 6infinity8
最佳解决方案! - undefined
这个方法是可行的,但它使用了 Lombok 的实验性包,这个包存在着错误和 API 变动的风险。在它退出实验阶段并经过更全面的测试之前,我不会在生产环境中使用它。 - undefined

11

以下是使用额外接口和元注解的解决方案。我还包括了一个实用类来帮助反射获取一组注释中的角色类型,以及一个小测试:

/**
 * empty interface which must be implemented by enums participating in
 * annotations of "type" @RolesAllowed.
 */
public interface RoleType {
    public String toString();
}

/** meta annotation to be applied to annotations that have enum values implementing RoleType. 
 *  the value() method should return an array of objects assignable to RoleType*.
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ANNOTATION_TYPE})
public @interface RolesAllowed { 
    /* deliberately empty */ 
}

@RolesAllowed
@Retention(RetentionPolicy.RUNTIME)
@Target({TYPE, METHOD})
public @interface AcademicRolesAllowed {
    public AcademicRoleType[] value();
}

public enum AcademicRoleType implements RoleType {
    STUDENT, TEACHER, DEANERY;
    @Override
    public String toString() {
        return name();
    }
}


public class RolesAllowedUtil {

    /** get the array of allowed RoleTypes for a given class **/
    public static List<RoleType> getRoleTypesAllowedFromAnnotations(
            Annotation[] annotations) {
        List<RoleType> roleTypesAllowed = new ArrayList<RoleType>();
        for (Annotation annotation : annotations) {
            if (annotation.annotationType().isAnnotationPresent(
                    RolesAllowed.class)) {
                RoleType[] roleTypes = getRoleTypesFromAnnotation(annotation);
                if (roleTypes != null)
                    for (RoleType roleType : roleTypes)
                        roleTypesAllowed.add(roleType);
            }
        }
        return roleTypesAllowed;
    }

    public static RoleType[] getRoleTypesFromAnnotation(Annotation annotation) {
        Method[] methods = annotation.annotationType().getMethods();
        for (Method method : methods) {
            String name = method.getName();
            Class<?> returnType = method.getReturnType();
            Class<?> componentType = returnType.getComponentType();
            if (name.equals("value") && returnType.isArray()
                    && RoleType.class.isAssignableFrom(componentType)) {
                RoleType[] features;
                try {
                    features = (RoleType[]) (method.invoke(annotation,
                            new Object[] {}));
                } catch (Exception e) {
                    throw new RuntimeException(
                            "Error executing value() method in "
                                    + annotation.getClass().getCanonicalName(),
                            e);
                }
                return features;
            }
        }
        throw new RuntimeException(
                "No value() method returning a RoleType[] type "
                        + "was found in annotation "
                        + annotation.getClass().getCanonicalName());
    }

}

public class RoleTypeTest {

    @AcademicRolesAllowed({DEANERY})
    public class DeaneryDemo {

    }

    @Test
    public void testDeanery() {
        List<RoleType> roleTypes = RolesAllowedUtil.getRoleTypesAllowedFromAnnotations(DeaneryDemo.class.getAnnotations());
        assertEquals(1, roleTypes.size());
    }
}

哇!这是一个非常优雅的解决方案,适用于需要多个枚举角色类型的情况。我认为当只有一个角色类型枚举时,这可能有些过度设计,但对于多个(这正是我目前正在尝试解决的问题),这看起来非常棒。谢谢! - John B

1
我通过添加一个注释@RoleTypesAllowed和添加一个元数据源来解决了这个问题。如果只需要支持一个枚举类型,那么这种方法非常有效。如果需要支持多个枚举类型,请参考anomolos的帖子。
在下面的代码中,RoleType是我的角色枚举。
@Documented
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RoleTypesAllowed {
  RoleType[] value();
}

然后我向Spring添加了以下元数据源...

@Slf4j
public class CemsRolesAllowedMethodSecurityMetadataSource
    extends AbstractFallbackMethodSecurityMetadataSource {

  protected Collection<ConfigAttribute> findAttributes(Class<?> clazz) {
    return this.processAnnotations(clazz.getAnnotations());
  }

  protected Collection<ConfigAttribute> findAttributes(Method method, Class<?> targetClass) {
    return this.processAnnotations(AnnotationUtils.getAnnotations(method));
  }

  public Collection<ConfigAttribute> getAllConfigAttributes() {
    return null;
  }

  private List<ConfigAttribute> processAnnotations(Annotation[] annotations) {
    if (annotations != null && annotations.length != 0) {
      List<ConfigAttribute> attributes = new ArrayList();

      for (Annotation a : annotations) {
        if (a instanceof RoleTypesAllowed) {
          RoleTypesAllowed ra = (RoleTypesAllowed) a;
          RoleType[] alloweds = ra.value();
          for (RoleType allowed : alloweds) {
            String defaultedAllowed = new RoleTypeGrantedAuthority(allowed).getAuthority();
            log.trace("Added role attribute: {}", defaultedAllowed);
            attributes.add(new SecurityConfig(defaultedAllowed));
          }
          return attributes;
        }
      }
    }
    return null;
  }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接