Points d’entrée multiples dans la sécurité de printemps

1. Vue d’ensemble

Dans ce rapide didacticiel, nous allons examiner comment définir plusieurs points d’entrée dans une application Spring Security .

Cela implique principalement de définir plusieurs blocs http dans un fichier de configuration XML ou plusieurs instances HttpSecurity en étendant la classe WebSecurityConfigurerAdapter plusieurs fois.

2. Dépendances Maven

Pour le développement, nous aurons besoin des dépendances suivantes:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    <version>2.0.0.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>2.0.0.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
    <version>2.0.0.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <version>2.0.0.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
    <version>5.0.4.RELEASE</version>
</dependency>

Les dernières versions de spring-boot-starter-security , https://search.maven.org/classic/#search%7Cga%7C1%7Ca%3A%22spring-boot-starter-web%22%20and%20%%%%%%%%%%%%%Cs%A9%C3%A9%C3%A9%C3%A1%C3%A9s%C3%A9s ], spring-boot-starter-thymeleaf , https://recherche . maven.org/classic/#search%7Cga%7C1%7Ca%3A%22spring-boot-starter-test%22[spring-boot-starter-test], https://search.maven.org/classic/#search % 7Cga% 7C1% 7Ca% 3A% 22Spring-Security-Test% 22[Spring-Security-Test]peut être téléchargé à partir de Maven Central.

3. Points d’entrée multiples

** 3.1. Plusieurs points d’entrée avec plusieurs éléments HTTP

**

Définissons la classe de configuration principale qui contiendra une source d’utilisateurs:

@Configuration
@EnableWebSecurity
public class MultipleEntryPointsSecurityConfig {

    @Bean
    public UserDetailsService userDetailsService() throws Exception {
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User
          .withUsername("user")
          .password(encoder().encode("userPass"))
          .roles("USER").build());
        manager.createUser(User
          .withUsername("admin")
          .password(encoder().encode("adminPass"))
          .roles("ADMIN").build());
        return manager;
    }

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

Voyons maintenant comment nous pouvons définir plusieurs points d’entrée dans notre configuration de sécurité.

Nous allons utiliser ici un exemple basé sur l’authentification de base et utiliser le fait que Spring Security prend en charge la définition de plusieurs éléments HTTP dans nos configurations.

Lorsque vous utilisez la configuration Java, vous pouvez définir plusieurs domaines de sécurité en utilisant plusieurs classes @ Configuration qui étendent la classe de base WebSecurityConfigurerAdapter , chacune avec sa propre configuration de sécurité. Ces classes peuvent être statiques et placées dans la configuration principale.

La principale motivation pour avoir plusieurs points d’entrée dans une application réside dans le fait qu’il existe différents types d’utilisateurs pouvant accéder à différentes parties de l’application.

Définissons une configuration avec trois points d’entrée, chacun avec des autorisations et des modes d’authentification différents:

  • un pour les utilisateurs administratifs utilisant l’authentification HTTP de base

  • un pour les utilisateurs réguliers qui utilisent l’authentification par formulaire

  • et un pour les utilisateurs invités ne nécessitant pas d’authentification

Le point d’entrée défini pour les utilisateurs administratifs sécurise les URL de la forme /admin/ pour n’autoriser que les utilisateurs dotés du rôle ADMIN et requiert une authentification HTTP de base avec un point d’entrée de type BasicAuthenticationEntryPoint défini à l’aide de la méthode authenticationEntryPoint () :

@Configuration
@Order(1)
public static class App1ConfigurationAdapter extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.antMatcher("/admin/** ** ")
            .authorizeRequests().anyRequest().hasRole("ADMIN")
            .and().httpBasic().authenticationEntryPoint(authenticationEntryPoint());
    }

    @Bean
    public AuthenticationEntryPoint authenticationEntryPoint(){
        BasicAuthenticationEntryPoint entryPoint =
          new BasicAuthenticationEntryPoint();
        entryPoint.setRealmName("admin realm");
        return entryPoint;
    }
}

