2017-01-26 1 views
18

J'ai configuré ACL dans mon application Spring Boot. La configuration ACL est la suivante:L'accès est toujours refusé dans Spring Security - DenyAllPermissionEvaluator

@Configuration 
@ComponentScan(basePackages = "com.company") 
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) 
public class ACLConfigration extends GlobalMethodSecurityConfiguration { 

    @Autowired 
    DataSource dataSource; 

    @Bean 
    public EhCacheBasedAclCache aclCache() { 
     return new EhCacheBasedAclCache(aclEhCacheFactoryBean().getObject(), permissionGrantingStrategy(), aclAuthorizationStrategy()); 
    } 

    @Bean 
    public EhCacheFactoryBean aclEhCacheFactoryBean() { 
     EhCacheFactoryBean ehCacheFactoryBean = new EhCacheFactoryBean(); 
     ehCacheFactoryBean.setCacheManager(aclCacheManager().getObject()); 
     ehCacheFactoryBean.setCacheName("aclCache"); 
     return ehCacheFactoryBean; 
    } 

    @Bean 
    public EhCacheManagerFactoryBean aclCacheManager() { 
     return new EhCacheManagerFactoryBean(); 
    } 

    @Bean 
    public DefaultPermissionGrantingStrategy permissionGrantingStrategy() { 
     ConsoleAuditLogger consoleAuditLogger = new ConsoleAuditLogger(); 
     return new DefaultPermissionGrantingStrategy(consoleAuditLogger); 
    } 

    @Bean 
    public AclAuthorizationStrategy aclAuthorizationStrategy() { 
     return new AclAuthorizationStrategyImpl(new SimpleGrantedAuthority("ROLE_ACL_ADMIN")); 
    } 

    @Bean 
    public LookupStrategy lookupStrategy() { 
     return new BasicLookupStrategy(dataSource, aclCache(), aclAuthorizationStrategy(), new ConsoleAuditLogger()); 
    } 

    @Bean 
    public JdbcMutableAclService aclService() { 
     return new JdbcMutableAclService(dataSource, lookupStrategy(), aclCache()); 
    } 

    @Bean 
    public DefaultMethodSecurityExpressionHandler defaultMethodSecurityExpressionHandler() { 
     return new DefaultMethodSecurityExpressionHandler(); 
    } 

    @Override 
    public MethodSecurityExpressionHandler createExpressionHandler() { 
     DefaultMethodSecurityExpressionHandler expressionHandler = defaultMethodSecurityExpressionHandler(); 
     expressionHandler.setPermissionEvaluator(new AclPermissionEvaluator(aclService())); 
     expressionHandler.setPermissionCacheOptimizer(new AclPermissionCacheOptimizer(aclService())); 
     return expressionHandler; 
    } 
} 

Références:

et la configuration de la sécurité est la suivante:

@Configuration 
@EnableWebSecurity 
public class CustomSecurityConfiguration extends WebSecurityConfigurerAdapter { 

    @Bean 
    public AuthenticationEntryPoint entryPoint() { 
     return new LoginUrlAuthenticationEntryPoint("/authenticate"); 
    } 

    @Override 
    protected void configure(HttpSecurity http) throws Exception { 

     http 
       .csrf() 
       .disable() 
       .authorizeRequests() 
       .antMatchers("/authenticate/**").permitAll() 
       .anyRequest().fullyAuthenticated() 
       .and().requestCache().requestCache(new NullRequestCache()) 
       .and().addFilterBefore(authenticationFilter(), CustomUsernamePasswordAuthenticationFilter.class); 
    } 

    @Override 
    public void configure(WebSecurity web) throws Exception { 
     web.ignoring().antMatchers(HttpMethod.OPTIONS, "/**"); 
    } 

