Spring Security : définition et concepts fondamentaux

Spring Security : c’est quoi et comment ça marche ? Cet article passe en revue les fondamentaux à connaître
Enzo TAMAYOMis à jour le 13 Oct 2023
Spring Security : c’est quoi et comment ça marche ? les fondamentaux à connaître

Avant-propos

Depuis mai 2022, des changements ont été apportés en version 5.7. Le fonctionnement de Spring Security reste le même mais l’implémentation de certaines fonctionnalités peut différer entre les versions antérieures à 5.7 et la version actuelle. La version utilisée pour cet article sera la version 5.7.3 de Spring Security.

Introduction sur Spring Security

Cet article aborde l’implémentation de la sécurité dans Spring Boot 3. Le sujet traité étant assez vaste, cet article traitera d’abord le fonctionnement global de Spring Security tandis qu’un second article montrera comment le configurer selon vos propres besoins.

Les bases de Spring Security

Spring Security est un module de Spring BootFramework Java se basant sur Spring. permettant d’implémenter « l’authentification, l’autorisation ainsi que la protection contre les attaques les plus communes ».

Spring Security intervient en ajoutant des filtres entre le client et la servlet (l’exécution du code à partir du controller) au sein de la FilterChain Lorsque votre client (le navigateur par exemple) va appeler un serveur. Cette FilterChain va être générée par Spring Boot afin de répondre à la requête. Cette chaîne est composée de Filtres et d’une Servlet.

Spring Security intègre par défaut un ensemble de filtres permettant de configurer différents mécanismes de sécurité proposés par le module.

Nous avons créé un projet de départ vide en ajoutant simplement le module spring-boot-starter-web afin de créer un simple contrôleur permettant de réaliser un appel à notre APIUne API est un programme permettant à deux applications distinctes de communiquer entre elles et d’échanger des données.

Spring security tuto

@RestController
@RequestMapping("api/")
@RequestMapping("api/")
public class WelcomeController {

    @GetMapping("welcome")
    ResponseEntity<String> welcome() {
        return ResponseEntity.ok("Hello World!");
    }
}

En réalisant un appel sur ce endpoint depuis un client, il est possible de constater avec un point d’arrêt dans la classe ApplicationFilterChain la servlet qui produira une réponse au client ainsi que la liste des filtres appliqués par défaut dans Spring Boot :

Et voilà la réponse de notre serveur : 

Spring security tuto

Pour l’instant, seuls quatre filtres sont appliqués.

Si on ajoute la dépendance spring-boot-starter-security, on constate en reproduisant la même opération que Spring Security a ajouté un DelegatingFilterProxyRegistrationBean à la liste des filtres de la FilterChain.

Ce filtre exécute un autre filtre qui s’appelle le FilterChainProxy qui implémente la SecurityFilterChain.

C’est la SecurityFilterChain qui est à la base du fonctionnement de Spring Security. La configuration de Spring Security va générer le contenu de cette chaîne et modifier le comportement de toutes nos composantes de sécurité.

Maintenant que vous savez ce qu’est la FilterChain et comment Spring Security va intervenir en y intégrant son propre filtre. Nous allons observer le contenu de cette chaine de sécurité plus en détail afin de comprendre son agencement et ses responsabilités. 

Spring security tutoriel

Les différents composants de la chaîne de sécurité

Il est possible de voir ce que contient cette chaîne en ajoutant un point ou le paramètre suivant dans votre fichier application.properties :

logging.level.org.springframework.security.web.FilterChainProxy=DEBUG

La SecurityFilterChain par défaut dans SpringBoot est la DefaultSecurityFilterChain qui sans aucune configuration de votre part contient 16 filtres différents :

  • DisableEncodeUrlFilter
  • WebAsyncManagerIntegrationFilter
  • SecurityContextHolderFilter
  • HeaderWriterFilter
  • CsrfFilter
  • LogoutFilter
  • UsernamePasswordAuthenticationFilter
  • DefaultLoginPageGeneratingFilter
  • DefaultLogoutPageGeneratingFilter
  • BasicAuthenticationFilter
  • RequestCacheAwareFilter
  • SecurityContextHolderAwareRequestFilter
  • AnonymousAuthenticationFilter
  • SessionManagementFilter
  • ExceptionTranslationFilter
  • AuthorizationFilter

