web-dev-qa-db-fra.com

Utiliser le type Enum comme paramètre de valeur pour @ RolesAllowed-Annotation

Je développe une Java application d'entreprise, en train de faire Java EE des trucs de sécurité pour restreindre l'accès à des fonctions particulières à des utilisateurs spécifiques. J'ai configuré le serveur d'applications et tout le reste) , et maintenant j'utilise l'annotation RolesAllowed pour sécuriser les méthodes:

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

Quand j'utilise l'annotation comme ça, ça marche bien:

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

Mais ce n'est pas ce que je veux, car je dois utiliser une chaîne ici, le refactoring devient difficile et des fautes de frappe peuvent survenir. Donc, au lieu d'utiliser une chaîne, je voudrais utiliser une valeur Enum comme paramètre pour cette annotation. L'Enum ressemble à ceci:

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

J'ai donc essayé d'utiliser l'Enum comme paramètre comme ceci:

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

Mais alors j'obtiens l'erreur de compilation suivante, bien que Enum.name renvoie juste une chaîne (qui est toujours constante, n'est-ce pas?).

La valeur de l'attribut d'annotation RolesAllowed.value doit être une expression constante`

La prochaine chose que j'ai essayé était d'ajouter une chaîne finale supplémentaire à mon énumération:

public enum RoleType {
    ...
    public static final String STUDENT_ROLE = STUDENT.toString();
    ...
}

Mais cela ne fonctionne pas non plus en tant que paramètre, ce qui entraîne la même erreur de compilation:

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

Comment puis-je obtenir le comportement que je souhaite? J'ai même implémenté mon propre intercepteur pour utiliser mes propres annotations, ce qui est beau, mais beaucoup trop de lignes de codes pour un petit problème comme celui-ci.

[~ # ~] Avis de non-responsabilité [~ # ~]

Cette question était à l'origine une question Scala . J'ai découvert que Scala n'est pas la source du problème, donc j'essaie d'abord de le faire en Java.

55
Ingo Fischer

Je ne pense pas que votre approche de l'utilisation des énumérations fonctionnera. J'ai constaté que l'erreur du compilateur disparaissait si je modifiais le STUDENT_ROLE champ dans votre dernier exemple à une chaîne constante, par opposition à une expression:

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

Cependant, cela signifie alors que les valeurs d'énumération ne seraient utilisées nulle part, car vous utiliseriez plutôt les constantes de chaîne dans les annotations.

Il me semble que vous feriez mieux si votre classe RoleType ne contenait rien de plus qu'un tas de constantes String finales statiques.


Pour voir pourquoi votre code ne se compilait pas, j'ai jeté un œil à la Java Language Specification (JLS). Le JLS pour annotations indique que pour une annotation avec un paramètre de type [~ # ~] t [~ # ~] et valeur [~ # ~] v [~ # ~] ,

si [~ # ~] t [~ # ~] est un type primitif ou String, [~ # ~] v [~ # ~] est une expression constante.

A expression constante comprend, entre autres,

Noms qualifiés du formulaire TypeName . Identifiant qui fait référence à des variables constantes

et un variable constante est défini comme

une variable, de type primitif ou de type String, qui est finale et initialisée avec une expression constante au moment de la compilation

31
Luke Woodward

Que dis-tu de ça?

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

Et dans l'annotation, vous pouvez l'utiliser comme

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

Une petite préoccupation est que, pour toute modification, nous devons changer à deux endroits. Mais comme ils sont dans le même fichier, il est peu probable qu'ils soient manqués. En retour, nous obtenons l'avantage de ne pas utiliser de chaînes brutes et d'éviter le mécanisme sophistiqué.

Ou cela semble totalement stupide? :)

20
Samiron

Voici une solution utilisant une interface supplémentaire et une méta-annotation. J'ai inclus une classe utilitaire pour aider à faire la réflexion pour obtenir les types de rôles à partir d'un ensemble d'annotations, et un petit test pour cela:

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

J'ai résolu ce problème en ajoutant une annotation @RoleTypesAllowed et en ajoutant une source de métadonnées. Cela fonctionne très bien s'il n'y a qu'un seul type d'énumération à prendre en charge. Pour plusieurs types d'énumération, voir le post d'anomolos.

Ci-dessous RoleType est mon énumération de rôles.

@Documented
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RoleTypesAllowed {
  RoleType[] value();
}

J'ai ensuite ajouté la source de métadonnées suivante au printemps ...

@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;
  }
}
0
John B