    @Bean 
    public CustomUsernamePasswordAuthenticationFilter authenticationFilter() 
      throws Exception { 
     CustomUsernamePasswordAuthenticationFilter authenticationFilter = new CustomUsernamePasswordAuthenticationFilter(); 
     authenticationFilter.setUsernameParameter("username"); 
     authenticationFilter.setPasswordParameter("password"); 
     authenticationFilter.setFilterProcessesUrl("/authenticate"); 
     authenticationFilter.setAuthenticationSuccessHandler(new CustomAuthenticationSuccessHandler()); 
     authenticationFilter.setAuthenticationFailureHandler(new CustomAuthenticationFailureHandler()); 
     authenticationFilter.setAuthenticationManager(authenticationManagerBean()); 
     return authenticationFilter; 
    } 

    @Bean 
    public PasswordEncoder passwordEncoder() { 
     return new BCryptPasswordEncoder(); 
    } 
} 

Ma CustomAuthenticationProvider classe:

@Component 
public class CustomAuthenticationProvider implements AuthenticationProvider { 

    @Autowired 
    private UsersService usersService; 

    @Override 
    public Authentication authenticate(Authentication authentication) 
      throws AuthenticationException { 

     String username = authentication.getName(); 
     String password = authentication.getCredentials().toString(); 

     User user = usersService.findOne(username); 

     if(user != null && usersService.comparePassword(user, password)){ 

      return new UsernamePasswordAuthenticationToken(
        user.getUsername(), 
        user.getPassword(), 
        AuthorityUtils.commaSeparatedStringToAuthorityList(
          user.getUserRoles().stream().collect(Collectors.joining(",")))); 
     } else { 
      return null; 
     } 
    } 

    @Override 
    public boolean supports(Class<?> authentication) { 
     return authentication.equals(UsernamePasswordAuthenticationToken.class); 
    } 
} 

Voici mon CustomUsernamePasswordAuthenticationToken:

public class CustomUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter { 

    @Override 
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) 
      throws AuthenticationException { 

     if(!request.getMethod().equals("POST")) 
      throw new AuthenticationServiceException(String.format("Authentication method not supported: %s", request.getMethod())); 

     try { 

      CustomUsernamePasswordAuthenticationForm form = new ObjectMapper().readValue(request.getReader(), CustomUsernamePasswordAuthenticationForm.class); 

      String username = form.getUsername(); 
      String password = form.getPassword(); 

      if(username == null) 
       username = ""; 

      if(password == null) 
       password = ""; 

      UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password); 

      setDetails(request, token); 

      return getAuthenticationManager().authenticate(token); 

     } catch (IOException exception) { 
      throw new CustomAuthenticationException(exception); 
     } 
    } 

    private class CustomAuthenticationException extends RuntimeException { 
     private CustomAuthenticationException(Throwable throwable) { 
      super(throwable); 
     } 
    } 
} 

En dehors de ce qui précède, j'ai CustomAuthenticationFailureHandler, CustomAuthenticationSuccessHandler, CustomNoRedirectStrategy et CustomUsernamePasswordAuthenticationForm que je sauté à cause de la longueur de cette question.

Et j'utilise le schéma MySQL que l'on peut trouver here.

J'ajouterez des entrées à mes acl tables connexes, comme suit:

INSERT INTO acl_class VALUES (1, com.company.project.domain.users.User) 
INSERT INTO acl_sid VALUES (1, 1, "demo") 

(J'ai un utilisateur avec le nom d'utilisateur demo)

INSERT INTO acl_object_identity VALUES (1, 1, 1, NULL, 1, 0) 
INSERT INTO acl_entry VALUES (1, 1, 1, 1, 1, 1, 1, 1) 

Mais tout ce que je reçois est:

Denying user demo permission 'READ' on object [email protected] 

dans mon

@PostFilter("hasPermission(filterObject, 'READ')") 

Je soupçonnais de plusieurs questions ici:

  1. L'expression hasPermission: Je l'ai substitué par « lire » et « 1 », mais sans mesure.
  2. Mes entrées de base de données ne sont pas correctes
  3. Je ne suis pas en train d'implémenter un évaluateur de permission personnalisé. Est-ce nécessaire, ou est expressionHandler.setPermissionEvaluator(new AclPermissionEvaluator(aclService())); assez?

Mise à jour

Méthode d'échantillonnage où @PostFilter est utilisé:

@RequestMapping(method = RequestMethod.GET) 
    @PostFilter("hasPermission(filterObject, 'READ')") 
    List<User> find(@Min(0) @RequestParam(value = "limit", required = false, defaultValue = "10") Integer limit, 
        @Min(0) @RequestParam(value = "page", required = false, defaultValue = "0") Integer page, 
        @RequestParam(value = "email", required = false) String email, 
        @RequestParam(value = "firstName", required = false) String firstName, 
        @RequestParam(value = "lastName", required = false) String lastName, 
        @RequestParam(value = "userRole", required = false) String userRole) { 

     return usersService.find(
       limit, 
       page, 
       email, 
       firstName, 
       lastName, 
       userRole); 
    } 

Mise à jour # 2:

La question reflète maintenant tout mis en place en ce qui concerne l'authentification/autorisation/ACL.

Mise à jour # 3:

Je suis maintenant très proche de résoudre le problème, la seule chose qui reste est de résoudre ce:

https://stackoverflow.com/questions/42996579/custom-permissionevaluator-not-called-although-set-as-permissionevaluator-deny

Si quelqu'un pouvait me aider à cette question , Je peux enfin avoir un compte rendu de ce que j'ai vécu pour résoudre ce problème.

+0

est la méthode que @PostFilter est sur un public qui implémente une interface? – denov

+0

Non, c'est sur un '@ RestController' ou' @ Controller'. Je suspecte vraiment des entrées de base de données, ou un composant n'étant pas présent. –

+0

A quoi ressemble la méthode, où vous placez l'annotation @PostFilter? Avez-vous des Stacktrace dans votre journal de serveur? –

Répondre

2

Voici la réponse longtemps attendu:

Le documentation décrit clairement:

Pour utiliser des expressions hasPermission(), vous devez configurer explicitement PermissionEvaluator dans votre contexte d'application. Cela ressemblerait quelque chose comme ceci:

donc en gros je faisais dans mon AclConfiguration qui s'étend GlobalMethodSecurityConfiguration:

@Override 
    protected MethodSecurityExpressionHandler createExpressionHandler() { 
     DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler(); 
     expressionHandler.setPermissionEvaluator(new AclPermissionEvaluator(aclService())); 
     expressionHandler.setPermissionCacheOptimizer(new AclPermissionCacheOptimizer(aclService())); 
     return expressionHandler; 
    } 

qui n'a pas été traitée par obtenir le printemps!

J'ai dû séparer AclConfig et GlobalMethodSecurityConfiguration. Quand il y a @Bean s défini dans ce dernier, la méthode ci-dessus n'est pas traitée, ce qui pourrait être un bug (sinon, toute clarification sur le sujet est la bienvenue).

2

J'ai mis à jour mon application pour utiliser Spring Security 4.2.1.RELEASE puis j'ai commencé à rencontrer un accès inattendu refusé dans toutes les méthodes annotées @PreAuthorize, qui fonctionnait très bien avant la mise à niveau. J'ai débogué le code de sécurité du printemps et j'ai réalisé que le problème était que tous les rôles à vérifier étaient préfixés avec une chaîne par défaut "ROLE_" sans tenir compte du fait que j'avais défini mon préfixe par défaut comme indiqué dans le code ci-dessous .

auth.ldapAuthentication() 
     .groupSearchBase(ldapProperties.getProperty("groupSearchBase")) 
     .groupRoleAttribute(ldapProperties.getProperty("groupRoleAttribute")) 
     .groupSearchFilter(ldapProperties.getProperty("groupSearchFilter")) 

     //this call used to be plenty to override the default prefix 
     .rolePrefix("") 

     .userSearchBase(ldapProperties.getProperty("userSearchBase")) 
     .userSearchFilter(ldapProperties.getProperty("userSearchFilter")) 
     .contextSource(this.ldapContextSource); 