Chaque filtre est un standard proposé par Spring Security, il y en a encore d’autres qui ne sont pas présents par défaut comme le CorsFilter ou le OAuth2LoginAuthenticationFilter par exemple. Tous ces filtres seront exécutés dans l’ordre de premier jusqu’au dernier, chaque filtre appelant son successeur lorsqu’il a terminé son opération. L’ordre des filtres est extrêmement important.

À présent nous connaissons plus en détails le nom des différents filtres par défaut, gardez à l’esprit que chacun de ces filtres a une responsabilité particulière et que nous pouvons en ajouter ou en retirer en modifiant la configuration de Spring Security.

Nous allons voir quelques-uns de ces filtres pour comprendre un peu mieux comment Spring Security intervient sur notre requête et implémente ses protocoles d’authentification et de sécurité.

SecurityContextHolderFilter

Le SecurityContext est au cœur du fonctionnement de l’authentification, c’est à l’intérieur de celui-ci que l’identité de l’utilisateur, ses identifiants ainsi que ses droits seront injectés dans le SecurityContextHolder afin d’être accessible au sein de la chaine de validation. Cette information permettra à Spring de définir si l’utilisateur a le droit d’accéder à la ressource appelée ou non.

Le SecurityContextHolderFilter est chargé d’appliquer la stratégie de contexte propre à notre requête, la configuration par défaut de Spring Security applique la ThreadLocalSecurityContextHolderStrategy qui stocke le contexte dans un ThreadLocal permettant d’isoler celui-ci des autres threads pouvant être exécuté par Spring. Le contexte est disponible pour toutes les méthodes présentes au sein du thread courant et il n’est donc pas nécessaire de conserver manuellement une référence pour pouvoir y accéder. 

Spring security tutorial

BasicAuthenticationFilter

Lorsque nous ajoutons la dépendance à Spring Security pour la première fois et que nous lançons notre programme Spring Boot, le compilateur nous indique un mot de passe :

Using generated security password: ****-**-**-**-** This generated password is for development use only. Your security configuration must be updated before running your application in production.This generated password is for development use only. Your security configuration must be updated before running your application in production.

Une authentification par défaut est mise en place en installant Spring Security. Pour le vérifier, il suffit d’essayer à nouveau d’effectuer la requête que nous avons faite : 

Spring security tuto

Nous avons reçu un code d’erreur 401 Unauthorized. Cette erreur nous indique que l’accès à cette route nécessite une authentification que nous n’avons pas fournie. En effectuant à nouveau la requête en indiquant cette fois une authentification avec le nom d’utilisateur par défaut user et le mot de passe qui est écrit dans la console au lancement : 

Spring security tuto

On constate que l’authentification fonctionne correctement. Le client utilisé est le logiciel Postman, en indiquant une authentification avec nom et mot de passe, notre client va générer un entête nommé Authorization contenant la méthode d’authentification utilisé et la valeur de nos identifiants (Credentials). Dans notre cas le mot clé Basic représente la méthode d’authentification avec un nom et un mot de passe. 

Spring security tuto

Le filtre qui s’est chargé du traitement cet en-tête n’est autre que le BasicAuthenticationFilter, nous allons voir comment ce filtre lit notre requête pour produire une preuve d’authentification.

Notre filtre a reçu en paramètre la requête que nous avons envoyée, la réponse qui sera retournée ainsi que le reste des filtres qui sont appliqués par Spring Security.

Tout d’abord un convertisseur, le BasicAuthenticationConverter, se charge de récupérer la valeur de notre en-tête Authorization et de la convertir dans un objet appelé UsernamePasswordAuthenticationToken.

