web-dev-qa-db-fra.com

'Définition de bean non valide' lors de la migration de Spring Boot 2.0.6 vers 2.1.0 avec EvaluationContextExtensionSupport et de PermissionEvaluator personnalisé

Dans Spring Boot 2.1.0, EvaluationContextExtensionSupport est obsolète et https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/repository/query/spi/ EvaluationContextExtensionSupport.html dit à implémente directement EvaluationContextExtension

Même s'il est seulement obsolète, il échoue instantanément sur cette mise à jour avec cette pile:

Caused by: org.springframework.beans.factory.support.BeanDefinitionOverrideException: Invalid bean definition with name 'methodSecurityInterceptor' defined in class path resource [org/springframework/security/config/annotation/method/configuration/GlobalMethodSecurityConfiguration.class]: Cannot register bean definition [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration; factoryMethodName=methodSecurityInterceptor; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/security/config/annotation/method/configuration/GlobalMethodSecurityConfiguration.class]] for bean 'methodSecurityInterceptor': There is already [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=methodSecurityConfiguration; factoryMethodName=methodSecurityInterceptor; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [ournamespace/configuration/MethodSecurityConfiguration.class]] bound.
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.registerBeanDefinition(DefaultListableBeanFactory.Java:894)
    at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsForBeanMethod(ConfigurationClassBeanDefinitionReader.Java:274)
    at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsForConfigurationClass(ConfigurationClassBeanDefinitionReader.Java:141)
...and so on

Je ne remplace pas explicitement ce haricot, alors je suppose que ce n'est qu'un effet secondaire de ce que nous faisons dans notre code actuel. Si j'autorise le bean écrasant avec spring.main.allow-bean-definition-overriding=true conformément à https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.1-Release-Notes#bean-overriding alors j'obtiens simplement un autre exception.

Java.lang.IllegalStateException: Duplicate key org.springframework.data.spel.ExtensionAwareEvaluationContextProvider$EvaluationContextExtensionAdapter@10dfbbbb at Java.util.stream.Collectors.lambda$throwingMerger$0(Collectors.Java:133) ~[na:1.8.0_162]

Cependant, je ne veux même pas remplacer le comportement d'un bean, l'objectif est de faire fonctionner à nouveau un évaluateur d'autorisations personnalisées comme prévu par Spring. 

Voici comment cela fonctionnait dans la dernière version:

Dans Spring Boot 2.0.6, nous avions les éléments suivants pour que notre classe personnalisée PermissionEvaluator fonctionne:

Une classe qui a étendu EvaluationContextExtensionSupport

import org.springframework.data.repository.query.spi.EvaluationContextExtensionSupport;
import org.springframework.security.access.expression.SecurityExpressionRoot;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;

public class SecurityEvaluationContextExtension extends EvaluationContextExtensionSupport {

    @Override
    public String getExtensionId() {
        return "security";
    }

    @Override
    public SecurityExpressionRoot getRootObject() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            return new SecurityExpressionRoot(authentication) {
        };
    }
}

Et puis une classe où le gestionnaire d’expression est créé avec notre évaluateur d’autorisation et avec un @Bean avec un EvaluationContextExtension

import ournamespace.security.CustomPermissionEvaluator;
import ournamespace.security.SecurityEvaluationContextExtension;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.repository.query.spi.EvaluationContextExtension;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration;

@Configuration
@RequiredArgsConstructor
public class MethodSecurityConfiguration extends GlobalMethodSecurityConfiguration {

    private final CustomPermissionEvaluator permissionEvaluator;

    @Override
    protected MethodSecurityExpressionHandler createExpressionHandler() {
        DefaultMethodSecurityExpressionHandler expressionHandler =
                new DefaultMethodSecurityExpressionHandler();
        expressionHandler.setPermissionEvaluator(permissionEvaluator);
        return expressionHandler;
    }

    @Bean
    EvaluationContextExtension securityExtension() {
        return new SecurityEvaluationContextExtension();
    }
}

Et enfin, nous avons ceci dans une classe par ailleurs généralement vide:

@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
   ...
}