Toutes mes méthodes de commande ont été annotés avec @PreAuthorize("hasRole('my_ldap_group_name')"), cependant, le cadre n'a pas pris mon rôle préfixe vide mise en compte et donc qu'il utilisait ROLE_my_ldap_group_name pour vérifier le rôle réel à la place. Après avoir creusé profondément dans le code du framework, j'ai réalisé que la classe org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler avait toujours le préfixe de rôle par défaut défini sur "ROLE_". J'ai suivi la source de sa valeur et j'ai découvert qu'il vérifiait d'abord un bean déclaré de la classe org.springframework.security.config.core.GrantedAuthorityDefaults pour rechercher un préfixe par défaut lors de la première initialisation du bean org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer, car ce bean initialiseur ne pouvait pas le trouver déclaré , il a fini par utiliser le préfixe par défaut mentionné ci-dessus.

Je crois que ce n'est pas un comportement attendu: Spring Security aurait dû tenir compte de la même rolePrefix de LdapAuthentication, cependant, pour résoudre ce problème, il est nécessaire d'ajouter la fève org.springframework.security.config.core.GrantedAuthorityDefaults à mon contexte d'application (j'utilise base annotation configuration), comme suit:

@Configuration 
@EnableWebSecurity 
@EnableGlobalMethodSecurity(prePostEnabled = true) 
public class CesSecurityConfiguration extends WebSecurityConfigurerAdapter { 

    private static final String ROLE_PREFIX = ""; 
    //... ommited code ... 
    @Bean 
    public GrantedAuthorityDefaults grantedAuthorityDefaults() { 
     return new GrantedAuthorityDefaults(ROLE_PREFIX); 
    } 

} 

Peut-être que vous obtenez le même problème - je pouvais voir que vous utilisez DefaultMethodSecurityExpressionHandler et il utilise aussi les GrantedAuthorityDefaults de haricots, donc si vous utilisez la même version Spring Security comme moi - 4.2.1.RELEASE vous êtes probablement confronté au même problème. Vos données dans la base de données et votre configuration semblent bonnes.

+0

Merci pour la réponse, je l'apprécie. Je suis conscient que Spring nécessite le préfixe 'ROLE_' et tous mes rôles commencent par' ROLE_'. Donc, je pense à peine que cela devrait avoir quelque chose à voir avec les noms de rôle. De plus, je ne filtre pas ou n'autorise pas en fonction d'un rôle, mais j'utilise acl pour vérifier les autorisations sur la base de l'objet/utilisateur. –

+1

Ok, c'est ce que j'ai dû partager, j'espère que vous comprendrez le problème, je n'ai jamais utilisé ACL ... –

0

J'utilise @PostFilter("hasPermission(filterObject, 'READ')") tout le temps.

Je vérifierais pour vous assurer que votre classe d'utilisateur qui étend UserDetails renvoie le même nom d'utilisateur via getUsername() que vous avez dans la base de données. Avec vérification pour vous assurer que la sécurité et l'application sont dans le même contexte.

La méthode hasPermission prend un objet Authentication en tant que 1er paramètre.

boolean hasPermission(Authentication authentication, 
         Object targetDomainObject, 
         Object permission) 

L'objet d'authentification est une classe mise en œuvre, le plus souvent de UsernamePasswordAuthenticationToken. La méthode getPrincipal() doit donc renvoyer un objet doté d'une méthode getUserName() qui renvoie la même chose que celle que vous avez dans votre base de données.

Jetez un oeil à PrincipalSid

public PrincipalSid(Authentication authentication) { 
    Assert.notNull(authentication, "Authentication required"); 
    Assert.notNull(authentication.getPrincipal(), "Principal required"); 

    if (authentication.getPrincipal() instanceof UserDetails) { 
     this.principal = ((UserDetails) authentication.getPrincipal()).getUsername(); 
    } 
    else { 
     this.principal = authentication.getPrincipal().toString(); 
    } 
} 
+0

Je n'utilise pas tout à fait 'UserDetails' ni' UserDetailsService'. Je retourne seulement un 'UsernamePasswordAuthenticationToken' de ma classe qui' implements AuthenticationProvider'. Dois-je pour que cela fonctionne? –

+0

ajouter plus de détails. il semble que l'installation de l'objet Authentification ne soit pas correcte. – denov

+0

Je suis d'accord. J'ai mis à jour la question pour refléter tout ce que j'ai configuré pour l'authentification/autorisation et ACL. –