Introduction à la sécurité de la méthode Spring

Introduction à la sécurité de la méthode Spring

1. introduction

En termes simples, Spring Security prend en charge la sémantique des autorisations au niveau de la méthode.

En règle générale, nous pouvons sécuriser notre couche service en limitant, par exemple, les rôles capables d'exécuter une méthode particulière et en la testant à l'aide d'un support dédié de test de sécurité au niveau de la méthode.

Dans cet article, nous allons d'abord examiner l'utilisation de certaines annotations de sécurité. Ensuite, nous nous concentrerons sur le test de la sécurité de nos méthodes avec différentes stratégies.

2. Activation de la sécurité des méthodes

Tout d'abord, pour utiliser Spring Method Security, nous devons ajouter la dépendancespring-security-config:


    org.springframework.security
    spring-security-config

On peut trouver sa dernière version surMaven Central.

Si nous voulons utiliser Spring Boot, nous pouvons utiliser la dépendancespring-boot-starter-security qui inclutspring-security-config:


    org.springframework.boot
    spring-boot-starter-security

Encore une fois, la dernière version peut être trouvée surMaven Central.

Ensuite, nous devons activer la sécurité globale des méthodes:

@Configuration
@EnableGlobalMethodSecurity(
  prePostEnabled = true,
  securedEnabled = true,
  jsr250Enabled = true)
public class MethodSecurityConfig
  extends GlobalMethodSecurityConfiguration {
}
  • La propriétéprePostEnabled active les annotations pré / post Spring Security

  • La propriétésecuredEnabled détermine si l'annotation@Secured doit être activée

  • La propriétéjsr250Enabled nous permet d'utiliser l'annotation@RoleAllowed

Nous explorerons plus en détail ces annotations dans la section suivante.

3. Application de la sécurité des méthodes

3.1. Utilisation de l'annotation@Secured

The @Secured annotation is used to specify a list of roles on a method. Par conséquent, un utilisateur ne peut accéder à cette méthode que s'il possède au moins un des rôles spécifiés.

Définissons une méthodegetUsername:

@Secured("ROLE_VIEWER")
public String getUsername() {
    SecurityContext securityContext = SecurityContextHolder.getContext();
    return securityContext.getAuthentication().getName();
}

Ici, l'annotation@Secured(“ROLE_VIEWER”) définit que seuls les utilisateurs ayant le rôleROLE_VIEWER peuvent exécuter la méthodegetUsername.

De plus, nous pouvons définir une liste de rôles dans une annotation@Secured:

@Secured({ "ROLE_VIEWER", "ROLE_EDITOR" })
public boolean isValidUsername(String username) {
    return userRoleRepository.isValidUsername(username);
}

Dans ce cas, la configuration indique que si un utilisateur possèdeROLE_VIEWER ouROLE_EDITOR, cet utilisateur peut appeler la méthodeisValidUsername.

L'annotation@Secured ne prend pas en charge Spring Expression Language (SpEL).

3.2. Utilisation de l'annotation@RoleAllowed

L’annotation@RoleAllowed est l’annotation équivalente du JSR-250 de l’annotation@Secured .

Fondamentalement, nous pouvons utiliser l'annotation@RoleAllowed de la même manière que@Secured. Ainsi, nous pourrions redéfinir les méthodesgetUsername etisValidUsername:

@RolesAllowed("ROLE_VIEWER")
public String getUsername2() {
    //...
}

@RolesAllowed({ "ROLE_VIEWER", "ROLE_EDITOR" })
public boolean isValidUsername2(String username) {
    //...
}

De même, seul l'utilisateur qui a le rôleROLE_VIEWER peut exécutergetUsername2.

Encore une fois, un utilisateur ne peut appelerisValidUsername2 que s'il a au moins un des rôlesROLE_VIEWER ouROLER_EDITOR.

3.3. Utilisation des annotations@PreAuthorize et@PostAuthorize

Both @PreAuthorize and @PostAuthorize annotations provide expression-based access control. Par conséquent, les prédicats peuvent être écrits en utilisantSpEL (Spring Expression Language).

The @PreAuthorize annotation checks the given expression before entering the method, alors que,the @PostAuthorize annotation verifies it after the execution of the method and could alter the result.

