Archives Wild Code School
Spring

Spring Security

Quête 15 sur 16

Aujourd'hui, tu vas découvrir un sous-projet de Spring : Spring Security ! Ce framework va te permettre d'intégrer des mécanismes d'authentification et d'autorisation dans tes projets.

Objectifs

  • S'initier au fonctionnement de Spring Security
  • Configurer le framework
  • Gérer les utilisateurs et leurs autorisations

Spring Initializr

Ce n'est pas ton premier rodéo, tu sais comment tout ça va commencer : rends-toi sur https://start.spring.io/

Crée un nouveau projet avec les informations suivantes :

  • "Group" : com.wildcodeschool

  • "Artefact" : myProjectWithSecurity

  • "Dependencies" : Web et Security

Télécharge le zip généré, extrais-le et importe-le dans ton IDE.

Afin d'avoir au moins une route testable, tu peux mettre en place un point d'entrée avec le contrôleur suivant dans le package com.wildcodeschool.myProjectWithSecurity.controllers :

java
package com.wildcodeschool.myProjectWithSecurity.controllers;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloWorldController {

    @GetMapping("/")
    public String hello() {
        return "Hello World!!!";
    }
}

Lance maintenant ton projet avec la commande ./mvnw spring-boot:run, et ouvre l'url suivante dans ton navigateur :

http://localhost:8080/

Là, ce n'est pas comme d'habitude... Au lieu de voir le message "Hello World!!!", tu devrais voir maintenant un formulaire te demandant un nom d'utilisateur et un mot de passe. Tu noteras en passant que tu as été redirigé vers cette url :

http://localhost:8080/login

Fichtre, la porte est verrouillée... Si tu regardes attentivement le flot de texte dans ta console, tu devrais trouver une ligne qui ressemble à celle-ci :

shell
Using generated security password: #######-####-####-####-############

Essaie de rentrer ce mot de passe généré avec le nom d'utilisateur user dans le formulaire de connexion : cela t'ouvre la porte et te ramène sur la page d'accueil. Par défaut, Spring Security te crée donc un utilisateur user avec un mot de passe généré au lancement de l'application.

Ouvre maintenant l'url suivante pour te déconnecter :

http://localhost:8080/logout

C'est bien gentil tout ça, mais quand je veux développer une API, je ne peux pas passer par un formulaire de connexion...

C'est juste, mais comme la vie est bien faite, tu peux intégrer les identifiants directement dans une requête : tu vas le vérifier avec Postman. D'abord, lance une requête GET sur http://localhost:8080/ : tu auras comme réponse une erreur 401 -- accès non autorisé.

Va maintenant dans l'onglet "Authorization", choisis comme type "Basic Auth" et renseigne les champs "Username" et "Password".

Sauf que, pour l'instant, tu as un nom d'utilisateur et un mot de passe imposés par Spring : tu vas devoir bidouiller un peu la configuration pour aller plus loin.

↗
Spring Security
spring.io

Configurer Spring Security

Pour configurer Spring Security selon tes envies, tu as besoin d'un objet de configuration qui active la sécurité. Bref, une classe avec l'annotation @Configuration et @EnableWebSecurity: cet objet va décrire la configuration pour la sécurité de ton projet.

java
package com.wildcodeschool.myProjectWithSecurity.config;


import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableWebSecurity
public class MySecurityConfig {

}

Pour configurer la chaine de sécurité, il faut créer un @Bean renvoyant une SecurityFilterChain. Commençons avec celle qui prend en paramètre une instance de type HttpSecurity. En fouillant dans la documentation officielle -- regarde dans les ressources, tu trouveras son implémentation par défaut :

java
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
	http.authorizeHttpRequests((authz) -> {
		try {
			authz
				.anyRequest().authenticated()
				.and().formLogin()
				.and().httpBasic();
		} catch (Exception e) {

			e.printStackTrace();
		}
	});
	return http.build();
}

L'instance http te permet d'ajouter des blocs de paramètres à ta configuration, chaque bloc suivant étant ajouté avec la méthode and(). L'implémentation par défaut est donc à lire comme ceci :

  • Sur la configuration pour les requêtes http, j'ajoute :

un bloc authorizeRequests() pour activer la sécurité ; je précise que :pour anyRequest() -- n'importe quelle requête,je veux que l'utilisateur soit authenticated() -- authentifié,

  • and() -- un autre bloc :

pour préciser que l'authentification passe par un formLogin() -- un formulaire de connexion,

  • and() -- un autre bloc :

pour configurer httpBasic() -- l'authentification basique HTTP.

Un test que tu peux faire pour rendre cela plus concret est d'enlever tout le bloc authorizeRequests :

java
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
	http
			.formLogin()
			.and().httpBasic();
	return http.build();
}

Relance ton application, et ouvre d'abord l'url :

