Sécurisation des API REST avec Spring Security
Introduction : L'Impératif de la Sécurité des API REST
Bienvenue dans ce module avancé de notre cours "Développement Backend Robuste avec Java et Spring Boot". Aujourd'hui, nous abordons un sujet critique et incontournable : la sécurisation des API REST. Dans le monde interconnecté actuel, nos API sont les portes d'entrée vers nos données et nos services. Les laisser non sécurisées serait une faute professionnelle majeure, exposant nos applications à des risques de vol de données, de manipulations non autorisées, et d'atteinte à la réputation.
Les API REST, par leur nature sans état (stateless) et leur exposition potentielle au monde extérieur, présentent des défis de sécurité spécifiques. C'est là que Spring Security intervient. Cadre de sécurité robuste et hautement configurable pour les applications Spring, il fournit une solution complète pour gérer l'authentification et l'autorisation, et bien plus encore.
Dans cette leçon, nous allons explorer :
- Les concepts fondamentaux de la sécurité des API REST.
- Comment intégrer et configurer Spring Security pour nos API.
- Les mécanismes d'authentification sans état, notamment avec les Jetons Web JSON (JWT).
- La mise en œuvre d'une autorisation granulaire pour contrôler l'accès aux ressources.
- Un bref aperçu d'OAuth2 et OpenID Connect dans le contexte des API.
Préparez-vous à renforcer considérablement la robustesse de vos applications !
1. Comprendre la Sécurité des API REST
Avant de plonger dans Spring Security, il est essentiel de comprendre les spécificités des API REST en matière de sécurité.
1.1 La Nature Sans État (Stateless) des API REST
Une caractéristique fondamentale des API REST est leur nature sans état. Cela signifie que chaque requête d'un client au serveur doit contenir toutes les informations nécessaires pour comprendre et traiter la requête. Le serveur ne conserve aucune information de session sur le client entre les requêtes.
- Avantages : Améliore la scalabilité (facilite la distribution des requêtes entre plusieurs serveurs), la robustesse (un serveur peut tomber sans perdre l'état du client), et la simplicité (moins de complexité côté serveur pour la gestion des sessions).
- Défis de sécurité : Sans état de session, les mécanismes d'authentification traditionnels basés sur des cookies de session ne sont pas idéaux. Chaque requête doit être authentifiée ou, du moins, validée.
1.2 Authentification vs. Autorisation : Les Fondamentaux
Ce sont les deux piliers de la sécurité :
- Authentification (Authentication) : Il s'agit du processus de vérification de l'identité d'un utilisateur ou d'un service. C'est la réponse à la question "Qui êtes-vous ?". Cela implique généralement la présentation de preuves d'identité (comme un nom d'utilisateur et un mot de passe, ou un jeton).
- Autorisation (Authorization) : Une fois l'identité établie, l'autorisation détermine ce que cet utilisateur ou service est autorisé à faire. C'est la réponse à la question "Que pouvez-vous faire ?". Cela implique la vérification des permissions ou des rôles attribués à l'entité authentifiée.
1.3 Menaces Courantes contre les API REST
Connaître les menaces est la première étape pour les prévenir. Parmi les menaces courantes, on trouve :
- Injection de code : SQL Injection, NoSQL Injection, etc.
- Authentification brisée : Faiblesse dans la gestion des identifiants, sessions non sécurisées.
- Exposition excessive des données : Renvoyer trop de données non nécessaires.
- Manque de limites de ressources/taux : Permet des attaques par déni de service (DoS).
- Mauvaise configuration de sécurité : Paramètres par défaut non modifiés, erreurs de configuration.
- Absence de chiffrement de transport : Non-utilisation de HTTPS.
2. Introduction à Spring Security
Spring Security est le framework de facto pour la sécurité des applications Spring. Il est extrêmement flexible et puissant, conçu pour s'adapter à une multitude de scénarios de sécurité.
2.1 Qu'est-ce que Spring Security ?
Spring Security est un framework déclaratif qui fournit des services d'authentification et d'autorisation pour les applications Spring. Il s'intègre profondément avec le conteneur Spring IoC, permettant une configuration aisée via des classes Java de configuration ou des fichiers XML (bien que les classes Java soient préférées aujourd'hui).
- Principales fonctionnalités :
- Authentification (formulaire, HTTP Basic, OAuth2, OpenID Connect, JWT, LDAP, etc.)
- Autorisation (contrôle d'accès basé sur les rôles, les permissions, les expressions)
- Protection contre les attaques courantes (CSRF, XSS, Fixation de session, etc.)
- Intégration transparente avec Spring MVC et Spring Boot.
2.2 Composants Clés de Spring Security
Spring Security fonctionne grâce à une chaîne de filtres (servlets Filters) qui interceptent les requêtes. Voici quelques composants essentiels :
FilterChainProxy: Le point d'entrée principal de Spring Security. Il délègue la requête à la chaîne de filtres de sécurité appropriée.SecurityContextHolder: Une classe utilitaire qui stocke les détails de l'utilisateur authentifié (leAuthenticationobject) dans unThreadLocal. Cela signifie que l'objetAuthenticationest accessible depuis n'importe où dans le thread de la requête.AuthenticationManager: L'interface principale pour effectuer l'authentification. Il reçoit unAuthentication(généralement non authentifié) et, s'il réussit, renvoie unAuthenticationentièrement authentifié.UserDetailsService: Interface utilisée par l'authentification pour récupérer les détails de l'utilisateur (nom d'utilisateur, mot de passe encodé, rôles/autorités) à partir d'une source de données (base de données, LDAP, etc.).PasswordEncoder: Interface pour l'encodage et la vérification des mots de passe. Ne jamais stocker les mots de passe en clair !AccessDecisionManager: Décide si l'utilisateur authentifié est autorisé à accéder à une ressource ou à exécuter une méthode, basé sur les rôles/permissions.
3. Authentification Basique (Stateless) avec Spring Security
Pour les API REST, nous privilégions des mécanismes d'authentification sans état.
3.1 Dépendance Maven/Gradle
Tout d'abord, ajoutez la dépendance Spring Security à votre projet Spring Boot.
<!-- Maven -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
// Gradle
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
3.2 Configuration de base d'une API REST avec Spring Security
La configuration principale de Spring Security se fait via une classe annotée @Configuration et @EnableWebSecurity. Cette classe étend généralement WebSecurityConfigurerAdapter (déprécié à partir de Spring Security 5.7, on préférera les SecurityFilterChain et WebSecurityCustomizer Beans) ou fournit des Beans SecurityFilterChain directement.
Voici une configuration moderne pour une API REST :
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity // Active la sécurité web
@EnableMethodSecurity // Active la sécurité au niveau des méthodes (@PreAuthorize, etc.)
public class SecurityConfig {
// 1. Définir le PasswordEncoder
@Bean
public PasswordEncoder passwordEncoder() {
// Utilisation de BCrypt pour hacher les mots de passe.
// C'est le standard recommandé pour sa robustesse.
return new BCryptPasswordEncoder();
}
// 2. Configurer les utilisateurs (pour l'exemple, en mémoire)
@Bean
public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
// Crée des utilisateurs en mémoire pour la démonstration.
// En production, vous implémenteriez une logique pour charger les utilisateurs
// depuis une base de données (UserDetailsService personnalisé).
UserDetails user = User.builder()
.username("user")
.password(passwordEncoder.encode("password"))
.roles("USER") // Assignation du rôle USER
.build();
UserDetails admin = User.builder()
.username("admin")
.password(passwordEncoder.encode("adminpass"))
.roles("ADMIN", "USER") // Assignation des rôles ADMIN et USER
.build();
return new InMemoryUserDetailsManager(user, admin);
}
// 3. Configurer la chaîne de filtres de sécurité HTTP
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// Désactive la protection CSRF pour les API REST.
// CSRF est principalement pour les applications web avec des sessions basées sur des cookies.
.csrf(csrf -> csrf.disable())
// Gère la politique de création de session : STATELESS est crucial pour les API REST.
// Cela indique à Spring Security de ne pas créer ni utiliser de sessions HTTP.
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// Configure l'authentification HTTP Basic.
// Le navigateur demandera des identifiants (pop-up) si non fournis.
.httpBasic(httpBasic -> {})
// Autorisation des requêtes HTTP :
.authorizeHttpRequests(authorize -> authorize
// Permet l'accès à /public/** sans authentification
.requestMatchers("/public/**").permitAll()
// Nécessite le rôle ADMIN pour accéder à /admin/**
.requestMatchers("/admin/**").hasRole("ADMIN")
// Toutes les autres requêtes nécessitent une authentification
.anyRequest().authenticated()
);
return http.build();
}
}
Explication du code :
@EnableWebSecurity: Active la configuration de la sécurité web par Spring.@EnableMethodSecurity: Permet l'utilisation des annotations de sécurité au niveau des méthodes (comme@PreAuthorize).passwordEncoder(): Définit unBCryptPasswordEncoder, indispensable pour hacher les mots de passe de manière sécurisée.userDetailsService(): Dans cet exemple, nous configurons unInMemoryUserDetailsManageravec deux utilisateurs (useretadmin). En production, vous implémenteriez votre propreUserDetailsServicepour charger les utilisateurs depuis une base de données.securityFilterChain(HttpSecurity http): C'est la méthode clé pour configurer la sécurité HTTP.csrf(csrf -> csrf.disable()): Désactive la protection Cross-Site Request Forgery. Pour les API REST sans état gérant l'authentification via jetons (comme JWT), le CSRF n'est généralement pas nécessaire car il repose sur des sessions côté serveur et des cookies.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)): Très important pour les API REST ! Indique à Spring Security de ne pas créer ni utiliser de sessions HTTP. Chaque requête doit être authentifiée.httpBasic(httpBasic -> {}): Active l'authentification HTTP Basic. Pour tester, vous pouvez utiliser un client HTTP (Postman, curl) et inclure un en-têteAuthorization: Basic [base64(username:password)].authorizeHttpRequests(...): Configure les règles d'autorisation basées sur les chemins d'URL..requestMatchers("/public/**").permitAll(): Rend le chemin/publicet ses sous-chemins accessibles à tous..requestMatchers("/admin/**").hasRole("ADMIN"): N'autorise l'accès à/adminqu'aux utilisateurs ayant le rôleADMIN..anyRequest().authenticated(): Exige une authentification pour toutes les autres requêtes.
Pour tester cette configuration, créez un simple contrôleur :
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class MyRestController {
@GetMapping("/public/hello")
public String publicHello() {
return "Bonjour du monde public !";
}
@GetMapping("/secured/hello")
public String securedHello() {
return "Bonjour du monde sécurisé !";
}
@GetMapping("/admin/hello")
public String adminHello() {
return "Bonjour du monde admin !";
}
}
GET /public/hello: Accessible sans authentification.GET /secured/hello: Nécessite une authentification (e.g.,user:passwordouadmin:adminpass).GET /admin/hello: Nécessite une authentification et le rôleADMIN(e.g.,admin:adminpass).
4. Authentification par Jetons (JWT) avec Spring Security
L'authentification HTTP Basic est simple mais envoie les identifiants à chaque requête. Pour une meilleure gestion de l'authentification sans état, les Jetons Web JSON (JWT) sont la solution privilégiée.
4.1 Pourquoi JWT pour les API REST ?
- Sans état : Les jetons contiennent toutes les informations nécessaires à la validation, éliminant le besoin de sessions côté serveur.
- Scalabilité : Facilite la montée en charge horizontale, car aucun état de session n'est à partager entre les instances du serveur.
- Sécurité : Les jetons sont signés numériquement, garantissant leur intégrité et leur authenticité.
- Flexibilité : Peuvent inclure des données (claims) personnalisées.
4.2 Flux JWT simplifié
- Connexion : Un client envoie ses identifiants (username/password) à un point de terminaison de connexion (par exemple,
/api/auth/login). - Génération du JWT : Le serveur authentifie les identifiants et, s'ils sont valides, génère un JWT contenant les informations de l'utilisateur (ID, rôles) et une date d'expiration.
- Renvoi du JWT : Le serveur renvoie le JWT au client (généralement dans le corps de la réponse ou un en-tête
Authorization). - Requêtes ultérieures : Le client stocke le JWT (par exemple, dans le localStorage) et l'inclut dans l'en-tête
Authorization: Bearer <token>de toutes les requêtes suivantes. - Validation du JWT : Pour chaque requête, le serveur intercepte l'en-tête
Authorization, valide le JWT (signature, expiration, etc.) et extrait les informations de l'utilisateur pour l'authentification et l'autorisation.
4.3 Implémentation Spring Security + JWT
L'intégration de JWT dans Spring Security implique l'ajout d'un filtre personnalisé à la chaîne de filtres de Spring Security.
4.3.1 Dépendance pour JWT
Nous utiliserons la bibliothèque JJWT (Java JWT).
<!-- Maven -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
4.3.2 Composants JWT
JwtUtil(ouJwtService) : Pour générer et valider les JWT.JwtAuthenticationFilter: Un filtre qui intercepte les requêtes, extrait le JWT, le valide et configure leSecurityContextHolder.
Exemple de JwtUtil :
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
@Component
public class JwtUtil {
@Value("${jwt.secret:defaultSecretKeyForDevelopmentPurposesOnlyShouldBeAtLeast256Bit}")
private String secret;
@Value("${jwt.expiration:3600000}") // 1 heure en millisecondes
private long expiration;
private Key key;
@PostConstruct
public void init() {
this.key = Keys.hmacShaKeyFor(secret.getBytes());
}
// Extrait le nom d'utilisateur du jeton
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
// Extrait la date d'expiration du jeton
public Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
// Extrait une 'claim' spécifique du jeton
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
// Extrait toutes les 'claims' du jeton
private Claims extractAllClaims(String token) {
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
}
// Vérifie si le jeton est expiré
private Boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
// Génère un jeton pour un UserDetails
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
// Vous pouvez ajouter des informations supplémentaires (rôles, etc.) aux claims si nécessaire
claims.put("roles", userDetails.getAuthorities().stream().map(Object::toString).toList());
return createToken(claims, userDetails.getUsername());
}
// Crée le jeton JWT
private String createToken(Map<String, Object> claims, String subject) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(key, SignatureAlgorithm.HS256)
.compact();
}
// Valide le jeton
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
}
Explication du code JwtUtil :
@Value: Charge la clé secrète (jwt.secret) et la durée d'expiration (jwt.expiration) depuisapplication.propertiesouapplication.yml. La clé secrète doit être forte et gardée secrète !init(): Initialise la clé de signature HMAC SHA-256 à partir de la chaîne secrète.extractUsername,extractExpiration,extractClaim,extractAllClaims: Méthodes pour parser le jeton et en extraire les informations.isTokenExpired: Vérifie si le jeton est encore valide en termes de temps.generateToken: Crée un nouveau JWT en utilisant les informations de l'UserDetails(principalement le nom d'utilisateur et les rôles viaclaims).validateToken: Vérifie si le nom d'utilisateur extrait du jeton correspond à celui de l'UserDetailset si le jeton n'est pas expiré.
Exemple de JwtAuthenticationFilter :
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final UserDetailsService userDetailsService;
public JwtAuthenticationFilter(JwtUtil jwtUtil, UserDetailsService userDetailsService) {
this.jwtUtil = jwtUtil;
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
final String authorizationHeader = request.getHeader("Authorization");
String username = null;
String jwt = null;
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
jwt = authorizationHeader.substring(7); // Extraire le jeton après "Bearer "
username = jwtUtil.extractUsername(jwt);
}
// Si un nom d'utilisateur est trouvé et qu'il n'y a pas déjà d'authentification dans le contexte de sécurité
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
// Valider le jeton
if (jwtUtil.validateToken(jwt, userDetails)) {
// Si le jeton est valide, créer un objet d'authentification
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
// Définir les détails de la requête
usernamePasswordAuthenticationToken
.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// Placer l'objet d'authentification dans le SecurityContext
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
}
filterChain.doFilter(request, response); // Passer la requête au filtre suivant dans la chaîne
}
}
Explication du code JwtAuthenticationFilter :
OncePerRequestFilter: Garantit que ce filtre est exécuté une seule fois par requête HTTP.doFilterInternal: La logique principale du filtre.- Il extrait l'en-tête
Authorization. - Si l'en-tête existe et commence par "Bearer ", il extrait le JWT et le nom d'utilisateur.
- Il charge les
UserDetailsà partir duUserDetailsServiceen utilisant le nom d'utilisateur extrait. - Il valide le jeton en utilisant
jwtUtil.validateToken(). - Si le jeton est valide, il crée un objet
UsernamePasswordAuthenticationTokenet le définit dans leSecurityContextHolder. C'est ainsi que Spring Security sait que l'utilisateur est authentifié pour la requête courante. filterChain.doFilter(): Permet à la requête de continuer sa route dans la chaîne de filtres.
- Il extrait l'en-tête
4.3.3 Configuration Spring Security pour JWT
Maintenant, nous devons intégrer le JwtAuthenticationFilter dans notre SecurityConfig. Nous aurons également besoin d'un point de terminaison pour l'authentification (login) qui générera le JWT.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; // Import du filtre par défaut
import com.example.demo.security.JwtAuthenticationFilter; // Votre filtre JWT
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final UserDetailsService userDetailsService; // Injectez votre UserDetailsService
public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter, UserDetailsService userDetailsService) {
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
this.userDetailsService = userDetailsService;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// Ceci est votre UserDetailsService, en production il chargerait les utilisateurs depuis une DB
@Bean
public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
UserDetails user = User.builder()
.username("user")
.password(passwordEncoder.encode("password"))
.roles("USER")
.build();
UserDetails admin = User.builder()
.username("admin")
.password(passwordEncoder.encode("adminpass"))
.roles("ADMIN", "USER")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
// Permet d'obtenir l'AuthenticationManager pour l'authentification des identifiants (login)
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
// Configure le DaoAuthenticationProvider pour qu'il utilise votre UserDetailsService et PasswordEncoder
@Bean
public DaoAuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(passwordEncoder());
return provider;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // Toujours désactiver CSRF pour les API stateless avec JWT
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // Pas de session
// Autorisation des requêtes
.authorizeHttpRequests(authorize -> authorize
// Permet l'accès public (ex: endpoint de connexion)
.requestMatchers("/api/auth/**", "/public/**").permitAll()
// Nécessite le rôle ADMIN pour accéder à /admin/**
.requestMatchers("/admin/**").hasRole("ADMIN")
// Toutes les autres requêtes nécessitent une authentification
.anyRequest().authenticated()
)
// Ajoute votre filtre JWT avant le filtre d'authentification par nom d'utilisateur et mot de passe de Spring Security
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
Changements et explications :
- Injection de
JwtAuthenticationFilter: Le filtre que nous avons créé est injecté dans la configuration. authenticationManager()etdaoAuthenticationProvider(): Ces beans sont nécessaires pour permettre l'authentification du nom d'utilisateur/mot de passe au moment de la connexion (pour générer le JWT). LeAuthenticationManagersera utilisé dans votre contrôleur de connexion..requestMatchers("/api/auth/**").permitAll(): Il est crucial de rendre public le point de terminaison de connexion (/api/auth/login) afin que les utilisateurs puissent s'authentifier et obtenir un jeton..addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class): Ceci insère notreJwtAuthenticationFilterdans la chaîne de filtres de Spring Security, juste avant leUsernamePasswordAuthenticationFilter(qui est le filtre par défaut pour l'authentification par formulaire/HTTP Basic). Ainsi, notre filtre intercepte le JWT avant que Spring Security n'essaie d'autres mécanismes d'authentification.
4.3.4 Contrôleur d'Authentification (Login Endpoint)
Enfin, nous avons besoin d'un contrôleur pour gérer la connexion des utilisateurs et la génération du JWT.
import com.example.demo.security.JwtUtil;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
// Objet DTO pour les requêtes de connexion
class AuthenticationRequest {
private String username;
private String password;
// Getters et Setters
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
}
// Objet DTO pour la réponse de connexion
class AuthenticationResponse {
private final String jwt;
public AuthenticationResponse(String jwt) { this.jwt = jwt; }
public String getJwt() { return jwt; }
}
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final AuthenticationManager authenticationManager;
private final UserDetailsService userDetailsService;
private final JwtUtil jwtUtil;
public AuthController(AuthenticationManager authenticationManager,
UserDetailsService userDetailsService,
JwtUtil jwtUtil) {
this.authenticationManager = authenticationManager;
this.userDetailsService = userDetailsService;
this.jwtUtil = jwtUtil;
}
@PostMapping("/login")
public ResponseEntity<?> createAuthenticationToken(@RequestBody AuthenticationRequest authenticationRequest) throws Exception {
try {
// Tente d'authentifier l'utilisateur avec les identifiants fournis
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(authenticationRequest.getUsername(), authenticationRequest.getPassword())
);
} catch (Exception e) {
// En cas d'échec d'authentification (mauvais identifiants)
throw new Exception("Incorrect username or password", e);
}
// Si l'authentification réussit, charge les détails de l'utilisateur
final UserDetails userDetails = userDetailsService.loadUserByUsername(authenticationRequest.getUsername());
// Génère le JWT
final String jwt = jwtUtil.generateToken(userDetails);
// Renvoie le JWT dans la réponse
return ResponseEntity.ok(new AuthenticationResponse(jwt));
}
}
Ce contrôleur reçoit un nom d'utilisateur et un mot de passe, utilise l'AuthenticationManager pour les valider, puis si l'authentification réussit, génère un JWT et le renvoie au client. Le client pourra ensuite utiliser ce JWT pour accéder aux ressources protégées.
5. Autorisation Granulaire avec Spring Security
Une fois authentifié, un utilisateur n'a pas forcément accès à toutes les ressources. Spring Security offre des mécanismes d'autorisation précis.
5.1 Autorisation au niveau de la méthode (@PreAuthorize, @PostAuthorize)
Spring Security permet de protéger les méthodes de vos services et contrôleurs en utilisant des annotations. Pour cela, assurez-vous que @EnableMethodSecurity (ou l'ancienne @EnableGlobalMethodSecurity(prePostEnabled = true)) est activée dans votre configuration de sécurité.
@PreAuthorize("hasRole('ADMIN')"): Vérifie l'autorisation avant l'exécution de la méthode. L'utilisateur doit avoir le rôleADMIN.@PreAuthorize("hasAnyRole('ADMIN', 'USER')"): Vérifie si l'utilisateur a l'un des rôles spécifiés.@PreAuthorize("hasAuthority('product:write')"): Vérifie si l'utilisateur a une permission spécifique (plus granulaire qu'un rôle).
Exemple :
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class SecuredResourceController {
@GetMapping("/secured/data")
@PreAuthorize("isAuthenticated()") // Seulement si l'utilisateur est authentifié
public String getSecuredData() {
return "Données accessibles aux utilisateurs authentifiés.";
}
@GetMapping("/admin/dashboard")
@PreAuthorize("hasRole('ADMIN')") // Seulement si l'utilisateur a le rôle ADMIN
public String getAdminDashboard() {
return "Bienvenue sur le tableau de bord administrateur !";
}
@GetMapping("/product/create")
@PreAuthorize("hasAuthority('product:create')") // Seulement si l'utilisateur a la permission product:create
public String createProduct() {
return "Création de produit autorisée.";
}
}
5.2 Autorisation basée sur les URLs (authorizeHttpRequests)
Comme vu dans la configuration SecurityFilterChain, vous pouvez définir des règles d'autorisation basées sur les chemins d'URL.
// Dans SecurityConfig.java
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/api/auth/**", "/public/**").permitAll() // Accès public
.requestMatchers("/admin/**").hasRole("ADMIN") // Rôle ADMIN requis
.requestMatchers("/product/**").hasAnyRole("ADMIN", "USER") // Rôle ADMIN ou USER requis
.requestMatchers(HttpMethod.POST, "/items").hasAuthority("item:create") // Permission spécifique pour une méthode HTTP
.anyRequest().authenticated() // Toutes les autres requêtes nécessitent une authentification
)
Conseil : Généralement, les autorisations au niveau de la méthode sont plus flexibles et granulaires pour les logiques métier complexes, tandis que les autorisations basées sur les URLs sont bonnes pour des règles d'accès générales à des groupes de ressources. Utilisez les deux de manière complémentaire.
6. OAuth2 et OpenID Connect (Bref Aperçu)
Bien que JWT soit excellent pour l'authentification des API par jetons, il est important de connaître OAuth2 et OpenID Connect.
6.1 Qu'est-ce qu'OAuth2 ?
OAuth2 est un cadre d'autorisation (pas d'authentification directe !) qui permet à une application (client) d'obtenir un accès limité aux ressources d'un utilisateur sur un serveur de ressources (API), avec l'approbation de l'utilisateur, sans jamais partager les identifiants de l'utilisateur avec l'application cliente.
- Scénario typique : "Voulez-vous permettre à cette application de consulter vos photos Facebook ?".
- Composants clés :
- Resource Owner : L'utilisateur final qui possède les données.
- Client : L'application qui veut accéder aux données (votre application Spring Boot, une application mobile, etc.).
- Authorization Server : Serveur qui authentifie le Resource Owner et émet des jetons d'accès.
- Resource Server : Serveur qui héberge les ressources protégées (votre API REST).
6.2 OAuth2 vs JWT
- OAuth2 : Un protocole ou un cadre pour l'autorisation déléguée. Il définit comment obtenir des jetons d'accès.
- JWT : Un format de jeton. Les jetons d'accès émis dans le cadre d'OAuth2 peuvent être des JWT (mais pas obligatoirement).
Souvent, on utilise OAuth2 pour gérer l'authentification et l'autorisation avec un fournisseur d'identité tiers (Google, Facebook, Okta, Keycloak), et ces fournisseurs émettent des JWT comme jetons d'accès.
6.3 OpenID Connect (OIDC)
OpenID Connect est une couche d'identité construite sur OAuth2. Il ajoute la capacité pour les clients de vérifier l'identité de l'utilisateur final et d'obtenir des informations de profil de base sur lui de manière interopérable et RESTful.
- OAuth2 est pour l'autorisation ("Pouvez-vous accéder à mes données ?").
- OpenID Connect est pour l'authentification ("Qui êtes-vous ?").
Quand utiliser OAuth2/OIDC avec Spring Security ? Principalement quand vous avez besoin de :
- Single Sign-On (SSO) : Authentifier vos utilisateurs via un fournisseur d'identité centralisé.
- Intégration tierce : Permettre à des applications tierces d'accéder à vos API en toute sécurité.
- Microservices : Gérer l'authentification et l'autorisation dans une architecture distribuée.
Spring Security propose un support robuste pour agir en tant que client OAuth2 (pour se connecter à des fournisseurs comme Google) ou même en tant que serveur d'autorisation OAuth2.
Conclusion
La sécurisation des API REST est un aspect fondamental du développement backend robuste. Avec Spring Security, nous disposons d'un arsenal puissant pour protéger nos applications contre les menaces.
Nous avons couvert :
- La compréhension de la nature sans état des API REST et des concepts d'authentification/autorisation.
- L'intégration de base de Spring Security avec une configuration de sécurité HTTP.
- L'implémentation de l'authentification par JWT, la méthode privilégiée pour les API REST sans état, incluant la génération et la validation des jetons.
- La mise en œuvre de l'autorisation granulaire via des règles d'URL et des annotations au niveau de la méthode (
@PreAuthorize). - Une introduction à OAuth2 et OpenID Connect pour des scénarios d'authentification déléguée et de fournisseurs d'identité.
La sécurité est un processus continu. Gardez toujours à l'esprit les meilleures pratiques, restez informé des nouvelles menaces et mettez régulièrement à jour vos dépendances pour bénéficier des dernières améliorations de sécurité. Avec ces connaissances, vous êtes bien équipé pour construire des API REST sécurisées et fiables avec Spring Boot. Continuez à expérimenter et à approfondir ces concepts dans vos projets !