L’annotation @ Order sur chaque classe statique indique l’ordre dans lequel les configurations seront considérées pour en trouver une qui correspond à l’URL demandée. La valeur de order pour chaque classe doit être unique.

Le bean de type BasicAuthenticationEntryPoint nécessite que la propriété realName soit définie.

3.2. Plusieurs points d’entrée, même élément HTTP

Ensuite, définissons la configuration pour les URL de la forme /utilisateur/ accessibles aux utilisateurs ordinaires dotés du rôle USER à l’aide de l’authentification de formulaire:

@Configuration
@Order(2)
public static class App2ConfigurationAdapter extends WebSecurityConfigurerAdapter {

    protected void configure(HttpSecurity http) throws Exception {
        http.antMatcher("/user/** ** ")
            .authorizeRequests().anyRequest().hasRole("USER")
            .and()
           //formLogin configuration
            .and()
            .exceptionHandling()
            .defaultAuthenticationEntryPointFor(
              loginUrlauthenticationEntryPointWithWarning(),
              new AntPathRequestMatcher("/user/private/** ** "))
            .defaultAuthenticationEntryPointFor(
              loginUrlauthenticationEntryPoint(),
              new AntPathRequestMatcher("/user/general/** ** "));
    }
}

Comme nous pouvons le constater, un autre moyen de définir des points d’entrée, outre la méthode authenticationEntryPoint (), consiste à utiliser la méthode defaultAuthenticationEntryPointFor () . Cela peut définir plusieurs points d’entrée correspondant à différentes conditions en fonction d’un objet RequestMatcher .

L’interface RequestMatcher a des implémentations basées sur différents types de conditions, telles que le chemin d’accès, le type de support ou l’expression rationnelle correspondants. Dans notre exemple, nous avons utilisé AntPathRequestMatch pour définir deux points d’entrée différents pour les URL des formulaires /user/private/ et /user/general/ .

Ensuite, nous devons définir les beans de points d’entrée dans la même classe de configuration statique:

@Bean
public AuthenticationEntryPoint loginUrlauthenticationEntryPoint(){
    return new LoginUrlAuthenticationEntryPoint("/userLogin");
}

@Bean
public AuthenticationEntryPoint loginUrlauthenticationEntryPointWithWarning(){
    return new LoginUrlAuthenticationEntryPoint("/userLoginWithWarning");
}

Le point principal ici est de savoir comment configurer ces points d’entrée multiples - pas nécessairement les détails d’implémentation de chacun.

Dans ce cas, les points d’entrée sont tous deux de type LoginUrlAuthenticationEntryPoint et utilisent une URL de page de connexion différente:

/userLogin pour une page de connexion simple et /userLoginWithWarning pour une page de connexion qui affiche également un avertissement lors de la tentative d’accès aux URL /utilisateur/ privées.

Cette configuration nécessitera également la définition des mappages MVC /userLogin et /userLoginWithWarning MVC ainsi que deux pages avec un formulaire de connexion standard.

Pour l’authentification du formulaire, il est très important de noter que toute URL nécessaire à la configuration, telle que l’URL de traitement de la connexion, doit également suivre le format /user/ ou être autrement configurée pour être accessible.

Les deux configurations ci-dessus seront redirigées vers une URL /403 si un utilisateur sans le rôle approprié tente d’accéder à une URL protégée.

  • Veillez à utiliser des noms uniques pour les beans même s’ils se trouvent dans des classes statiques différentes ** , sinon l’un des deux remplace l’autre.

3.3. Nouvel élément HTTP, pas de point d’entrée

Enfin, définissons la troisième configuration pour les URL de la forme /guest/ qui autorisera tous les types d’utilisateur, y compris les utilisateurs non authentifiés:

@Configuration
@Order(3)
public static class App3ConfigurationAdapter extends WebSecurityConfigurerAdapter {

    protected void configure(HttpSecurity http) throws Exception {
        http.antMatcher("/guest/** ** ").authorizeRequests().anyRequest().permitAll();
    }
}

3.4. Configuration XML

