Sortez couvert avec Spring Security

Introduction

Votre maman vous l’a toujours dit, il faut sortir couvert. C’est pourquoi je vous propose cette introduction à SpringSecurity. Cet outil va vous permettre de protéger votre API, exiger certains droits pour certaines API. Par défaut il utilise des sessions HTTP, coté client c’est dans les cookies. Ce que l’on va voir dans cet article, c’est le début de l’utilisation de Spring Security, il y a bien sûr des possibilités bien plus grandes.

Import de Spring security

Comme d’habitude on modifie notre build.gradle. En ajoutant la ligne suivante :

implementation 'org.springframework.boot:spring-boot-starter-security'

On fait clic droit sur le projet Gradle => Refresh Gradle Project.

Configurer Spring Security

Comme tout springboot, la configuration se fait via des classes et des annotations. On va créer un package com.raphlys.config et y mettre le fichier SecurityConfig.java, suivant :

package com.raphlys.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import jakarta.servlet.http.HttpServletResponse;

@Configuration
@EnableWebSecurity
public class SecurityConfig {
	@Autowired
	private CustomUserDetailsService customUserDetailsService;

	@Bean
	SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
			http.csrf(csrf -> csrf.disable())
				.cors(cors -> cors.disable()).formLogin(Customizer.withDefaults())
				.authorizeHttpRequests(authorizeHttpRequests -> authorizeHttpRequests
						.requestMatchers(new AntPathRequestMatcher("/raph/all", "GET")).permitAll()
						.requestMatchers(new AntPathRequestMatcher("/raph/**")).authenticated())
				.userDetailsService(customUserDetailsService);

return http.build();
}
	@Bean
	PasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}
}

On étudie ça ensemble.

@Configuration => On indique à Springboot que la classe va contenir des Beans que le conteneur Spring doit gérer.
@EnableWebSecurity => Permet d’activer les fonctionnalités de sécurité web de SpringSecurity. Et d’indiquer à Spring que l’on va faire nos propres régles.

@Autowired
	private CustomUserDetailsService customUserDetailsService;

Là on va créer une autre classe qui va permettre de gérer l’authentification des utilisateurs. Nous verrons cette classe plus tard, c’est elle qui va nous permettre de savoir si une personne est autorisée ou non. Nous reviendrons sur cette classe ensuite.

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

C’est la méthode qui va nous permettre de définir la configuration générale de SpringSecurity. C’est ici le cœur de votre protection.

http.csrf(csrf -> csrf.disable())

Ça, c’est le code qu’il ne faut pas avoir en production. Il permet de désactiver les protections contre les attaques de type CSRF (Cross Site Request forgery). Des attaques qui pourraient faire exécuter du code à l’utilisateur sans qu’il le veuille. Comme aujourd’hui nous faisons du test et que l’on va utiliser Postman, on désactive cette sécurité.

.cors(cors -> cors.disable())

Cette fonctionnalité vous permettrait de faire en sorte qu’une application tournant sur un autre port, ou dans un autre domaine puisse faire appel à votre backend. Ici nous n’en avons pas besoin, alors on désactive.

.formLogin(Customizer.withDefaults())

Ce code permet d’activer la page d’authentification, vous pourrez remplacer Customizer.withDefaults(), par du code à vous afin de spécifier ce que vous souhaitez. Donc par défaut grâce à ce code Springsecurity va nous générer une page de connexion à l’adresse http://localhost:8080/login et la possibilité de nous déconnecter à l’adresse http://localhost:8080/logout .

.authorizeHttpRequests(authorizeHttpRequests -> authorizeHttpRequests
						.requestMatchers(new AntPathRequestMatcher("/raph/all", "GET")).permitAll()
						.requestMatchers(new AntPathRequestMatcher("/raph/**")).authenticated())

Ce code-là va vous permettre d’indiquer les droits sur vos APIs. Il est lu par Springboot de haut en bas, donc si votre url match avec raph/all, il regardera le droit associé, en l’occurrence, ici permitAll et n’ira pas voir la règle suivante. Par défaut, rien n’est protégé, il faut donc expliciter toutes les règles.
Donc ici on autorise raph/all pour tout le monde , pour les requêtes GET. Pour toutes les autres requêtes qui commenceraient par /raph il faut être authentifié.

.userDetailsService(customUserDetailsService);

