I. Spring Security
1. Présentation
Spring est considéré comme un terme générique qui couvre unn ensemble de framework dont Spring Security dans l’écosystème Java. Spring Security est un framework d'authentification et d'autorisation puissant qu'on peut personnaliser ( customiser). Pour sécuriser des applications basées sur Spring, on l'utilise et lorsqu'on cherche à implémenter une solution de jeton Spring JWT, il est logique de la baser sur Spring Security. Malgré sa popularité, Spring Security n'est pas simple et direct à configurer. Dans Spring Security des termes son utilisés et il est important de comprendre leur signification.
1.1. L'authentification
Elle fait référence au processus de vérification de l'identité d'un utilisateur, sur la base des informations d'identification fournies. Un exemple courant consiste à saisir un nom d'utilisateur et un mot de passe lorsque vous vous connectez à un site Web. Vous pouvez y voir une réponse à la question Qui êtes-vous ? .
1.2. L'autorisation
Elle fait référence au processus permettant de déterminer si un utilisateur dispose de l'autorisation appropriée pour effectuer une action particulière ou lire des données particulières, en supposant que l'utilisateur est authentifié avec succès. Vous pouvez y voir une réponse à la question Un utilisateur peut-il faire/lire ceci ? .
1.3. Le principe
Elle fait référence à l'utilisateur actuellement authentifié.
1.4. L'autorité accordée
Elle fait référence à l'autorisation de l'utilisateur authentifié.
1.5. Le rôle
il fait référence à un groupe d'autorisations de l'utilisateur authentifié
Une fois qu'on a défini ces termes, regardons de prêt l'architecture ou plutôt comment fonctionne Spring Security. L'architecture de Spring Security est conçue de telle sorte que l'on puisse distinguer l'authentification et l'autorisation puis de pouvoir gérer des stratégies et des points d'extension pour les deux.
2. Architecture de Spring Security
Les filtres sont des composants Java enfichables qu'on utilise pour intercepter et traiter des requêtes clientes avant qu'elles ne soient envoyées aux servlets; pour une application basée sur un servlet, Le conteneur de servlet crée un objet de type FilterChain qui contient les instances des différents filtre et de la Servlet de l'application.
comme Spring est un framework basé sur un servlet en l'occurence DispatcherServlet, Si on utilise Spring Security le système de filtres de Spring Security "Spring Security Filter Chain" est pris en charge par la chaine de filtrage du conteneur de servlets au niveau de FilterChaineProxy (voir shéma ci-dessous) par un filtre nommée DelegatingFilterProxy; celui-ci permet de faire le pont entre le cycle de vie du conteneur de servlets et ApplicationContext de Spring
On peut enregistrer DelegatingFilterProxy via les mécanismes du conteneur de Servlet standard, mais déléguer tout le travail à un Spring Bean qui implémente Filter.
Le client envoie une requête à l'application et le conteneur crée un filtre de type FilterChain, qui contient les instances de filtres et le servlet qui doivent traiter les requêtes de type HttpServletRequest, en fonction de l'URI de la requête. Dans une application Spring MVC, le/la Servlet qui traite ces requêtes est une instance de DispatcherServlet. Tout au plus, un/une servlet peut gérer une seule requête de type HttpServletRequest et d'une éponse de type HttpServletResponse.
Spring Security ne sécurise pas les réseaux ou les ordinateurs, mais les applications: il intervient dans le dialogue entre l'application et l'utilisateur (ou entre deux applications). Ce dialogue est géré par le servlet Spring DispatcherServlet , qui redirige les requêtes http vers les classes contrôleur de l'application. En résumé, le rôle de Spring Security est d'insérer des opérations dans cette interaction, grâce à un ensemble de filtres de servlet. Ce groupe de filtres est la chaîne de filtres de Spring Security.
3. Chaîne de filtres de Spring Security
3.1. Généralités
Comme il est précisé ci-dessus, dans une application basée sur des servlets, les requêtes en entrée passent par un certain nombre de filtres; il en est de même des filtres de Spring Security. L'image suivante montre la superposition typique des gestionnaires de filtres pour une seule requête HTTP.
Avant de commencer à personnaliser la configuration, discutons d'abord du fonctionnement de l'authentification de Spring Security en coulisses. Le diagramme suivant montre que Spring Security est un ensemble de filtres; le traitement des requêtes en entrée est géré par "AuthenticationManager" ; le flux est ensuite dispatcher aux différents "AuthentificationProvider".
Lorsqu'on ajoute le framework Spring Security à une application, il enregistre automatiquement une chaîne de filtres qui interceptent toutes les requêtes entrantes. Cette chaîne se compose de différents filtres, et chacun d'entre eux gère un cas d'utilisation particulier.
Cependant, plusieurs filtres peuvent être utilisés pour par exemple :
- Empêchez l'appel des instances de filtre en aval ou certains appels du servlet. Dans ce cas, le filtre écrit généralement la réponse HttpServletResponse.
- Modifiez la requête de type HttpServletRequest ou la réponse de type HttpServletResponse utilisé par les instances de filtre en aval ou du servlet.
La puissance du filtre vient de la FilterChain qui y est transmise.
La chaîne de filtrage effectue des actions avant d'atteindre le Servlet DispatcherServlet, afin de vérifier si une requête provient d'un utilisateur authentifié et autorisé, avant de la laisser aller vers les contrôleurs. En résumé La chaîne de filtrage traite 2 concepts fondamentaux :
authentification : l'utilisateur doit être identifié par une combinaison nom d'utilisateur/mot de passe.
autorisations : les utilisateurs ne sont pas égaux quant aux opérations qu'ils sont autorisés à effectuer. A titre d'exemple, un utilisateur qui n'est pas administrateur ne doit pas être autorisé à modifier le compte d'un autre utilisateur.
3.2. Exemples d'actions traitées par les filtre:
- Vérifiez si l'URL demandée est accessible au public, en fonction de la configuration.
- En cas d'authentification basée sur la session, vérifiez si l'utilisateur est déjà authentifié dans la session en cours.
- Vérifiez si l'utilisateur est autorisé à effectuer l'action demandée, et ainsi de suite. Un détail important qu'il ne faut pas oublier c'est que tous les filtres Spring Security sont enregistrés avec un ordre de traitement; les ordres les plus bas sont les premiers invoqués. Pour certains cas d'utilisation, si on souhaite placer un filtre personnalisé devant ceux de Spring Security, on doit faire la commande. Cela peut être fait avec la configuration suivante :
spring.security.filter.order=10
Une fois que nous aurons ajouté cette configuration à notre fichier " application.properties", nous aurons de l'espace pour 10 filtres personnalisés devant les filtres de Spring Security.
3.3. Exemple
......
4. Chaîne de filtres par défaut
Lors du lancement d'une application implémentant Spring Security, l'un des premiers logs apparaissant dans la console ressemble à:
2020-02-25 10:24:27.875 INFO 11116 — — [ main] o.s.s.web.DefaultSecurityFilterChain : Création d'une chaîne de filtres : toute requête […]
Ce journal répertorie les filtres par défaut implémentés par Spring Security. Ci-après, une liste ordonnée des filtres constituant la chaîne de filtres par défaut (Spring Security v5.4.2).
WebAsyncManagerIntegrationFilter : ce type est comme le ciment entre le SecurityContext et le WebAsyncManager, permettant de remplir le SecurityContext pour chaque requête.
SecurityContextPersistenceFilter : envoie les informations du SecurityContextRepository au SecurityContextHolder, ce dernier étant nécessaire au processus d'authentification, qui nécessite un SecurityContext valide.
HeaderWriterFilter : ajoute des en-têtes à la requête en cours. Ce filtre est particulièrement utile pour prévenir certaines attaques courantes en ajoutant des en-têtes comme X-Frame-Options, X-XSS-Protection et X-Content-Type-Options (voir la partie sur la protection générale ci-dessous)
CsrfFilter : ajoute une protection contre les attaques Cross-Site Request Forgery, en impliquant un jeton, qui est généralement enregistré sous forme de cookie dans la HttpSession. Il est courant d'appeler ce filtre avant toute requête pouvant changer l'état de l'application (principalement POST, PUT, DELETE et parfois OPTIONS).
LogoutFilter : gère le processus de déconnexion en appelant plusieurs lougoutHandlers chargés d'effacer le contexte de sécurité, d'invalider la session utilisateur, de rediriger vers la page par défaut…
UsernamePasswordAuthenticationFilter : analyse la soumission d'un formulaire d'authentification, qui doit fournir un nom d'utilisateur et un mot de passe. Ce filtre est activé par défaut sur l'URL /login.
DefaultLoginPageGeneratingFilter : construit une page d'authentification par défaut, à moins qu'elle ne soit explicitement inactivée. Ce filtre est la raison pour laquelle une page de connexion apparaît lors de l'implémentation de Spring Security, avant même que le développeur n'en crée une personnalisée.
DefaultLogoutPageGeneratingFilter : construit une page de déconnexion par défaut, à moins qu'elle ne soit explicitement inactivée.
BasicAuthenticationFilter : vérifie si une demande inclut un en-tête d'authentification de base et tente de se connecter avec le nom d'utilisateur et le mot de passe lus dans cet en-tête.
RequestCacheAwareFilter : vérifie dans le cache si la requête en cours est similaire à une ancienne afin d'accélérer son traitement.
SecurityContextHolderAwareRequestFilter : offre de multiples fonctionnalités pour chaque requête, notamment concernant le processus d'authentification : récupérer les informations des utilisateurs, vérifier s'ils sont authentifiés (et sinon, offrir la possibilité de s'authentifier via le gestionnaire d'authentification), obtenir leurs rôles, proposer de se déconnecter via les gestionnaires de déconnexion, maintenant le contexte de sécurité sur plusieurs threads…
AnonymousAuthenticationFilter : fournit un objet d'authentification au détenteur du contexte de sécurité s'il n'y en a pas.
SessionManagementFilter : vérifie si un utilisateur est authentifié depuis le début de la requête, et dans ce cas, procède aux vérifications liées à la session (à titre d'exemple, vérifie si plusieurs connexions simultanées sont actuellement en cours d'utilisation).
ExceptionTranslationFilter : gérez les exceptions AccessDeniedException et AuthenticationException levées par filterChain. Ce filtre est essentiel pour que l'interface graphique reste cohérente lorsque des erreurs se produisent, car il constitue le pont entre les exceptions Java et les réponses http.
FilterSecurityInterceptor : vérifie si les rôles de l'utilisateur correspondent aux exigences d'autorisation pour la demande en cours.
II. Interfaces et classes importantes
1. Authentication
1.1. AuthenticationManager
AuthenticationManager est l'interface principale pour l'authentification pour une application dont la securité est gérée par Spring Security; cette interface peut être considérée comme un coordinateur qui enregistre plusieurs fournisseurs et, en fonction du type de demande elle envoie la requête d'authentification au bon fournisseur; elle possède une seule méthode appelée authenticate et sa signature est :
public interface AuthenticationManager { Authentication authenticate (Authentication authentication) throws AuthenticationException; }
Pour Spring Security, une configuration rapide des détails des utilisateurs en mémoire, pour JDBC , LDAP ou pour l'ajout de UserDetailsService personnalisé, on se sert de AuthenticationManagerBuilder, L'exemple suivant montre une application qui configure l'AuthentificationManager global (parent) :
@Configuration public class ApplicationSecurity extends WebSecurityConfigurerAdapter { ... // c e qui concerne le web par exemple @Autowired public void initialize(AuthenticationManagerBuilder builder, DataSource dataSource) { builder.jdbcAuthentication().dataSource(dataSource).withUser("dave") .password("secret").roles("USER"); } }
1.2. AuthenticationProvider
L'interface AuthenticationProvider dérive de l'interface AuthentificationManager ; L'implémentation courante de AuthenticationProvider est la classe ProviderManager ; AuthenticationProvider a une méthode supplémentaire par rapport à AuthenticationProvider pour permettre à l'appelant de demander s'il prend en charge une "Authentication" type donné car AuthenticationProvider traite des types spécifiques d’authentification; elle possède deux méthodes :
la méthode authenticate effectue l'authentification avec la demande et la méthode supports vérifie si le fournisseur prend en charge le type d'authentification indiqué. La signature de AuthenticationProvider est :
public interface AuthenticationProvider { Authentication authenticate(Authentication authentication) throws AuthenticationException; boolean supports(Class<?> authentication); }
L'argument Class<?> dans la méthode supports() est en réalité Class<? extends Authentication> (on lui demande uniquement s'il prend en charge quelque chose qui est transmis à la méthode Authenticate()). Un objet de type ProviderManager peut prendre en charge plusieurs mécanismes d'authentification différents dans la même application en déléguant à une chaîne de AuthenticationProviders. Si un ProviderManager ne reconnaît pas un type d’instance d’authentification particulier, ce dernier est ignoré.
Un ProviderManager a un parent facultatif, qu'il peut consulter si tous les fournisseurs renvoient null. Si le parent n'est pas disponible, une authentification nulle entraîne une AuthenticationException.
Parfois, une application dispose de groupes logiques de ressources protégées (par exemple, toutes les ressources Web qui correspondent à un modèle de chemin, tel que /api/**), et chaque groupe peut avoir son propre AuthenticationManager dédié. Souvent, chacun d’eux est un ProviderManager et ils partagent un parent. Le parent est alors une sorte de ressource « globale », agissant comme une ressource de secours pour tous les prestataires.
Une implémentation importante de l'interface souvent utilisé dans des projets est DaoAuthenticationProvider , qui récupère les détails de l'utilisateur à partir d'une implémentation de l'interface UserDetailsService.
1.3. Interface UserdetailsService
L'interface UserDetailsService est décrit comme une interface principale qui charge des données spécifiques à l'utilisateur .
Dans la plupart des cas d'utilisation, AuthenticationProvider permet d'extraire les informations d'identité des utilisateurs en fonction des informations d'identification d'une base de données, puis effectue la validation. Ce cas d'utilisation étant si courant, les développeurs Spring ont décidé de l'extraire en tant qu'interface distincte qui expose la méthode unique "loadUserByUsername" qui accepte le nom d'utilisateur comme paramètre et renvoie l'objet d'identité de l'utilisateur.
III. Application avec Spring Security
1. Exemple pour Authentification par JWT
Après avoir discuté des éléments internes du framework Spring Security, configurons-le pour l'authentification "STATELESS" (sans état) avec un jeton JWT .
Pour personnaliser Spring Security pour l'utilisation de JWT, nous avons besoin d'une classe de configuration annotée avec @EnableWebSecurity . De plus, pour simplifier le processus de personnalisation, le framework expose une classe appelée WebSecurityConfigurerAdapter .On étend cette classe et on surcharge ses deux méthodes de manière à :
1) Configurer le gestionnaire d'authentification avec le bon fournisseur
2) Configurer la sécurité Web (URL publiques, URL privées, autorisation, etc.) ; voir ci-dessous
@EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // TODO configure authentication manager } @Override protected void configure(HttpSecurity http) throws Exception { // TODO configure web security } }
Dans une application classique , nous stockons les identités des utilisateurs dans une base de données. Ces identités sont mappées par l'entité User et leurs opérations CRUD sont définies par un référentiel UserRepo Spring Data.
Désormais, lorsque nous acceptons la demande d'authentification, nous devons récupérer l'identité correcte de la base de données à l'aide des informations d'identification fournies, puis la vérifier. Pour cela, nous avons besoin de l'implémentation de l'interface UserDetailsService, qui est définie comme suit :
public interface UserDetailsService { UserDetails loadUserByUsername (String username) throws UsernameNotFoundException ; }
Ici, nous pouvons voir qu'il est nécessaire de renvoyer l'objet qui implémente l'interface UserDetails, et notre entité User l'implémente (pour les détails d'implémentation.
Étant donné qu'il n'expose que le prototype à fonction unique, nous pouvons le traiter comme une interface fonctionnelle et fournir une implémentation sous la forme d'une expression lambda. Ici, l'appel de fonction auth.userDetailsService lancera l'instance DaoAuthenticationProvider à l'aide de notre implémentation de l'interface UserDetailsService et l'enregistrera dans le gestionnaire d'authentification. Avec le fournisseur d'authentification, nous devons configurer un gestionnaire d'authentification avec le schéma de codage de mot de passe correct qui sera utilisé pour la vérification des informations d'identification. Pour cela, nous devons exposer l'implémentation préférée de l'interface PasswordEncoder sous forme de bean. Dans notre exemple de projet, nous utiliserons l’algorithme de hachage de mot de passe bcrypt.
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService userDetailsService; @Autowired private PasswordEncoder passwordEncoder; @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/admin/**").hasRole("ADMIN") .antMatchers("/user/**").hasRole("USER") .anyRequest().authenticated() .and() .formLogin() .and() .logout() .and() .csrf().disable(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService) .passwordEncoder(passwordEncoder); } }
Dans le code ci-dessus, Spring Security est activé à l'aide de l'annotation @EnableWebSecurity. La méthode configure(HttpSecurity http) est utilisée pour configurer le contrôle d'accès, en spécifiant quelles URL nécessitent quels rôles accéder et que toute demande nécessite une authentification. La méthode formLogin() active l'authentification basée sur un formulaire, la méthode logout() active la prise en charge de la déconnexion et la méthode csrf().disable() désactive la protection CSRF. La méthode configure(AuthenticationManagerBuilder auth) est utilisée pour configurer l'authentification, en spécifiant quelle implémentation UserDetailsService utiliser pour obtenir les informations utilisateur et les mots de passe, et quelle implémentation PasswordEncoder utiliser pour la vérification du mot de passe.