J'ai configuré ACL dans mon application Spring Boot. La configuration de la liste de contrôle d'accès est la suivante:
@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 classe CustomAuthenticationProvider
:
@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 ma 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 j'ai sauté pour des raisons de longueur.
Et j'utilise un schéma MySQL qui peut être trouvé ici .
J'ajoute des entrées à mes tables connexes acl 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, c'est:
Denying user demo permission 'READ' on object com.company.project.domain.users.User@4a49e9b4
dans mon
@PostFilter("hasPermission(filterObject, 'READ')")
Je soupçonne plusieurs problèmes ici:
hasPermission
: Je l'ai remplacée par «READ» et «1», mais pas du tout.expressionHandler.setPermissionEvaluator(new AclPermissionEvaluator(aclService()));
est suffisant?Mettre à jour
Exemple de méthode 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 n ° 2:
La question reflète maintenant tout ce qui a été mis en place en matière d’authentification/autorisation/ACL.
Mise à jour n ° 3:
Je suis maintenant très proche pour résoudre le problème, la seule chose qui reste à faire est de résoudre ceci:
Si quelqu'un peut m'aider avec cette question, je peux enfin avoir un compte rendu de ce que j'ai vécu pour résoudre ce problème.
Voici la réponse attendue depuis longtemps:
La documentation décrit clairement:
Pour utiliser les expressions hasPermission (), vous devez configurer explicitement un PermissionEvaluator dans votre contexte d'application. Cela ressemblerait quelque chose comme ça:
donc en gros je faisais dans ma AclConfiguration
qui étend GlobalMethodSecurityConfiguration
:
@Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
expressionHandler.setPermissionEvaluator(new AclPermissionEvaluator(aclService()));
expressionHandler.setPermissionCacheOptimizer(new AclPermissionCacheOptimizer(aclService()));
return expressionHandler;
}
Ce qui n'était pas traité par Spring!
J'ai dû séparer AclConfig
et GlobalMethodSecurityConfiguration
. Quand il y a @Bean
s définis 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).
J'ai mis à niveau mon application pour utiliser Spring Security 4.2.1.RELEASE, puis un accès inattendu a été refusé dans toutes les méthodes annotées avec @PreAuthorize
, qui fonctionnait parfaitement avant la mise à niveau . J'ai débogué le code de sécurité Spring et j'ai réalisé que le problème était que tous les rôles à vérifier étaient précédés d'une chaîne par défaut "ROLE_", même si j'avais défini mon préfixe par défaut sur vide, 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 contrôleur ont été annotées avec @PreAuthorize("hasRole('my_ldap_group_name')")
; cependant, la structure ne prenait pas en compte mon paramètre de préfixe de rôle vide et utilisait donc ROLE_my_ldap_group_name pour vérifier le rôle réel.
Après avoir approfondi le code du framework, je me suis rendu compte que le préfixe de rôle par défaut de la classe org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler
était toujours défini sur "ROLE_"
. J'ai suivi la source de sa valeur et j'ai découvert qu'il recherchait d'abord un bean déclaré de la classe org.springframework.security.config.core.GrantedAuthorityDefaults
afin de rechercher un préfixe par défaut lors de la première initialisation du bean org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer
; toutefois, cet initialiseur n'ayant pas pu le trouver déclaré , il a fini par utiliser le préfixe par défaut susmentionné.
Je pense que ce n'est pas un comportement attendu: Spring Security aurait dû prendre en compte le même rolePrefix de ldapAuthentication. Cependant, pour résoudre ce problème, il était nécessaire d'ajouter le bean org.springframework.security.config.core.GrantedAuthorityDefaults
à mon contexte d'application (j'utilise une configuration basée sur des annotations). Suivant:
@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);
}
}
Vous rencontrez peut-être le même problème - je pourrais voir que vous utilisez DefaultMethodSecurityExpressionHandler et qu'il utilise également le bean GrantedAuthorityDefaults. Par conséquent, si vous utilisez la même version de Spring Security que moi - 4.2.1.RELEASE, vous êtes probablement confronté le même problème.
Vos données dans la base de données et votre configuration sont bonnes. J'utilise @PostFilter("hasPermission(filterObject, 'READ')")
tout le temps.
Je vérifierais que votre classe d'utilisateurs qui étend UserDetails renvoie le même nom d'utilisateur via getUsername () que vous avez dans la base de données. En plus de vérifier que la sécurité et l'application sont dans le même contexte.
la méthode hasPermission prend un objet Authentication en tant que premier paramètre.
boolean hasPermission(Authentication authentication,
Object targetDomainObject,
Object permission)
L'objet Authentification est une classe d'implémentation, généralement de UsernamePasswordAuthenticationToken . Ainsi, la méthode getPrincipal () doit renvoyer un objet doté d'une méthode getUserName () qui renvoie la même chose que 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();
}
}