http://localhost:8080/logout

Maintenant que tu es sûr d'être déconnecté, ouvre l'url :

http://localhost:8080/

Tu peux y accéder sans connexion parce que tu as enlevé un bloc de la configuration par défaut . C'était le bloc qui précisait que l'accès sur chaque requête devait être authentifié :

java
authorizeRequests().anyRequest().authenticated()

Tu peux faire des tests en enlevant/rajoutant des blocs, et chercher d'autres options. Rétablis la configuration par défaut pour la suite :

java
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
	http.authorizeHttpRequests((authz) -> {
		try {
			authz
				.anyRequest().authenticated()
				.and().formLogin()
				.and().httpBasic();
		} catch (Exception e) {

			e.printStackTrace();
		}
	});
	return http.build();
}

Gérer les utilisateurs

Pour l'instant, tu es bloqué avec ton utilisateur user et son mot de passe généré. Il est temps de redéfinir un autre Bean InMemoryUserDetailsManager -- mais attends la suite avant de la tester :

java
@Bean
public InMemoryUserDetailsManager userDetailsService() {
	UserDetails user = User
			.withUsername("user")
			.password("password")
			.roles("")
			.build();

	return new InMemoryUserDetailsManager(user);
}

Cette déclaration permet de déclarer des utilisateurs "en mémoire" -- c'est-à-dire sans base de données. Avec ce code, tu es en train d'associer l'identifiant user au mot de passe password. Ce n'est pas un mot de passe très recommandé, mais ça nous ira bien pour tester. Tu peux relancer ton application, et aller au formulaire de login... mais password ne marchera pas !

Avec Spring 4, il était possible de stocker des mots de passe en clair. Avec Spring 5, les choses sont un peu plus subtiles : un décodeur par défaut est utilisé. Avant de tester, complète la méthode en utilisant un encodeur par défaut :

java
PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();

UserDetails user = User
		.withUsername("user")
		.password(encoder.encode("password"))
		.roles("USER")
		.build();

Note que tu es obligé d'associer des rôles à ton utilisateur : pour l'instant, une chaîne de caractères vide fera l'affaire.

Relance, va à la page de login, et connecte-toi :)

↗
Default Password Encoder in Spring Security 5
baeldung.com
↗
HttpSecurity
docs.spring.io

Gérer les autorisations

Que dirais-tu de démarrer un petit back office ? Rajoute une route à ton contrôleur :

java
@GetMapping("/admin")
public String admin() {
    return "Hello Admin!!!";
}

Le back office ne devrait être accessible qu'à un administrateur. Ajoute-le dans les utilisateurs déclarés :

java
@Bean
public InMemoryUserDetailsManager userDetailsService() {
	PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();

	UserDetails user = User
			.withUsername("user")
			.password(encoder.encode("password"))
			.roles("")
			.build();

	UserDetails admin = User
			.withUsername("admin")
			.password(encoder.encode("12345678"))
			.roles("ADMIN")
			.build();

	return new InMemoryUserDetailsManager(List.of(user, admin));
}

Reste maintenant à adapter la gestion des requêtes HTTP -- ton SecurityFilterChain filterChain(HttpSecurity http). Je pourrais t'expliquer comment faire, mais, puisque tu es arrivé aussi loin, j'ai mieux à te proposer. Tu trouveras ci-dessous une ressource qui te permettra de finir seul cette partie. Vois cela comme un test avant de te recruter pour une mission un peu "sensible". Je t'en dirai plus sur cette mission dans la partie challenge.

↗
Authorize Requests
docs.spring.io

Challenge

Stratégie, Habileté, Intervention, Exécution et Logistique Défensive

Bravo, je savais que tu étais la personne de la situation. Je peux maintenant te parler de ta mission. Le SHIELD a découvert des accès mal sécurisés sur sa plateforme web. Le directeur veut refondre tout le projet, et m'a demandé de confier la réalisation d'un POC à quelqu'un de confiance. Tu vas créer des routes selon les critères suivants :

  • / : la route est accessible par tout le monde, sans authentification, et retourne le message "Welcome to the SHIELD".

  • /avengers/assemble : la route est accessible uniquement aux champions du SHIELD, et retourne le message "Avengers... Assemble".

  • /secret-bases : cette page n'est accessible que par le directeur du SHIELD, et retourne la liste de toutes les villes où il y a une Wild Code School (regarde cette page pour t'aider).

Ton projet doit créer des identifiants pour les utilisateurs suivants :

  • Nom : Steve | Mot de passe : motdepasse | Rôle : CHAMPION

  • Nom : Nick | Mot de passe : flerken | Rôle : DIRECTOR

Critères de validation

  • Le projet implémente les routes demandées.
  • Les identifiants permettent d'accéder aux routes sécurisées.
  • Le code est disponible sur GitHub.

Ressources partagées sur cette quête