Jetons un coup d’œil à la configuration XML équivalente pour les trois instances de HttpSecurity dans la section précédente.

Comme prévu, il contiendra trois blocs XML <http> distincts.

Pour les URL /admin/ , la configuration XML utilisera l’attribut entry-point-ref de l’élément http-basic :

<security:http pattern="/admin/** ** " use-expressions="true" auto-config="true">
    <security:intercept-url pattern="/** ** " access="hasRole('ROLE__ADMIN')"/>
    <security:http-basic entry-point-ref="authenticationEntryPoint"/>
</security:http>

<bean id="authenticationEntryPoint"
  class="org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint">
     <property name="realmName" value="admin realm"/>
</bean>

Il est à noter que si vous utilisez la configuration XML, les rôles doivent être de la forme ROLE <NOM RÔLE> .

La configuration des URL /utilisateur/ devra être scindée en deux blocs http dans xml car il n’existe pas d’équivalent direct à la méthode defaultAuthenticationEntryPointFor () .

La configuration pour les URL/utilisateur/général/ est la suivante:

<security:http pattern="/user/general/** ** " use-expressions="true" auto-config="true"
  entry-point-ref="loginUrlAuthenticationEntryPoint">
    <security:intercept-url pattern="/** ** " access="hasRole('ROLE__USER')"/>
   //form-login configuration
</security:http>

<bean id="loginUrlAuthenticationEntryPoint"
  class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
  <constructor-arg name="loginFormUrl" value="/userLogin"/>
</bean>

Pour les URL /user/private/ , nous pouvons définir une configuration similaire:

<security:http pattern="/user/private/** ** " use-expressions="true" auto-config="true"
  entry-point-ref="loginUrlAuthenticationEntryPointWithWarning">
    <security:intercept-url pattern="/** ** " access="hasRole('ROLE__USER')"/>
   //form-login configuration
</security:http>

<bean id="loginUrlAuthenticationEntryPointWithWarning"
  class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
    <constructor-arg name="loginFormUrl" value="/userLoginWithWarning"/>
</bean>

Pour les URL /guest/ , nous aurons l’élément http :

<security:http pattern="/** ** " use-expressions="true" auto-config="true">
    <security:intercept-url pattern="/guest/** ** " access="permitAll()"/>
</security:http>

Il est également important de noter qu’au moins un bloc XML <http> doit correspondre au modèle/ .

4. Accéder aux URL protégées

4.1. Configuration MVC

Créons des mappages de demandes qui correspondent aux modèles d’URL que nous avons sécurisés:

@Controller
public class PagesController {

    @GetMapping("/admin/myAdminPage")
    public String getAdminPage() {
        return "multipleHttpElems/myAdminPage";
    }

    @GetMapping("/user/general/myUserPage")
    public String getUserPage() {
        return "multipleHttpElems/myUserPage";
    }

    @GetMapping("/user/private/myPrivateUserPage")
    public String getPrivateUserPage() {
        return "multipleHttpElems/myPrivateUserPage";
    }

    @GetMapping("/guest/myGuestPage")
    public String getGuestPage() {
        return "multipleHttpElems/myGuestPage";
    }

    @GetMapping("/multipleHttpLinks")
    public String getMultipleHttpLinksPage() {
        return "multipleHttpElems/multipleHttpLinks";
    }
}

Le mappage /multipleHttpLinks renverra une simple page HTML avec des liens vers les URL protégées:

<a th:href="@{/admin/myAdminPage}">Admin page</a>
<a th:href="@{/user/general/myUserPage}">User page</a>
<a th:href="@{/user/private/myPrivateUserPage}">Private user page</a>
<a th:href="@{/guest/myGuestPage}">Guest page</a>

Chacune des pages HTML correspondant aux URL protégées aura un texte simple et un lien en arrière:

Welcome admin!

<a th:href="@{/multipleHttpLinks}" >Back to links</a>

4.2. Initialisation de l’application

Nous allons exécuter notre exemple en tant qu’application Spring Boot. Définissons donc une classe avec la méthode main:

@SpringBootApplication
public class MultipleEntryPointsApplication {
    public static void main(String[]args) {
        SpringApplication.run(MultipleEntryPointsApplication.class, args);
    }
}

Si nous voulons utiliser la configuration XML, nous devons également ajouter l’annotation @ ImportResource (\ {"classpath ** : spring-security-multiple-entry.xml"}) à notre classe principale.

4.3. Test de la configuration de sécurité

Configurons une classe de test JUnit que nous pouvons utiliser pour tester nos URL protégées:

@RunWith(SpringRunner.class)
@WebAppConfiguration
@SpringBootTest(classes = MultipleEntryPointsApplication.class)
public class MultipleEntryPointsTest {

    @Autowired
    private WebApplicationContext wac;

    @Autowired
    private FilterChainProxy springSecurityFilterChain;

    private MockMvc mockMvc;

    @Before
    public void setup() {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac)
          .addFilter(springSecurityFilterChain).build();
    }
}

Ensuite, testons les URL avec l’utilisateur admin .

Lorsque vous demandez l’URL /admin/adminPage sans authentification HTTP de base, vous devez vous attendre à recevoir un code d’état Non autorisé. Après l’ajout de l’authentification, le code d’état doit être 200 OK.

Si vous tentez d’accéder à l’URL /user/userPage avec l’utilisateur admin, vous devriez recevoir le statut 302 Forbidden:

@Test
public void whenTestAdminCredentials__thenOk() throws Exception {
    mockMvc.perform(get("/admin/myAdminPage")).andExpect(status().isUnauthorized());

    mockMvc.perform(get("/admin/myAdminPage")
      .with(httpBasic("admin", "adminPass"))).andExpect(status().isOk());

    mockMvc.perform(get("/user/myUserPage")
      .with(user("admin").password("adminPass").roles("ADMIN")))
      .andExpect(status().isForbidden());
}

Créons un test similaire en utilisant les informations d’identité de l’utilisateur habituel pour accéder aux URL:

@Test
public void whenTestUserCredentials__thenOk() throws Exception {
    mockMvc.perform(get("/user/general/myUserPage")).andExpect(status().isFound());

    mockMvc.perform(get("/user/general/myUserPage")
      .with(user("user").password("userPass").roles("USER")))
      .andExpect(status().isOk());

    mockMvc.perform(get("/admin/myAdminPage")
      .with(user("user").password("userPass").roles("USER")))
      .andExpect(status().isForbidden());
}

Dans le deuxième test, nous pouvons constater que l’absence de l’authentification du formulaire entraîne le statut 302 Trouvé au lieu de Non autorisé, car Spring Security redirigera vers le formulaire de connexion.

Enfin, créons un test dans lequel nous accédons à l’URL /guest/guestPage avec les trois types d’authentification et vérifions que nous recevons le statut 200 OK:

@Test
public void givenAnyUser__whenGetGuestPage__thenOk() throws Exception {
    mockMvc.perform(get("/guest/myGuestPage")).andExpect(status().isOk());

    mockMvc.perform(get("/guest/myGuestPage")
      .with(user("user").password("userPass").roles("USER")))
      .andExpect(status().isOk());

    mockMvc.perform(get("/guest/myGuestPage")
      .with(httpBasic("admin", "adminPass")))
      .andExpect(status().isOk());
}

5. Conclusion

Dans ce tutoriel, nous avons montré comment configurer plusieurs points d’entrée lors de l’utilisation de Spring Security.

Le code source complet des exemples est disponible à l’adresse over sur GitHub . Pour exécuter l’application, décommentez la balise MultipleEntryPointsApplication start-class dans le pom.xml et exécutez la commande mvn spring-boot: run , puis accédez à l’URL /multipleHttpLinks

Notez qu’il n’est pas possible de vous déconnecter lorsque vous utilisez l’authentification HTTP de base. Vous devrez donc fermer et rouvrir le navigateur pour supprimer cette authentification.

Pour exécuter le test JUnit, utilisez le profil Maven défini entryPoints à l’aide de la commande suivante:

mvn installation propre -PentryPoints