En effet, l'évaluateur d'autorisation personnalisé ne s'est jamais appliqué à toutes les méthodes si nous l'inscrivions dans la classe MethodSecurityConfiguration... Le serveur en question est un serveur de ressources oauth2, de sorte que nous ne configurons rien d'autre dans la WebSecurityConfigurerAdapter. Nous implémentons également notre propre UserDetails et nous étendons le DefaultUserAuthenticationConverter, si cela est pertinent pour la nouvelle solution.

J'ai essayé d'implémenter directement EvaluationContextExtension class, comme indiqué dans l'avertissement relatif à la dépréciation. C'est juste une simple modification en changeant l'interface étendue en implements EvaluationContextExtension. J'ai aussi essayé de changer pour le paquet apparemment plus récent org.springframework.data.spel.spi

J'ai essayé de supprimer notre propre SecurityEvaluationContextExtension et de renvoyer https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/data/repository/query/ SecurityEvaluationContextExtension.html directement sous forme de bean, mais pour une raison quelconque, ce paquet de données n'est pas disponible dans Spring Boot 2.1.0

J'ai simplement essayé de supprimer la définition de ce haricot.

Toutes ces choses entraînent différentes erreurs de 'définition du bean invalide' au démarrage.

Quelqu'un sait-il où trouver un guide de migration ou toute autre ressource expliquant comment cela est censé fonctionner maintenant?

Juste pour référence, la classe CustomPermissionEvaluator actuelle:

import ournamespace.configuration.Constants;
import ournamespace.exception.InternalException;
import ournamespace.model.Account;
import ournamespace.model.Member;
import ournamespace.model.Project;
import ournamespace.repository.MemberRepository;
import ournamespace.service.ServiceUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;

import Java.io.Serializable;

import static ournamespace.model.MemberStatus.JOINED;
import static ournamespace.model.ProjectRole.*;

@RequiredArgsConstructor
@Component
public class CustomPermissionEvaluator implements PermissionEvaluator {

    private final MemberRepository memberRepository;

    @Override
    public boolean hasPermission(Authentication auth, Object targetDomainObject, Object permission) {
        if (targetDomainObject == null)
            return false;

        if (!(permission instanceof String))
            return false;

        if (auth == null)
            return false;

        Account account = ServiceUtil.getAccount(auth);

        if (targetDomainObject instanceof Project)
            return hasPermissionOnProject(account, (Project) targetDomainObject, (String) permission);
        //and so on
    }
}

Et un exemple sur la façon dont il est utilisé:

public interface ProjectRepository extends PagingAndSortingRepository<Project, UUID> {

    @Override
    @PreAuthorize("hasPermission(#project, " + Constants.WRITE + ")")
    <S extends Project> S save(@Param("project") S project);
}

J'ai pris votre code et créé un exemple d'application à partir de celui-ci. Je l'ai posté ici:

https://github.com/jzheaux/stackoverflow-53410526

Votre annotation @EnableGlobalMethodSecurity est sur WebSecurityConfigurerAdapter. Vous avez également une classe qui étend GlobalMethodSecurityConfiguration. Cela peut parfois poser des problèmes de classement au démarrage, ce qui peut correspondre à ce que vous voyez => deux MethodSecurityExpressionHandlers sont créés ainsi que deux EvaluationContextExtensions.

Que ce soit précisément ou non le cas (j'imagine que c'est le cas), lorsque j'ai fait correspondre votre @EnableGlobalMethodSecurity avec votre GlobalMethodSecurityConfiguration personnalisé, tout a bien commencé.

De plus, il semble que votre EvaluationContextExtension personnalisée soit très similaire à la sécurité par défaut de Spring. Vous pouvez envisager de supprimer cette classe ainsi que la méthode de bean correspondante, si vous le pouvez, car Spring Boot en expose une automatiquement lorsque vous avez les dépendances spring-boot-starter-security et spring-security-data.

3
jzheaux

Vous écrasez methodSecurityInterceptor bean. Cela fonctionnait auparavant car la suppression des haricots était autorisée. 

La substitution de bean a été désactivée par défaut pour éviter la substitution accidentelle d'un bean. Si vous comptez sur le remplacement, vous devez définir spring.main.allow-bean-definition-override sur true.

Notes de parution Spring-Boot-2.1 # bean-overriding

2
Sukhpal Singh