On indique le service qui va s’occuper de valider si un utilisateur peut être connecté ou pas.

return http.build();

On construit la configuration, pour la donner à Springboot.

@Bean
PasswordEncoder passwordEncoder() {
	return new BCryptPasswordEncoder();
}

On donne un bean à Springboot qui va crypter les mots de passe, ici BCryptPasswordEncoder. C’est essentiel d’en avoir un pour protéger les mots de passe.

Le Service de validation de l’authentification des utilisateurs.

C’est la classe dont on parlait tout à l’heure CustomUserDetailsService.java, voici son code

package com.raphlys.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;


@Service
public class CustomUserDetailsService implements UserDetailsService {

	@Autowired
	@Lazy
	private PasswordEncoder passwordEncoder;
	
	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		return User.builder().username(username).password(passwordEncoder.encode("password")).build();
	}
}

On regarde chaque ligne ensemble, je vous épargne juste l’@Service qui permet d’indiquer qu’on affaire à un service et le implements UserDetailsService qui indique que l’on implémente cette interface qui contient une seule méthode « loadUserByUsername(String username) « .

@Autowired
@Lazy
private PasswordEncoder passwordEncoder;

Faut vraiment vous expliquer ça? Bon @Autowired on l’a déjà vu ça permet l’injection de dependance. @Lazy, c’est parce que SecurityConfig a besoin du service que l’on est en train de définir et que le service qu’on est en train de définir à besoin d’un bean défini par SecurityConfig qui a besoin du service que l’on est en train de définir et que le service qu’on est en train de définir à besoin d’un bean défini par SecurityConfig qui a besoin du service que l’on est en train de définir et que le service qu’on est en train de définir à besoin d’un bean défini par SecurityConfig qui …. Grâce à cette annotation ont dit à Springboot qui pourra injecter PasswordEncoder aprés la construction de notre Service.
Et ce PasswordEncoder c’est le Brean que l’on a défini dans SecurityConfig et qui permet d’encoder les mots de passe (Password = mot de passe && Encoder = encodeur )

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
	return User.builder().username(username).password(passwordEncoder.encode("password")).build();
}

Cette méthode fonctionne de la façon suivante, Springboot vous donne un nom d’utilisateur (user = utilisateur && name = nom 😉) en contre partie vous devez lui retourner un objet UserDetails. User.builder va vous permettre de le contruire. Ici on récupère le nom et on le met dans username et on encode le mot de passe « password » pour que Springboot puisse le comparer avec le mot de passe envoyé pour s’authentifié. Donc si vous voulez complexifier un peu la chose, vous pouvez mettre une BDD derrière qui contiendrait tout les utilisateur avec le mot de passe encrypté. Ou une hashmap qui aurait comme clé le username et comme valeur le password encrypté.
J’essaie de donner le code le plus simple possible afin que vous puissiez le skinner à votre convennance. Nous verrons plus tard, comment mettre une BDD derrière. Mais si vous comprennez ça le reste est facile.

Testons notre sécurité

  1. Lancer l’application
  2. Ouvrir votre navigateur sur http://localhost:8080/login
  3. Mettez les informations de connection suivantes :
    • UserName: Topper.Harley
    • Password: password
  4. Faut il vous dire, qu’il faut cliquer sur le bouton « Sign In »?
  5. F12 (Sur Firefox, chromium…)
  6. Onglet « Application »
  7. 🍪 Cookie => « http://localhost:8080 »
  8. Copier la value de JSESSIONID
  1. Ouvrir postman
  2. Sous le bouton « Send » cliquez sur « Cookie »
  3. Ajouter un cookie au domain localhost avec les informations suivantes : JSESSIONID={{🍪}}; Path=/; HttpOnly;
    Où 🍪 représente la valeur copiée
  4. Maintenant vous pouvez utiliser votre API avec Postman

Vous pouvez vous déconnecter en allant sur http://localhost:8080/logout ni postman ni votre navigateur ne pourront accéder à l’API, car springboot utilise une session HTTP et non un Bearer par défaut.

La prochaine fois nous verrons comment mettre un front end Angular sur notre backend Springboot, en utilisant le moins de code possible et en gardant le mode de session HTTP. Petit à petit, on verra sécuriser notre application.

Rendre vos super services incroyablement génériques

Protéger incroyablement votre application Angular/Springboot

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur la façon dont les données de vos commentaires sont traitées.