2017-01-26 61 views
18

Mam skonfigurowaną listę ACL w mojej aplikacji Spring Boot. Konfiguracja ACL jest następujący:Dostęp jest zawsze odmowy w 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; 
    } 
} 

Odniesienia:

i konfiguracji zabezpieczeń jest następujący:

@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(); 
    } 
} 

Moja CustomAuthenticationProvider klasa:

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

Oto mój 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); 
     } 
    } 
} 

Oprócz powyższego, mam CustomAuthenticationFailureHandler, CustomAuthenticationSuccessHandler, CustomNoRedirectStrategy i CustomUsernamePasswordAuthenticationForm które pominąłem ze względu na długość to pytanie jest.

I używam schematu MySQL, który można znaleźć here.

dodaję wpisy do moich acl powiązanych tabelach następująco:

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

(Mam użytkownika z nazwą użytkownika 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) 

Ale jestem coraz to:

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

w moim

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

jestem podejrzewać o kilku kwestiach tutaj:

  1. Wyrażenie hasPermission: Mam podstawione go 'czytać' i '1', ale bez stopnia.
  2. Moje wpisy w bazie danych nie są poprawne
  3. Nie wdrażam niestandardowego narzędzia do sprawdzania uprawnień. Czy jest to wymagane, czy wystarcza expressionHandler.setPermissionEvaluator(new AclPermissionEvaluator(aclService()));?

Aktualizacja

metoda Próbka gdzie służy @PostFilter:

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

Aktualizacja # 2:

Pytanie teraz odzwierciedla wszystko skonfigurować w odniesieniu do uwierzytelniania/autoryzacji/ACL.

Aktualizacja # 3:

Jestem teraz bardzo blisko, aby rozwiązać problem, jedyne co pozostało to aby rozwiązać ten problem:

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

Jeśli ktoś może mi pomóc w tej kwestii , Mogę wreszcie napisać o tym, co przeszedłem, aby rozwiązać ten problem.

+0

to metoda, w której @PostFilter jest publicznie implementująca interfejs? – denov

+0

Nie, ale na kontrolerze '@ RestController' lub' @ Controller'. Naprawdę podejrzewam, że wpisy w bazie danych lub składnik nie jest obecny. –

+0

Jak wygląda ta metoda, w której umieszczasz adnotację @PostFilter? Czy w dzienniku serwera znajduje się dowolny Stacktrace? –

Odpowiedz

2

Oto długo czekał odpowiedź:

documentation jasno opisuje:

Aby korzystać hasPermission() wyrażenia, trzeba jawnie skonfigurować PermissionEvaluator w kontekście aplikacji. To będzie wyglądać coś takiego:

więc w zasadzie robiłem w moim AclConfiguration która rozciąga GlobalMethodSecurityConfiguration:

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

Który nie był już przetworzony przez wiosnę!

Musiałem oddzielić AclConfig i GlobalMethodSecurityConfiguration. Jeśli w tym ostatnim jest zdefiniowane @Bean, powyższa metoda nie jest przetwarzana, co może być błędem (jeśli nie, wszelkie wyjaśnienia dotyczące tematu są mile widziane).

2

Uaktualniłem moją aplikację, aby używać Spring Security 4.2.1.RELEASE, a następnie zacząłem odczuwać nieoczekiwany odmowy dostępu we wszystkich metodach opatrzonych komentarzem @PreAuthorize, które działało dobrze przed aktualizacją. I debugowałem wiosenny kod bezpieczeństwa i zdałem sobie sprawę, że problem polegał na tym, że wszystkie role do sprawdzenia zostały poprzedzone prefiksem z domyślnym łańcuchem "ROLE_" niezależnie od tego, że ustawiłem domyślny prefiks na pusty, jak pokazano w poniższym kodzie .

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

Wszystkie moje metody kontrolera zostały opatrzone @PreAuthorize("hasRole('my_ldap_group_name')") jednak ramy nie było przy mojej pustej roli prefiks ustawienie pod uwagę i dlatego używała ROLE_my_ldap_group_name zamiast sprawdzić rzeczywistą rolę.

Po tym, jak zagłębiłem się w kod frameworku, zdałem sobie sprawę, że klasa org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler nadal ma domyślny prefiks roli ustawiony na "ROLE_". Poszukałem źródła jego wartości i odkryłem, że po raz pierwszy sprawdzono, czy deklarowany komponent bean klasy org.springframework.security.config.core.GrantedAuthorityDefaults szuka domyślnego prefiksu podczas pierwszej inicjalizacji komponentu bean, ale nie mógł go znaleźć. , skończyło się na użyciu wspomnianego prefiksu domyślnego.

Wierzę, że nie jest to oczekiwane zachowanie: Spring Security powinien mieć tę samą rolęPrefix od ldapAuthentication, jednak aby rozwiązać ten problem, konieczne było dodanie komponentu bean org.springframework.security.config.core.GrantedAuthorityDefaults do kontekstu aplikacji (korzystam z adnotacji konfiguracja), w następujący sposób:

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

} 

Może dostajesz ten sam problem - nie mogłem zobaczyć, że używasz DefaultMethodSecurityExpressionHandler a także wykorzystuje GrantedAuthorityDefaults bean, więc jeśli używasz tej samej wersji Wiosna zabezpieczeń tak jak ja - 4.2.1.ZWRÓCIĆ, że prawdopodobnie używasz tego samego problemu.

+0

Dzięki za odpowiedź, doceniam to. Wiem, że Spring wymaga prefiksu "ROLE_", a wszystkie moje role zaczynają się od 'ROLE_'. Więc nie sądzę, że powinien on mieć coś wspólnego z nazwami ról. Ponadto nie filtruje ani nie autoryzuje na podstawie roli, ale zamiast tego używam acl, aby sprawdzić uprawnienia dla obiektu/użytkownika. –

+1

Ok, właśnie to musiałem się podzielić, mam nadzieję, że wymyślisz problem, nigdy nie używałem ACL ... –

0

Twoje dane w DB i Twojej konfiguracji wyglądają dobrze. Cały czas używam @PostFilter("hasPermission(filterObject, 'READ')").

Sprawdzam, aby upewnić się, że klasa użytkownika, która rozszerza UserDetails, zwraca tę samą nazwę użytkownika przez getUsername(), którą posiadasz w bazie danych. Oprócz sprawdzania, czy zabezpieczenia i aplikacja są w tym samym kontekście.

Metoda hasPermission przyjmuje obiekt Authentication jako pierwszy parametr.

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

Przedmiotem Uwierzytelnianie jest klasa realizacji, zwykle UsernamePasswordAuthenticationToken. Tak więc metoda getPrincipal() musi zwrócić obiekt, który ma metodę getUserName(), która zwraca to samo, co w DB.

Spójrz na 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

Nie całkiem używam 'UserDetails' ani' UserDetailsService'. Zwracam tylko 'UsernamePasswordAuthenticationToken' z mojej klasy, która' implementuje AuthenticationProvider'. Czy muszę to zrobić? –

+0

dodać więcej szczegółów. brzmi to tak, jakbyś nie miał prawidłowego ustawienia obiektu uwierzytelniania. – denov

+0

Zgadzam się. Zaktualizowałem to pytanie, aby odzwierciedlić wszystko, co skonfigurowałem do uwierzytelniania/autoryzacji i ACL. –