Aller au contenu principal

Spring Security et rôles

Notions théoriques

SecurityFilterChain : configurer les règles d'accès

Spring Security 6 configure les règles d'accès via un bean SecurityFilterChain. C'est ici que vous définissez quelles URL nécessitent une authentification et quels rôles sont requis.

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/admin/**").hasRole("ADMIN") // Réservé aux admins
.requestMatchers("/articles/nouveau").authenticated() // Connecté requis
.requestMatchers("/", "/articles", "/inscription", "/connexion").permitAll() // Public
.anyRequest().authenticated() // Tout le reste : connexion requise
)
.formLogin(form -> form
.loginPage("/connexion") // URL du formulaire de connexion personnalisé
.loginProcessingUrl("/connexion") // URL où Spring traite le formulaire (POST)
.defaultSuccessUrl("/articles", true) // Redirection après connexion réussie
.failureUrl("/connexion?erreur") // Redirection après échec
.permitAll()
)
.logout(logout -> logout
.logoutUrl("/deconnexion") // URL pour se déconnecter (POST)
.logoutSuccessUrl("/connexion") // Redirection après déconnexion
.permitAll()
);
return http.build();
}
Comparaison avec Symfony
Symfony security.yamlSpring Security
access_control: [{path: '^/admin', roles: ['ROLE_ADMIN']}].requestMatchers("/admin/**").hasRole("ADMIN")
firewalls: main: form_login: login_path: /connexion.formLogin(form -> form.loginPage("/connexion"))
firewalls: main: logout: path: /deconnexion.logout(logout -> logout.logoutUrl("/deconnexion"))
is_granted('ROLE_ADMIN') dans Twigsec:authorize="hasRole('ADMIN')" dans Thymeleaf

Rôles : ROLE_USER et ROLE_ADMIN

Spring Security préfixe automatiquement les rôles avec ROLE_ dans certains contextes. Quand vous utilisez hasRole("ADMIN"), Spring cherche ROLE_ADMIN. Stockez donc les rôles avec le préfixe dans la base :

utilisateur.setRole("ROLE_USER"); // Utilisateur standard
utilisateur.setRole("ROLE_ADMIN"); // Administrateur

@PreAuthorize sur les méthodes

L'annotation @PreAuthorize permet de sécuriser des méthodes individuelles de service ou de contrôleur. Pour l'activer, ajoutez @EnableMethodSecurity sur la configuration :

@Configuration
@EnableWebSecurity
@EnableMethodSecurity // Active @PreAuthorize, @PostAuthorize, @Secured
public class SecurityConfig {
// ...
}

Utilisation :

@PreAuthorize("hasRole('ADMIN')")
@DeleteMapping("/articles/{id}")
public String supprimer(@PathVariable Long id) {
articleRepository.deleteById(id);
return "redirect:/articles";
}

@PreAuthorize("hasRole('ADMIN') or #article.auteur.email == authentication.name")
@GetMapping("/articles/{id}/editer")
public String editer(@PathVariable Long id) {
// Accessible par l'admin OU par l'auteur de l'article
}

Thymeleaf Security : affichage conditionnel selon le rôle

Ajoutez la dépendance thymeleaf-extras-springsecurity6 dans pom.xml :

<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity6</artifactId>
</dependency>

Puis dans les templates (namespace sec) :

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<body>

<!-- Affiché uniquement si l'utilisateur est connecté -->
<div sec:authorize="isAuthenticated()">
Bonjour, <span sec:authentication="name"></span> !
<a th:href="@{/deconnexion}">Se déconnecter</a>
</div>

<!-- Affiché uniquement si l'utilisateur n'est pas connecté -->
<div sec:authorize="isAnonymous()">
<a th:href="@{/connexion}">Se connecter</a>
<a th:href="@{/inscription}">S'inscrire</a>
</div>

<!-- Réservé aux administrateurs -->
<div sec:authorize="hasRole('ADMIN')">
<a th:href="@{/admin/utilisateurs}">Gérer les utilisateurs</a>
</div>

<!-- Bouton visible uniquement par l'auteur ou un admin -->
<div sec:authorize="hasRole('ADMIN') or #authentication.name == ${article.auteur.email}">
<a th:href="@{/articles/{id}/editer(id=${article.id})}">Modifier</a>
</div>

</body>
</html>

Formulaire de connexion personnalisé

@Controller
public class AuthController {

@GetMapping("/connexion")
public String afficherConnexion(@RequestParam(required = false) String erreur,
Model model) {
if (erreur != null) {
model.addAttribute("erreur", "Email ou mot de passe incorrect.");
}
return "auth/connexion";
}
}

Vue templates/auth/connexion.html :

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head><title>Connexion - MonBlog</title></head>
<body>
<h1>Connexion</h1>
<div th:if="${erreur}" th:text="${erreur}" class="erreur"></div>
<div th:if="${message}" th:text="${message}" class="succes"></div>

<!-- action="/connexion" doit correspondre à loginProcessingUrl -->
<form action="/connexion" method="post">
<!-- Token CSRF généré automatiquement par Thymeleaf avec Spring Security -->
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
<div>
<label for="username">Email</label>
<!-- name="username" est attendu par Spring Security par défaut -->
<input type="email" id="username" name="username" />
</div>
<div>
<label for="password">Mot de passe</label>
<input type="password" id="password" name="password" />
</div>
<button type="submit">Se connecter</button>
</form>
<a th:href="@{/inscription}">Pas encore de compte ? S'inscrire</a>
</body>
</html>
Token CSRF obligatoire

Spring Security active la protection CSRF par défaut. Tous les formulaires POST doivent inclure le token CSRF. Avec Thymeleaf, le tag th:action l'injecte automatiquement. Pour un formulaire action statique (sans th:action), ajoutez manuellement <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />.

Logout : déconnexion

Le logout doit se faire en POST (pas GET) pour éviter que des liens externes ne déconnectent l'utilisateur à son insu :

<form th:action="@{/deconnexion}" method="post">
<button type="submit">Se déconnecter</button>
</form>

Exemple pratique

Configuration complète SecurityConfig.java pour MonBlog :

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {

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

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/css/**", "/js/**", "/images/**").permitAll()
.requestMatchers("/", "/articles", "/articles/{id}").permitAll()
.requestMatchers("/inscription", "/connexion").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/connexion")
.loginProcessingUrl("/connexion")
.defaultSuccessUrl("/articles", true)
.failureUrl("/connexion?erreur")
.permitAll()
)
.logout(logout -> logout
.logoutUrl("/deconnexion")
.logoutSuccessUrl("/connexion")
.permitAll()
);
return http.build();
}
}

Test de mémorisation/compréhension


Quelle méthode dans SecurityFilterChain autorise tout le monde (sans connexion) ?


Que signifie .requestMatchers("/admin/**").hasRole("ADMIN") ?


Quelle annotation active @PreAuthorize sur les méthodes ?


Pourquoi la déconnexion doit-elle se faire en POST et non en GET ?


Quel namespace HTML est requis pour utiliser sec:authorize dans Thymeleaf ?


Quel est le nom du champ HTML attendu par Spring Security pour l'email/login dans un formulaire de connexion ?


TP pour réfléchir et résoudre des problèmes

Dans ce TP, vous allez configurer Spring Security pour protéger les routes du projet MonBlog.

Étape 1 — Configurer SecurityFilterChain

Créez la classe SecurityConfig avec les règles d'accès de MonBlog.


Bonne pratique - Ordre des règles Spring Security

L'ordre des requestMatchers est important. Spring Security évalue les règles dans l'ordre et s'arrête à la première correspondance. Placez toujours les règles les plus spécifiques avant les règles générales. anyRequest().authenticated() doit toujours être en dernier.

Étape 2 — Configurer le formulaire de connexion


Bonne pratique - defaultSuccessUrl avec alwaysUse=true

Avec defaultSuccessUrl("/articles", true), l'utilisateur est toujours redirigé vers /articles après connexion, quelle que soit la page qu'il essayait d'atteindre. Avec false (par défaut), Spring redirige vers la page demandée originalement. Pour un blog, true est plus adapté ; pour une application métier où l'utilisateur doit revenir à sa page de travail, préférez false.

Étape 3 — Affichage conditionnel dans la vue avec sec:authorize


Bonne pratique - sec:authorize masque l'affichage mais ne sécurise pas

sec:authorize cache uniquement l'élément HTML côté affichage. Un utilisateur malin peut toujours taper directement /admin dans l'URL. La vraie sécurité est dans SecurityFilterChain (.requestMatchers("/admin/**").hasRole("ADMIN")) et/ou @PreAuthorize. Utilisez toujours les deux niveaux : protection côté serveur + masquage côté vue.

📌 Une solution