UsernamePasswordAuthenticationToken authRequest = this.authenticationConverter.convert(request);
if (authRequest == null) {
   this.logger.trace("Did not process authentication request since failed to find "
         + "username and password in Basic Authorization header");
   chain.doFilter(request, response);
   return;
}

Si l’objet n’a pas été généré, cela signifie que ce type d’authentification n’a pas été utilisé, on passe donc simplement au filtre suivant.

Dans le cas contraire, on vérifie si l’authentification est nécessaire en regardant ce qui est déjà présent dans le securityContext car si une authentification différente est déjà présente, on ne réalise pas une nouvelle authentification par-dessus.

Si une authentification est exécutée avant ce filtre, elle prend donc la priorité sur la méthode nom / mot de passe.

if (authenticationIsRequired(username)) {
   Authentication authResult = this.authenticationManager.authenticate(authRequest);
   SecurityContext context = SecurityContextHolder.createEmptyContext();
   context.setAuthentication(authResult);
   SecurityContextHolder.setContext(context);
   if (this.logger.isDebugEnabled()) {
      this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
   }
   this.rememberMeServices.loginSuccess(request, response, authResult);
   this.securityContextRepository.saveContext(context, request, response);
   onSuccessfulAuthentication(request, response, authResult);
}

Si l’authentification est nécessaire, le filtre commence l’exécution du cheminement standard de Spring Security. Ce protocole va permettre à Spring Security d’appliquer une méthode de vérification à la méthode d’authenfication. 

Spring security tuto
Source

Le token qui a été généré par notre filtre implémente la classe AbstractAuthenticationToken qui implémente elle-même la classe Authentication, elle représente une forme standard et valide d’authentification.

Le filtre va envoyer le token au AuthenticationManager. Par défaut c’est un ProviderManager qui contient une liste d’AuthenticationProvider, une classe chargée de recevoir un objet Authentication et de fournir une réponse à la question suivante :

L’Authentication fournie est-elle valide et si oui, quel droit possède-t-elle ?

Cependant, il peut exister plusieurs manières de répondre à ces questions dans une application en fonction du type d’authentification utilisé. Un nombre illimité de providers peut être ajouté mais un seul provider sera utilisé.

ExceptionTranslationFilter

Ce filtre est exécuté à la fin de la chaîne de sécurité. Son rôle est de gérer la réponse HTTP produite en cas d’erreur. Par défaut, certaines erreurs comme les RuntimeException sont simplement relancées en l’état.

Les AuthenticationException et les AccessDeniedException en revanche font chacune l’objet d’une implémentation spéciale. En effet, lorqu’un utilisateur tente d’accéder à une ressource protégée par l’authentification, il faut parfois lui demander de s’authentifier pour ensuite le rediriger vers la ressource à laquelle il tentait d’accéder.

C’est pourquoi, le filtre implémente deux fonctions handleAuthenticationException et handleAccessDeniedException chargées définir si le client doit pouvoir être réauthentifié ou si une erreur doit être propagée. Dans le cas où l’authentification doit être proposée, la fonction suivante est appelée afin de mettre en attente la requête de l’utilisateur le temps de sa nouvelle authentification. Le SecurityContext est également vidé car considéré caduc dans cette situation.

protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
      AuthenticationException reason) throws ServletException, IOException {
   // SEC-112: Clear the SecurityContextHolder's Authentication, as the
   // existing Authentication is no longer considered valid
   SecurityContext context = SecurityContextHolder.createEmptyContext();
   SecurityContextHolder.setContext(context);
   this.requestCache.saveRequest(request, response);
   this.authenticationEntryPoint.commence(request, response, reason);

Pour conclure sur Spring Security

En tant que développeur, la sécurité est un domaine critique des solutions que nous construisons.

Il est essentiel de mettre en place une implémentation robuste des différents protocoles de sécurité dans chaque application. Comme vu dans cet article, Spring Security nous permet en quelques secondes d’intégrer automatiquement tous ces protocoles.

Évidemment, il faut ensuite les configurer pour répondre au besoin spécifique à chaque environnement, encore faut-il savoir comment !

RDV dans un prochain article pour aborder ce point !