Maintenant, déclarons une méthodegetUsernameInUpperCase comme ci-dessous:

@PreAuthorize("hasRole('ROLE_VIEWER')")
public String getUsernameInUpperCase() {
    return getUsername().toUpperCase();
}

Le@PreAuthorize(“hasRole(‘ROLE_VIEWER')”) a la même signification que@Secured(“ROLE_VIEWER”) que nous avons utilisé dans la section précédente. N'hésitez pas à découvrir plus desecurity expressions details in previous articles.

Par conséquent, l'annotation@Secured(\{“ROLE_VIEWER”,”ROLE_EDITOR”}) peut être remplacée par@PreAuthorize(“hasRole(‘ROLE_VIEWER') or hasRole(‘ROLE_EDITOR')”):

@PreAuthorize("hasRole('ROLE_VIEWER') or hasRole('ROLE_EDITOR')")
public boolean isValidUsername3(String username) {
    //...
}

De plus,we can actually use the method argument as part of the expression:

@PreAuthorize("#username == authentication.principal.username")
public String getMyRoles(String username) {
    //...
}

Ici, un utilisateur ne peut appeler la méthodegetMyRoles que si la valeur de l’argumentusername est la même que le nom d’utilisateur du principal actuel.

It’s worth to note that @PreAuthorize expressions can be replaced by @PostAuthorize ones.

RéécrivonsgetMyRoles:

@PostAuthorize("#username == authentication.principal.username")
public String getMyRoles2(String username) {
    //...
}

Dans l'exemple précédent, toutefois, l'autorisation serait retardée après l'exécution de la méthode cible.

De plus,the @PostAuthorize annotation provides the ability to access the method result:

@PostAuthorize
  ("returnObject.username == authentication.principal.nickName")
public CustomUser loadUserDetail(String username) {
    return userRoleRepository.loadUserByUserName(username);
}

Dans cet exemple, la méthodeloadUserDetail ne s’exécuterait avec succès que si leusername duCustomUser renvoyé est égal aunickname. du principal d’authentification actuel

Dans cette section, nous utilisons principalement des expressions simples Spring. Pour des scénarios plus complexes, nous pourrions créer descustom security expressions.

3.4. Utilisation des annotations@PreFilter et@PostFilter

Spring Security provides the @PreFilter annotation to filter a collection argument before executing the method:

@PreFilter("filterObject != authentication.principal.username")
public String joinUsernames(List usernames) {
    return usernames.stream().collect(Collectors.joining(";"));
}

Dans cet exemple, nous joignons tous les noms d’utilisateurs à l’exception de celui qui est authentifié.

Ici,our expression uses the name filterObject to represent the current object in the collection.

Cependant, si la méthode a plus d'un argument qui est un type de collection, nous devons utiliser la propriétéfilterTarget pour spécifier l'argument que nous voulons filtrer:

@PreFilter
  (value = "filterObject != authentication.principal.username",
  filterTarget = "usernames")
public String joinUsernamesAndRoles(
  List usernames, List roles) {

    return usernames.stream().collect(Collectors.joining(";"))
      + ":" + roles.stream().collect(Collectors.joining(";"));
}

De plus,we can also filter the returned collection of a method by using @PostFilter annotation:

@PostFilter("filterObject != authentication.principal.username")
public List getAllUsernamesExceptCurrent() {
    return userRoleRepository.getAllUsernames();
}

Dans ce cas, le nomfilterObject fait référence à l'objet actuel dans la collection retournée.

Avec cette configuration, Spring Security parcourra la liste renvoyée et supprimera toute valeur correspondant au nom d'utilisateur du mandant.

Plus de détails sur@PreFilter et@PostFilter peuvent être trouvés dans l'articleSpring Security – @PreFilter and @PostFilter.

3.5. Méta-annotation de sécurité de méthode

Nous nous trouvons généralement dans une situation où nous protégeons différentes méthodes en utilisant la même configuration de sécurité.

Dans ce cas, nous pouvons définir une méta-annotation de sécurité:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('VIEWER')")
public @interface IsViewer {
}

Ensuite, nous pouvons directement utiliser l'annotation @IsViewer pour sécuriser notre méthode:

@IsViewer
public String getUsername4() {
    //...
}

Les méta-annotations de sécurité sont une excellente idée car elles ajoutent davantage de sémantique et découplent notre logique métier du cadre de sécurité.

3.6. Annotation de sécurité au niveau de la classe

Si nous nous retrouvons à utiliser la même annotation de sécurité pour chaque méthode d'une classe, nous pouvons envisager de placer cette annotation au niveau de la classe:

@Service
@PreAuthorize("hasRole('ROLE_ADMIN')")
public class SystemService {

    public String getSystemYear(){
        //...
    }

    public String getSystemDate(){
        //...
    }
}

Dans l'exemple ci-dessus, la règle de sécuritéhasRole(‘ROLE_ADMIN') sera appliquée aux méthodesgetSystemYear etgetSystemDate.

3.7. Annotations de sécurité multiples sur une méthode

Nous pouvons également utiliser plusieurs annotations de sécurité sur une méthode:

@PreAuthorize("#username == authentication.principal.username")
@PostAuthorize("returnObject.username == authentication.principal.nickName")
public CustomUser securedLoadUserDetail(String username) {
    return userRoleRepository.loadUserByUserName(username);
}

Par conséquent, Spring vérifiera l'autorisation avant et après l'exécution de la méthodesecuredLoadUserDetail.

4. Considérations importantes

Nous souhaitons rappeler deux points concernant la sécurité des méthodes:

  • By default, Spring AOP proxying is used to apply method security – si une méthode sécurisée A est appelée par une autre méthode de la même classe, la sécurité dans A est complètement ignorée. Cela signifie que la méthode A sera exécutée sans vérification de sécurité. La même chose s'applique aux méthodes privées

  • Spring SecurityContext is thread-bound – par défaut, le contexte de sécurité n'est pas propagé aux threads enfants. Pour plus d'informations, nous pouvons vous référer à l'article deSpring Security Context Propagation

5. Sécurité des méthodes de test

5.1. Configuration

Pour tester Spring Security avec JUnit, nous avons besoin de la dépendancespring-security-test:


    org.springframework.security
    spring-security-test

Nous n'avons pas besoin de spécifier la version de la dépendance car nous utilisons le plugin Spring Boot. Les dernières versions de cette dépendance peuvent être trouvées surMaven Central.

Ensuite, configurons un simple test Spring Integration en spécifiant le runner et la configuration deApplicationContext:

@RunWith(SpringRunner.class)
@ContextConfiguration
public class TestMethodSecurity {
    // ...
}

5.2. Test du nom d'utilisateur et des rôles

Maintenant que notre configuration est prête, essayons de tester notre méthodegetUsername qui est sécurisée par l'annotation@Secured(“ROLE_VIEWER”):

@Secured("ROLE_VIEWER")
public String getUsername() {
    SecurityContext securityContext = SecurityContextHolder.getContext();
    return securityContext.getAuthentication().getName();
}

Puisque nous utilisons ici l'annotation@Secured, il faut qu'un utilisateur soit authentifié pour appeler la méthode. Sinon, nous obtiendrons unAuthenticationCredentialsNotFoundException.

Par conséquent,we need to provide a user to test our secured method. To achieve this, we decorate the test method with @WithMockUser and provide a user and roles:

@Test
@WithMockUser(username = "john", roles = { "VIEWER" })
public void givenRoleViewer_whenCallGetUsername_thenReturnUsername() {
    String userName = userRoleService.getUsername();

    assertEquals("john", userName);
}

Nous avons fourni un utilisateur authentifié dont le nom d'utilisateur estjohn et dont le rôle estROLE_VIEWER. Si nous ne spécifions pas lesusername ourole, leusername par défaut estuser et lerole par défaut estROLE_USER.

Notez qu'il n'est pas nécessaire d'ajouter le préfixeROLE_ ici, Spring Security ajoutera ce préfixe automatiquement.

Si nous ne voulons pas avoir ce préfixe, nous pouvons envisager d'utiliserauthority au lieu derole.

Par exemple, déclarons une méthodegetUsernameInLowerCase:

@PreAuthorize("hasAuthority('SYS_ADMIN')")
public String getUsernameLC(){
    return getUsername().toLowerCase();
}

Nous pourrions tester cela en utilisant les autorités:

@Test
@WithMockUser(username = "JOHN", authorities = { "SYS_ADMIN" })
public void givenAuthoritySysAdmin_whenCallGetUsernameLC_thenReturnUsername() {
    String username = userRoleService.getUsernameInLowerCase();

    assertEquals("john", username);
}

Idéalement,if we want to use the same user for many test cases, we can declare the @WithMockUser annotation at test class:

@RunWith(SpringRunner.class)
@ContextConfiguration
@WithMockUser(username = "john", roles = { "VIEWER" })
public class TestWithMockUserAtClassLevel {
    //...
}

Si nous voulions exécuter notre test en tant qu'utilisateur anonyme, nous pourrions utiliser l'annotation@WithAnonymousUser:

@Test(expected = AccessDeniedException.class)
@WithAnonymousUser
public void givenAnomynousUser_whenCallGetUsername_thenAccessDenied() {
    userRoleService.getUsername();
}

Dans l'exemple ci-dessus, nous attendons unAccessDeniedException car l'utilisateur anonyme ne dispose pas du rôleROLE_VIEWER ou de l'autoritéSYS_ADMIN.

5.3. Test avec unUserDetailsService personnalisé

For most applications, it’s common to use a custom class as authentication principal. Dans ce cas, la classe personnalisée doit implémenter l'interfaceorg.springframework.security.core.userdetails.UserDetails.

Dans cet article, nous déclarons une classeCustomUser qui étend l'implémentation existante deUserDetails, qui estorg.springframework.security.core.userdetails.User:

public class CustomUser extends User {
    private String nickName;
    // getter and setter
}

Reprenons l'exemple avec l'annotation@PostAuthorize de la section 3:

@PostAuthorize("returnObject.username == authentication.principal.nickName")
public CustomUser loadUserDetail(String username) {
    return userRoleRepository.loadUserByUserName(username);
}

Dans ce cas, la méthode ne s’exécute avec succès que si lesusername desCustomUser renvoyés sont égaux auxnickname du principal d’authentification actuel.

Si nous voulions tester cette méthode, we could provide an implementation of UserDetailsService which could load our CustomUser based on the username:

@Test
@WithUserDetails(
  value = "john",
  userDetailsServiceBeanName = "userDetailService")
public void whenJohn_callLoadUserDetail_thenOK() {

    CustomUser user = userService.loadUserDetail("jane");

    assertEquals("jane", user.getNickName());
}

Ici, l'annotation@WithUserDetails indique que nous utiliserons unUserDetailsService pour initialiser notre utilisateur authentifié. Le service est référencé par la propriétéuserDetailsServiceBeanName. CeUserDetailsService peut être une implémentation réelle ou un faux à des fins de test.

De plus, le service utilisera la valeur de la propriétévalue comme nom d'utilisateur pour chargerUserDetails.

De manière pratique, nous pouvons également décorer avec une annotation@WithUserDetails au niveau de la classe, de la même manière que nous avons fait avec l'annotation@WithMockUser.

5.4. Test avec des annotations méta

Nous nous retrouvons souvent à réutiliser les mêmes utilisateurs / rôles encore et encore dans divers tests.

Dans ces situations, il est pratique de créer unmeta-annotation.

En reprenant l'exemple précédent@WithMockUser(username=”john”, roles=\{“VIEWER”}), nous pouvons déclarer une méta-annotation comme:

@Retention(RetentionPolicy.RUNTIME)
@WithMockUser(value = "john", roles = "VIEWER")
public @interface WithMockJohnViewer { }

Ensuite, nous pouvons simplement utiliser@WithMockJohnViewer dans notre test:

@Test
@WithMockJohnViewer
public void givenMockedJohnViewer_whenCallGetUsername_thenReturnUsername() {
    String userName = userRoleService.getUsername();

    assertEquals("john", userName);
}

De même, nous pouvons utiliser des méta-annotations pour créer des utilisateurs spécifiques à un domaine en utilisant@WithUserDetails.

6. Conclusion

Dans ce didacticiel, nous avons exploré différentes options pour utiliser Method Security dans Spring Security.

Nous avons également passé en revue quelques techniques pour tester facilement la sécurité des méthodes et apprendre à réutiliser des utilisateurs fictifs lors de différents tests.

Tous les exemples de ce tutoriel peuvent être trouvésover on Github.