java 使用枚举类型作为@RolesAllowed-Annotation的值参数

pftdvrlh  于 2022-12-28  发布在  Java
关注(0)|答案(5)|浏览(125)

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

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

当我像这样使用注解时,它工作得很好:

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

但这不是我想要的,因为我必须在这里使用String,重构变得很困难,而且可能会出现拼写错误。因此,我不使用String,而是使用Enum值作为此注解的参数。Enum如下所示:

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;
    }
}

所以我试着用Enum作为一个参数,如下所示:

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

但是我得到了下面的编译器错误,尽管Enum.name只返回了一个String(它总是常量,不是吗?)
注解属性RolesAllowed的值。值必须是常量表达式'
接下来,我尝试向Enum添加一个额外的final String:

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来做这个。

hm2xizp9

hm2xizp91#

这个怎么样?

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) { ... }

有一点需要注意的是,对于任何修改,我们都需要在两个地方进行更改。但是由于它们位于同一个文件中,因此不太可能会错过。作为回报,我们得到了不使用原始字符串和避免复杂机制的好处。
或者这听起来完全愚蠢?:)

kzipqqlq

kzipqqlq2#

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

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

然而,这意味着枚举值不会在任何地方使用,因为您将在注解中使用字符串常量。
在我看来,如果您的RoleType类只包含一堆静态的final String常量,您会过得更好。
为了了解代码为什么不能编译,我查看了Java Language Specification(JLS)。注解的JLS声明,对于参数类型为 T、值为 V 的注解,
如果 T 是基元类型或String,则 V 是常量表达式。
常量表达式包括,除其他外,
引用常量变量的 TypeName . Identifier 格式的限定名称
常量变量定义为
一个基元类型或String类型的变量,它是final变量,并使用编译时常量表达式初始化

nszi6y05

nszi6y053#

下面是一个使用附加接口和元注解的解决方案,我包含了一个实用程序类来帮助执行反射,以便从一组注解中获取角色类型,并对它进行了一些测试:

/**
 * 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());
    }
}
ds97pgxw

ds97pgxw4#

我使用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(...) {
       ...
   }
}
smtd7mpg

smtd7mpg5#

我通过添加注解@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;
  }
}

相关问题