Différences
Ci-dessous, les différences entre deux révisions de la page.
| Les deux révisions précédentes Révision précédente Prochaine révision | Révision précédente | ||
| web:framework:spring:security [2023/12/05 11:07] – [Mise en place CSRF] jcheron | web:framework:spring:security [2026/10/26 11:24] (Version actuelle) – [Mise en place CSRF] jcheron | ||
|---|---|---|---|
| Ligne 1: | Ligne 1: | ||
| ====== Spring security ====== | ====== Spring security ====== | ||
| - | Spring | + | Spring |
| Spring security ajoute au Dispatcher servlet de Spring MVC un ensemble de filtres (servlet filters) qualifié de **filter chain**. | Spring security ajoute au Dispatcher servlet de Spring MVC un ensemble de filtres (servlet filters) qualifié de **filter chain**. | ||
| Ligne 7: | Ligne 7: | ||
| ===== Intégration ===== | ===== Intégration ===== | ||
| - | Ajouter | + | Ajouter |
| <sxh xml; | <sxh xml; | ||
| - | < | + | < |
| - | | + | < |
| - | | + | < |
| - | </ | + | </ |
| - | + | ||
| - | < | + | |
| - | < | + | |
| - | < | + | |
| - | < | + | |
| - | </ | + | |
| - | + | ||
| - | < | + | |
| - | < | + | |
| - | < | + | |
| - | </ | + | |
| </ | </ | ||
| ===== Authentification par défaut ===== | ===== Authentification par défaut ===== | ||
| - | L' | + | L' |
| dont le password s' | dont le password s' | ||
| Ligne 38: | Ligne 27: | ||
| Il est possible de modifier l' | Il est possible de modifier l' | ||
| - | <sxh ; | + | < |
| spring.security.user.name=boris | spring.security.user.name=boris | ||
| spring.security.user.password=boris-password | spring.security.user.password=boris-password | ||
| Ligne 47: | Ligne 36: | ||
| La configuration se fait au travers d'une classe de configuration activant la sécurité : | La configuration se fait au travers d'une classe de configuration activant la sécurité : | ||
| - | <sxh java;title:WebSecurityConfig> | + | <sxh java;title:SecurityConfig> |
| @Configuration | @Configuration | ||
| @EnableWebSecurity | @EnableWebSecurity | ||
| - | public class WebSecurityConfiguration | + | public class SecurityConfig |
| @Bean | @Bean | ||
| - | public SecurityFilterChain | + | public SecurityFilterChain |
| - | http.csrf(AbstractHttpConfigurer:: | + | http.authorizeHttpRequests(req -> req |
| - | | + | |
| - | AntPathRequestMatcher.antMatcher("/" | + | .anyRequest().authenticated() |
| - | AntPathRequestMatcher.antMatcher("/ | + | ); |
| - | AntPathRequestMatcher.antMatcher("/ | + | |
| - | AntPathRequestMatcher.antMatcher("/ | + | |
| - | AntPathRequestMatcher.antMatcher("/ | + | |
| - | ) | + | |
| - | | + | |
| - | .anyRequest() | + | |
| - | | + | |
| - | | + | |
| return http.build(); | return http.build(); | ||
| } | } | ||
| } | } | ||
| </ | </ | ||
| + | |||
| + | <wrap info round> | ||
| + | Les chaînes passées à **requestMatchers** sont interprétées comme des patterns de chemin. Il n'est plus nécessaire d' | ||
| + | </ | ||
| + | |||
| + | <wrap important round> | ||
| + | La protection CSRF est **activée par défaut**. Ne la désactivez pas (**csrf.disable()**) sans raison valable, voir la section [[# | ||
| + | </ | ||
| + | |||
| ===== Authentification depuis une BDD ===== | ===== Authentification depuis une BDD ===== | ||
| ==== Entity ==== | ==== Entity ==== | ||
| - | Créer ou reprendre votre entity **User** pour gérer les utilisateurs et leur rôle : | + | Créer ou reprendre votre entity **User** pour gérer les utilisateurs et leur rôle. |
| Elle devra implémenter l' | Elle devra implémenter l' | ||
| <sxh java; | <sxh java; | ||
| + | @Entity | ||
| public class User implements UserDetails { | public class User implements UserDetails { | ||
| @Id | @Id | ||
| Ligne 95: | Ligne 86: | ||
| @ManyToOne | @ManyToOne | ||
| private Role role; | private Role role; | ||
| - | | + | |
| @Override | @Override | ||
| public Collection<? | public Collection<? | ||
| Ligne 129: | Ligne 120: | ||
| return true; | return true; | ||
| } | } | ||
| + | |||
| + | // getters / setters ... | ||
| } | } | ||
| </ | </ | ||
| - | Créer le repository associé et ajouter une méthode permettant de rechercher un User par son username | + | Créer le repository associé et ajouter une méthode permettant de rechercher un User par son login ou son email : |
| <sxh java; | <sxh java; | ||
| Ligne 142: | Ligne 135: | ||
| ==== UserDetailsService ==== | ==== UserDetailsService ==== | ||
| - | Créer un Service implémentant **UserDetailsService** | + | Créer un Service implémentant **UserDetailsService**. |
| - | Ce service retourne depuis la base de données un utilisateur recherché par son nom ou son email et retourne | + | Ce service retourne depuis la base de données un utilisateur recherché par son login ou son email, sous la forme d'une instance de **UserDetails** : |
| - | <sxh java;title:DbUserService> | + | <sxh java;title:UserService> |
| @Service | @Service | ||
| public class UserService implements UserDetailsService { | public class UserService implements UserDetailsService { | ||
| - | | + | private |
| - | | + | private final RoleRepository rRepo; |
| + | private final PasswordEncoder pEncoder; | ||
| - | | + | |
| - | private | + | this.uRepo = uRepo; |
| - | + | | |
| - | | + | |
| - | private PasswordEncoder | + | } |
| @Override | @Override | ||
| public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { | public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { | ||
| - | | + | |
| - | | + | .orElseThrow(() -> new UsernameNotFoundException(" |
| } | } | ||
| Ligne 170: | Ligne 164: | ||
| } | } | ||
| </ | </ | ||
| + | |||
| + | <wrap info> | ||
| + | Préférez l' | ||
| + | </ | ||
| ==== Password encoder ==== | ==== Password encoder ==== | ||
| - | Modifier | + | Déclarer dans la configuration |
| - | <sxh java;title:WebSecurityConfig> | + | |
| + | <sxh java;title:SecurityConfig> | ||
| @Configuration | @Configuration | ||
| @EnableWebSecurity | @EnableWebSecurity | ||
| - | public class WebSecurityConfiguration | + | public class SecurityConfig |
| @Bean | @Bean | ||
| - | public SecurityFilterChain | + | public SecurityFilterChain |
| ... | ... | ||
| return http.build(); | return http.build(); | ||
| } | } | ||
| - | @Primary | ||
| @Bean | @Bean | ||
| - | | + | public PasswordEncoder |
| - | return new UserService(); | + | |
| - | } | + | |
| - | + | ||
| - | @Bean | + | |
| - | | + | |
| return new BCryptPasswordEncoder(); | return new BCryptPasswordEncoder(); | ||
| } | } | ||
| Ligne 198: | Ligne 191: | ||
| </ | </ | ||
| - | < | + | < |
| - | + | Spring Boot détecte automatiquement un bean **UserDetailsService** (ici **UserService**, | |
| - | ==== Déclaration du service ==== | + | Il n'est donc **pas nécessaire** de déclarer manuellement un **DaoAuthenticationProvider**, |
| - | + | </wrap> | |
| - | <sxh java; | + | |
| - | @Configuration | + | |
| - | @EnableWebSecurity | + | |
| - | public class WebSecurityConfiguration { | + | |
| - | ... | + | |
| - | @Bean | + | |
| - | public DaoAuthenticationProvider authenticationProvider(UserDetailsService userService) { | + | |
| - | DaoAuthenticationProvider auth = new DaoAuthenticationProvider(); | + | |
| - | auth.setUserDetailsService(userService); | + | |
| - | auth.setPasswordEncoder(getPasswordEncoder()); | + | |
| - | return auth; | + | |
| - | } | + | |
| - | } | + | |
| - | </sxh> | + | |
| + | <wrap important round> | ||
| + | Attention aux **dépendances circulaires** : si **UserService** injecte **PasswordEncoder** et que le bean **PasswordEncoder** est défini dans une classe qui injecte **UserService**, | ||
| + | </ | ||
| === Création d' | === Création d' | ||
| - | Ne pas oublier de hasher le password User avant création : | + | Ne pas oublier de hasher le password |
| - | <sxh java;title:DbUserService> | + | <sxh java;title:UserService> |
| @Service | @Service | ||
| public class UserService implements UserDetailsService { | public class UserService implements UserDetailsService { | ||
| Ligne 234: | Ligne 216: | ||
| user.setRole(getRole(" | user.setRole(getRole(" | ||
| return uRepo.save(user); | return uRepo.save(user); | ||
| + | } | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | === Récupération de l' | ||
| + | Récupération de l' | ||
| + | |||
| + | <sxh java; | ||
| + | @ControllerAdvice | ||
| + | public class MainAdvice { | ||
| + | @ModelAttribute(" | ||
| + | public User activeUser(Authentication auth) { | ||
| + | return (auth == null) ? null : (User) auth.getPrincipal(); | ||
| } | } | ||
| } | } | ||
| Ligne 241: | Ligne 236: | ||
| Créer un template pour le formulaire de connexion : | Créer un template pour le formulaire de connexion : | ||
| - | <sxh html;title:formLogin.html> | + | |
| + | <sxh html;title:loginForm.html> | ||
| <form method=" | <form method=" | ||
| <input type=" | <input type=" | ||
| <input type=" | <input type=" | ||
| + | <!-- Token CSRF (voir section CSRF) --> | ||
| + | <input type=" | ||
| <button type=" | <button type=" | ||
| </ | </ | ||
| </ | </ | ||
| - | Ajouter une route pour le **login** | + | Ajouter une route pour le **login** |
| - | <sxh java:AppConfig> | + | <sxh java;title:WebConfiguration> |
| @Configuration | @Configuration | ||
| public class WebConfiguration implements WebMvcConfigurer { | public class WebConfiguration implements WebMvcConfigurer { | ||
| - | + | ||
| @Override | @Override | ||
| public void addViewControllers(ViewControllerRegistry registry) { | public void addViewControllers(ViewControllerRegistry registry) { | ||
| - | registry.addViewController("/ | + | |
| } | } | ||
| } | } | ||
| </ | </ | ||
| - | Modifier la configuration dans **WebSecurityConfig** : | + | Modifier la configuration dans **SecurityConfig** : |
| - | <sxh java;title:WebSecurityConfiguration> | + | <sxh java;title:SecurityConfig> |
| @Configuration | @Configuration | ||
| @EnableWebSecurity | @EnableWebSecurity | ||
| - | public class WebSecurityConfiguration | + | public class SecurityConfig |
| @Bean | @Bean | ||
| - | public SecurityFilterChain | + | public SecurityFilterChain |
| - | http. | + | http |
| ... | ... | ||
| - | .formLogin( | + | .formLogin(form -> form |
| - | | + | |
| + | | ||
| + | | ||
| ); | ); | ||
| return http.build(); | return http.build(); | ||
| } | } | ||
| + | } | ||
| </ | </ | ||
| ==== Connexion à la console H2 ==== | ==== Connexion à la console H2 ==== | ||
| - | Pour autoriser l' | + | Pour autoriser l' |
| - | - Désactiver | + | - Ignorer |
| - Autoriser la route vers la console **/ | - Autoriser la route vers la console **/ | ||
| - | - Autoriser l' | + | - Autoriser l' |
| - | <sxh java;title:WebSecurityConfig> | + | <sxh java;title:SecurityConfig> |
| @Configuration | @Configuration | ||
| @EnableWebSecurity | @EnableWebSecurity | ||
| - | public class WebSecurityConfiguration | + | public class SecurityConfig |
| @Bean | @Bean | ||
| - | public SecurityFilterChain | + | public SecurityFilterChain |
| - | http.csrf(AbstractHttpConfigurer:: | + | http |
| - | (req) -> req.requestMatchers( | + | |
| - | | + | |
| - | ... | + | |
| - | ) | + | .requestMatchers("/", "/css/ |
| - | | + | .anyRequest().authenticated() |
| - | .anyRequest() | + | ) |
| - | | + | |
| - | ).headers( | + | |
| - | | + | |
| ); | ); | ||
| return http.build(); | return http.build(); | ||
| Ligne 308: | Ligne 308: | ||
| } | } | ||
| </ | </ | ||
| + | |||
| + | <wrap important round> | ||
| + | La console H2 ne doit **jamais** être exposée en production. Idéalement, | ||
| + | </ | ||
| + | |||
| ===== Rôles et authorities ===== | ===== Rôles et authorities ===== | ||
| - | Spring Security distingue les authorities, | + | Spring Security distingue les authorities, |
| - | et le rôle équivalent, | + | et le rôle équivalent, |
| - | Il est possible de faire référence à l'un ou à l' | + | Il est possible de faire référence à l'un ou à l' |
| + | * **hasRole(" | ||
| + | * **hasAuthority(" | ||
| + | |||
| + | Le rôle des utilisateurs pourra donc être stocké en base de données, sous la forme d'une chaîne, ou d'une liste de chaînes. | ||
| - | Le rôle des utilisateurs pourra dons être stocké en base de données, sous la forme d'une chaîne, ou d'une liste de chaînes. | ||
| ==== Hiérarchie des authorities ==== | ==== Hiérarchie des authorities ==== | ||
| Il est possible de définir les authorities de manière hiérarchique : | Il est possible de définir les authorities de manière hiérarchique : | ||
| - | <sxh java> | + | <sxh java; |
| @Bean | @Bean | ||
| static RoleHierarchy roleHierarchy() { | static RoleHierarchy roleHierarchy() { | ||
| - | RoleHierarchyImpl | + | |
| - | hierarchy.setHierarchy(" | + | |
| - | return hierarchy; | + | ROLE_REDACTOR |
| + | "" | ||
| } | } | ||
| - | |||
| @Bean | @Bean | ||
| - | static MethodSecurityExpressionHandler methodSecurityExpressionHandler() { | + | static MethodSecurityExpressionHandler methodSecurityExpressionHandler(RoleHierarchy roleHierarchy) { |
| DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler(); | DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler(); | ||
| - | expressionHandler.setRoleHierarchy(roleHierarchy()); | + | expressionHandler.setRoleHierarchy(roleHierarchy); |
| return expressionHandler; | return expressionHandler; | ||
| } | } | ||
| </ | </ | ||
| - | Les utilisateurs peuvent dans ce cas n' | + | Les utilisateurs peuvent dans ce cas n' |
| - | + | ||
| - | <WRAP alert> | + | |
| - | Inutilisable pour l' | + | |
| - | </ | + | |
| + | <wrap info> | ||
| + | Les méthodes **static** sont recommandées pour ces beans, afin qu'ils soient instanciés tôt dans le cycle de vie du contexte Spring.\\ | ||
| + | La méthode **RoleHierarchyImpl.fromHierarchy(...)** est disponible à partir de Spring Security 6.3 ; pour les versions antérieures, | ||
| + | </ | ||
| ==== Configuration des rôles ==== | ==== Configuration des rôles ==== | ||
| - | <sxh java;title:WebSecurityConfig> | + | <sxh java;title:SecurityConfig> |
| @Configuration | @Configuration | ||
| @EnableWebSecurity | @EnableWebSecurity | ||
| - | @EnableMethodSecurity( | + | @EnableMethodSecurity |
| - | prePostEnabled = true, proxyTargetClass = true | + | public class SecurityConfig |
| - | ) | + | |
| - | public class WebSecurityConfiguration | + | |
| @Bean | @Bean | ||
| - | public SecurityFilterChain | + | public SecurityFilterChain |
| - | http.csrf(AbstractHttpConfigurer:: | + | http.authorizeHttpRequests(req -> req |
| - | | + | .requestMatchers("/ |
| - | req.requestMatchers( | + | .requestMatchers("/ |
| - | AntPathRequestMatcher.antMatcher("/ | + | .requestMatchers("/ |
| - | AntPathRequestMatcher.antMatcher("/ | + | .requestMatchers("/ |
| - | AntPathRequestMatcher.antMatcher("/ | + | .anyRequest().authenticated() |
| - | ) | + | ); |
| - | | + | |
| - | req.requestMatchers(AntPathRequestMatcher.antMatcher("/ | + | |
| - | req.requestMatchers(AntPathRequestMatcher.antMatcher("/ | + | |
| - | req.requestMatchers(AntPathRequestMatcher.antMatcher("/ | + | |
| - | req.anyRequest().authenticated(); // (5) | + | |
| - | } | + | |
| - | | + | |
| return http.build(); | return http.build(); | ||
| } | } | ||
| Ligne 376: | Ligne 375: | ||
| ^Méthode ^ Description ^ | ^Méthode ^ Description ^ | ||
| | 1 - **permitAll** | Pas de sécurisation sur les urls commençant par **/css**, **/ | | 1 - **permitAll** | Pas de sécurisation sur les urls commençant par **/css**, **/ | ||
| - | | 2 - **hasRole** | Les utilisateurs ayant le rôle **ROLE_ADMIN** peuvent accéder aux urls commençant par **/admin**. | | + | | 2 - **hasRole** | Les utilisateurs ayant le rôle **ADMIN** (authority |
| - | | 3 - **hasAuthority** | Les utilisateurs ayant l' | + | | 3 - **hasAuthority** | Les utilisateurs ayant l' |
| - | | 4 - **hasAnyAuthority** | Les utilisateurs ayant l'une des authorities **USER**, **ADMIN** ou **MANAGER** peuvent accéder aux urls commençant par **/staff**. | | + | | 4 - **hasAnyAuthority** | Les utilisateurs ayant l'une des authorities **ROLE_USER**, **ROLE_ADMIN** ou **ROLE_MANAGER** peuvent accéder aux urls commençant par **/staff**. | |
| | 5 - **authenticated** | Pour toutes les autres URLs, il faut être authentifié. | | | 5 - **authenticated** | Pour toutes les autres URLs, il faut être authentifié. | | ||
| - | ==== defense | + | <wrap important round> |
| - | Spring security permet de définir des authorisations | + | **Ordre des règles** : elles sont évaluées dans l' |
| + | </ | ||
| + | |||
| + | ==== Defense | ||
| + | Spring security permet de définir des autorisations | ||
| === Activation === | === Activation === | ||
| - | <sxh java;title:WebSecurityConfig; | + | <sxh java;title:SecurityConfig; |
| @Configuration | @Configuration | ||
| @EnableWebSecurity | @EnableWebSecurity | ||
| @EnableMethodSecurity( | @EnableMethodSecurity( | ||
| - | prePostEnabled = true, | + | prePostEnabled = true, |
| - | securedEnabled = true, | + | securedEnabled = true, |
| jsr250Enabled = true | jsr250Enabled = true | ||
| ) | ) | ||
| - | public class WebSecurityConfiguration | + | public class SecurityConfig |
| ... | ... | ||
| } | } | ||
| Ligne 400: | Ligne 403: | ||
| ^Propriété ^Défaut ^Description ^ | ^Propriété ^Défaut ^Description ^ | ||
| - | |prePostEnabled | true | permet d’utiliser les annotations **@PreAuthorize** et **@PostAuthorize**. | | + | |prePostEnabled | true | permet d'utiliser les annotations **@PreAuthorize** et **@PostAuthorize**. | |
| - | |securedEnabled | false | permet d’utiliser l’annotation **@Secured**. | | + | |securedEnabled | false | permet d'utiliser l'annotation **@Secured**. | |
| - | |jsr250Enabled | false | permet d’utiliser l’annotation **@RolesAllowed**. | | + | |jsr250Enabled | false | permet d'utiliser l'annotation **@RolesAllowed**. | |
| + | <wrap info> | ||
| + | Dans les versions récentes de Spring Boot, **jakarta.annotation.security.RolesAllowed** remplace l' | ||
| + | </ | ||
| == @Secured == | == @Secured == | ||
| Ligne 417: | Ligne 422: | ||
| == @RolesAllowed == | == @RolesAllowed == | ||
| <sxh java> | <sxh java> | ||
| - | @RolesAllowed(value = [ " | + | @RolesAllowed({" |
| public String userOrManager() { | public String userOrManager() { | ||
| return " | return " | ||
| Ligne 430: | Ligne 435: | ||
| <sxh java> | <sxh java> | ||
| - | @PreAuthorize(" | + | @PreAuthorize(" |
| public String getUsernameInUpperCase() { | public String getUsernameInUpperCase() { | ||
| - | return getUserName().upperCase(); | + | return getUserName().toUpperCase(); |
| } | } | ||
| </ | </ | ||
| - | |||
| Comparaison du paramètre **username** avec le nom de l' | Comparaison du paramètre **username** avec le nom de l' | ||
| - | < | + | |
| + | < | ||
| @PreAuthorize("# | @PreAuthorize("# | ||
| - | public List< | + | public List< |
| ... | ... | ||
| } | } | ||
| Ligne 451: | Ligne 456: | ||
| * En testant éventuellement le retour. | * En testant éventuellement le retour. | ||
| - | < | + | < |
| - | @PostAuthorize(" | + | @PostAuthorize(" |
| public CustomUser loadUserDetail(String username) { | public CustomUser loadUserDetail(String username) { | ||
| return userRoleRepository.loadUserByUserName(username); | return userRoleRepository.loadUserByUserName(username); | ||
| Ligne 458: | Ligne 463: | ||
| </ | </ | ||
| - | voir https:// | + | Voir la [[https:// |
| ==== Mise en place CSRF ==== | ==== Mise en place CSRF ==== | ||
| Ligne 465: | Ligne 470: | ||
| === Activation === | === Activation === | ||
| - | Activer | + | La protection |
| + | Si votre configuration contient | ||
| <sxh java; | <sxh java; | ||
| http.csrf(AbstractHttpConfigurer:: | http.csrf(AbstractHttpConfigurer:: | ||
| </ | </ | ||
| - | <wrap info> | + | Exception éventuelle |
| - | La protection csrf est activée par défaut dans **Spring security** | + | |
| - | </ | + | |
| - | + | ||
| - | Mettre éventuellement une exception | + | |
| <sxh java; | <sxh java; | ||
| - | req.csrf(csrf-> | + | http.csrf(csrf -> csrf.ignoringRequestMatchers(PathRequest.toH2Console())) |
| </ | </ | ||
| === Intégration dans les vues === | === Intégration dans les vues === | ||
| - | Dans **application.properties**, | + | Dans **application.properties**, |
| - | <sxh ini; | + | <sxh ini; |
| - | # Expose Request Attributes | + | |
| spring.mustache.servlet.expose-request-attributes=true | spring.mustache.servlet.expose-request-attributes=true | ||
| </ | </ | ||
| Ligne 492: | Ligne 494: | ||
| <sxh html; | <sxh html; | ||
| - | < | + | < |
| - | ... | + | ... |
| <input type=" | <input type=" | ||
| | | ||
| | | ||
| - | ... | + | |
| </ | </ | ||
| </ | </ | ||
| + | <wrap info round> | ||
| + | Avec **Thymeleaf**, | ||
| + | </ | ||
| - | voir [[https://docs.spring.io/spring-security/ | + | <wrap important round> |
| - | + | Attention à la route **/ | |
| - | + | </wrap> | |
| + | Voir [[https:// | ||