Chapitre 5: Sécurité Avancée
Introduction
L'authentification n'est que la première ligne de défense. Ce chapitre couvre les mécanismes de protection avancée contre les attaques courantes et la gestion fine des autorisations.
🎯 Objectifs du Chapitre
À la fin de ce chapitre, vous saurez:
- ✅ Protéger contre les attaques CSRF
- ✅ Configurer CORS pour les API
- ✅ Utiliser @PreAuthorize et @Secured
- ✅ Implémenter un système RBAC
- ✅ Configurer les headers de sécurité
1. Protection CSRF
Qu'est-ce que CSRF?
Cross-Site Request Forgery : Un attaquant fait exécuter des actions à un utilisateur connecté sans son consentement.
Protection par Défaut
Spring Security active CSRF par défaut pour les applications web:
// Par défaut, CSRF est ACTIVÉ
// Le token CSRF est automatiquement vérifié sur POST, PUT, DELETE
Token CSRF dans les Formulaires
<!-- Thymeleaf - automatique avec th:action -->
<form th:action="@{/transfer}" method="post">
<!-- Le token CSRF est ajouté automatiquement -->
<input type="hidden" name="_csrf" th:value="${_csrf.token}"/>
...
</form>
<!-- Ou explicitement -->
<form action="/transfer" method="post">
<input type="hidden"
th:name="${_csrf.parameterName}"
th:value="${_csrf.token}"/>
...
</form>
CSRF pour API REST
Pour les API REST stateless avec JWT, désactivez CSRF:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable()) // ← Désactiver pour API REST
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
// ...
.build();
}
Désactivez CSRF uniquement si vous utilisez une authentification stateless (JWT, OAuth2). Pour les sessions, gardez-le activé!
CSRF Sélectif
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf
.ignoringRequestMatchers("/api/**") // Désactivé pour /api/
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
)
.build();
}
2. Configuration CORS
Qu'est-ce que CORS?
Cross-Origin Resource Sharing : Contrôle quels domaines peuvent accéder à votre API.
Frontend: https://app.example.com
Backend: https://api.example.com ← Domaine différent!
Sans CORS configuré → Requêtes bloquées par le navigateur
Configuration Globale
@Configuration
public class CorsConfig {
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
// Origines autorisées
configuration.setAllowedOrigins(List.of(
"http://localhost:3000",
"https://app.example.com"
));
// Méthodes HTTP autorisées
configuration.setAllowedMethods(List.of(
"GET", "POST", "PUT", "DELETE", "OPTIONS"
));
// Headers autorisés
configuration.setAllowedHeaders(List.of(
"Authorization",
"Content-Type",
"X-Requested-With"
));
// Autoriser les credentials (cookies, Authorization header)
configuration.setAllowCredentials(true);
// Durée de cache du preflight
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
Intégration avec Spring Security
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.cors(Customizer.withDefaults()) // ← Activer CORS
.csrf(csrf -> csrf.disable())
// ...
.build();
}
CORS par Controller
@RestController
@RequestMapping("/api/public")
@CrossOrigin(origins = "http://localhost:3000") // ← Spécifique
public class PublicController {
// ...
}
// Ou par méthode
@GetMapping("/data")
@CrossOrigin(origins = "*", maxAge = 3600)
public Data getData() {
// ...
}
3. Method-Level Security
Activation
@Configuration
@EnableWebSecurity
@EnableMethodSecurity // ← Active @PreAuthorize, @PostAuthorize, @Secured
public class SecurityConfig {
// ...
}
@PreAuthorize
Vérifie AVANT l'exécution de la méthode:
@RestController
@RequestMapping("/api")
public class UserController {
// Vérifier un rôle
@GetMapping("/admin/users")
@PreAuthorize("hasRole('ADMIN')")
public List<User> getAllUsers() { ... }
// Vérifier plusieurs rôles
@DeleteMapping("/users/{id}")
@PreAuthorize("hasAnyRole('ADMIN', 'MANAGER')")
public void deleteUser(@PathVariable Long id) { ... }
// Vérifier une authority/permission
@PostMapping("/users")
@PreAuthorize("hasAuthority('CREATE_USER')")
public User createUser(@RequestBody User user) { ... }
// Expression complexe
@PutMapping("/users/{id}")
@PreAuthorize("hasRole('ADMIN') or #id == authentication.principal.id")
public User updateUser(@PathVariable Long id, @RequestBody User user) { ... }
// Accès à l'objet authentification
@GetMapping("/me")
@PreAuthorize("isAuthenticated()")
public User getCurrentUser(Authentication auth) { ... }
}
@PostAuthorize
Vérifie APRÈS l'exécution (utile pour vérifier le résultat):
@GetMapping("/users/{id}")
@PostAuthorize("returnObject.username == authentication.name or hasRole('ADMIN')")
public User getUser(@PathVariable Long id) {
return userRepository.findById(id).orElseThrow();
}
@Secured (Legacy)
Plus simple mais moins flexible:
@Secured("ROLE_ADMIN")
public void adminOnly() { ... }
@Secured({"ROLE_ADMIN", "ROLE_MANAGER"})
public void adminOrManager() { ... }
Expressions SpEL Disponibles
| Expression | Description |
|---|---|
hasRole('ROLE') | A le rôle (ajoute ROLE_ automatiquement) |
hasAnyRole('R1', 'R2') | A un des rôles |
hasAuthority('AUTH') | A l'autorité exacte |
hasAnyAuthority('A1', 'A2') | A une des autorités |
isAuthenticated() | Est connecté |
isAnonymous() | N'est pas connecté |
isFullyAuthenticated() | Pas via remember-me |
permitAll | Toujours autorisé |
denyAll | Toujours refusé |
#paramName | Accès aux paramètres |
returnObject | Résultat (@PostAuthorize) |
authentication | Objet Authentication |
principal | L'utilisateur (UserDetails) |
4. Système RBAC (Role-Based Access Control)
Modèle de Données
Entités
@Entity
@Table(name = "permissions")
@Data
public class Permission {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String name; // READ_USERS, WRITE_USERS, DELETE_USERS
}
@Entity
@Table(name = "roles")
@Data
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String name; // ROLE_ADMIN, ROLE_USER, ROLE_MANAGER
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(
name = "role_permissions",
joinColumns = @JoinColumn(name = "role_id"),
inverseJoinColumns = @JoinColumn(name = "permission_id")
)
private Set<Permission> permissions = new HashSet<>();
}
UserDetails avec Permissions
public class CustomUserDetails implements UserDetails {
private final User user;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Set<GrantedAuthority> authorities = new HashSet<>();
for (Role role : user.getRoles()) {
// Ajouter le rôle
authorities.add(new SimpleGrantedAuthority(role.getName()));
// Ajouter les permissions du rôle
for (Permission permission : role.getPermissions()) {
authorities.add(new SimpleGrantedAuthority(permission.getName()));
}
}
return authorities;
}
// ...
}
Utilisation
// Par rôle
@PreAuthorize("hasRole('ADMIN')")
public void adminAction() { }
// Par permission
@PreAuthorize("hasAuthority('DELETE_USERS')")
public void deleteUser(Long id) { }
// Combinaison
@PreAuthorize("hasRole('MANAGER') and hasAuthority('APPROVE_ORDERS')")
public void approveOrder(Long orderId) { }
5. Headers de Sécurité
Configuration
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.headers(headers -> headers
// Content Security Policy
.contentSecurityPolicy(csp -> csp
.policyDirectives("default-src 'self'; script-src 'self'")
)
// X-Frame-Options (clickjacking)
.frameOptions(frame -> frame.sameOrigin())
// X-Content-Type-Options
.contentTypeOptions(Customizer.withDefaults())
// X-XSS-Protection
.xssProtection(xss -> xss.headerValue(XXssProtectionHeaderWriter.HeaderValue.ENABLED_MODE_BLOCK))
// HSTS (HTTPS obligatoire)
.httpStrictTransportSecurity(hsts -> hsts
.includeSubDomains(true)
.maxAgeInSeconds(31536000)
)
)
.build();
}
Headers Résultants
Content-Security-Policy: default-src 'self'; script-src 'self'
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Strict-Transport-Security: max-age=31536000; includeSubDomains
6. Protection Contre les Attaques
Brute Force (Limite de Tentatives)
@Service
public class LoginAttemptService {
private final Cache<String, Integer> attemptsCache = CacheBuilder.newBuilder()
.expireAfterWrite(1, TimeUnit.HOURS)
.build();
public void loginSucceeded(String key) {
attemptsCache.invalidate(key);
}
public void loginFailed(String key) {
int attempts = attemptsCache.get(key, () -> 0);
attemptsCache.put(key, attempts + 1);
}
public boolean isBlocked(String key) {
return attemptsCache.get(key, () -> 0) >= 5;
}
}
Intégration avec l'Authentification
@Component
@RequiredArgsConstructor
public class AuthenticationFailureListener
implements ApplicationListener<AuthenticationFailureBadCredentialsEvent> {
private final LoginAttemptService loginAttemptService;
@Override
public void onApplicationEvent(AuthenticationFailureBadCredentialsEvent event) {
String username = (String) event.getAuthentication().getPrincipal();
loginAttemptService.loginFailed(username);
}
}
@Component
@RequiredArgsConstructor
public class AuthenticationSuccessListener
implements ApplicationListener<AuthenticationSuccessEvent> {
private final LoginAttemptService loginAttemptService;
@Override
public void onApplicationEvent(AuthenticationSuccessEvent event) {
String username = event.getAuthentication().getName();
loginAttemptService.loginSucceeded(username);
}
}
7. Audit de Sécurité
Logging des Événements
@Component
@Slf4j
public class SecurityAuditListener {
@EventListener
public void onSuccess(AuthenticationSuccessEvent event) {
log.info("LOGIN SUCCESS: {} at {}",
event.getAuthentication().getName(),
LocalDateTime.now());
}
@EventListener
public void onFailure(AuthenticationFailureBadCredentialsEvent event) {
log.warn("LOGIN FAILED: {} at {}",
event.getAuthentication().getPrincipal(),
LocalDateTime.now());
}
@EventListener
public void onAccessDenied(AuthorizationDeniedEvent event) {
log.warn("ACCESS DENIED: {} tried to access {}",
event.getAuthentication().get().getName(),
event.getSource());
}
}
8. Résumé
Protections Essentielles
| Protection | Contre | Configuration |
|---|---|---|
| CSRF | Requêtes forgées | Activé par défaut (désactiver pour API REST) |
| CORS | Requêtes cross-origin | cors(Customizer.withDefaults()) |
| XSS | Scripts malveillants | Headers + CSP |
| Clickjacking | Frames malveillantes | X-Frame-Options |
| Brute Force | Attaques par force brute | Limitation de tentatives |
Method Security
@PreAuthorize("hasRole('ADMIN')") // Par rôle
@PreAuthorize("hasAuthority('DELETE_USER')") // Par permission
@PreAuthorize("#id == authentication.principal.id") // Par expression
Checklist de Sécurité
✅ CSRF activé pour les sessions
✅ CORS configuré pour l'API
✅ Mots de passe encodés (BCrypt)
✅ Headers de sécurité configurés
✅ @PreAuthorize sur les méthodes sensibles
✅ Audit des événements de sécurité
✅ Protection contre le brute force
🎯 Prochaine Étape
Vous avez maintenant toute la théorie! Place à la pratique avec les travaux pratiques.
➡️ TP8.1: Authentification Form Login