Définition de Spring Security
Spring Security est un Framework de sécurité léger qui fournit une authentification et un support d’autorisation afin de sécuriser les applications Spring. Il est livré avec des implémentations d’algorithmes de sécurité populaires.
Cet article nous guide tout au long du processus de création d’un simple exemple de connexion à une application avec Spring Boot, Spring Security, Spring Data JPA et MYSQL
Configuration d’application
Commençons par une application très basique (en termes d’installation nécessaire) qui démarre un contexte d’application Spring. Deux outils qui nous aideront avec cela sont Maven et Spring Boot. Je sauterai les lignes qui ne sont pas particulièrement intéressantes comme la configuration du référentiel Maven. Vous pouvez trouver le code complet sur GitHub.
Pour commencer nous allons ajouter la dépendance suivante au pom de notre application.
1. <dependency> 2. <groupId>org.springframework.boot</groupId> 3. <artifactId>spring-boot-starter-security</artifactId> 4. </dependency>
Au démarrage de notre application nous avons dans la console les informations suivantes :
. ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.0.4.RELEASE) 2018-09-10 22:44:09.059 INFO 645 --- [ main] .s.s.UserDetailsServiceAutoConfiguration : Using generated security password: c657aef6-758a-409d-ac02-814ff4df55be
Spring Boot nous fournit donc un mot de passe par défaut.
Nous venons d’ajouter Spring Security au classpath de notre application, Spring instaure une configuration par défaut, désormais pour accéder à notre application nous aurons besoin d’un User et d’un mot de passe.
Donc pour accéder à notre application avec la configuration par défaut de Spring, nous entrons l’user comme nom d’utilisateur user et le mot de passe par défaut fourni par Spring, celui affiché dans la console au démarrage de notre application (ici c657aef6-758a-409d-ac02-814ff4df55be) dans le formulaire d’authentification.
Dans la suite de cet article, nous allons personnaliser cette configuration.
Création de l’entité User
Nous allons créer la classe User.java, cette classe implémente interface UserDetails.
L’interface fournit des informations sur l’utilisateur principal. Les implémentations ne sont pas utilisées directement par Spring Security à des fins de sécurité. Ils stockent simplement les informations utilisateur qui sont ensuite encapsulées dans des objets d’authentification. Cela permet de stocker des informations non liées à la sécurité (telles que les adresses e-mail etc.) dans un emplacement approprié.
@Entity @Table(name = "USER") public class User implements Serializable , UserDetails { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer userId; private String username; private String password; public Integer getUserId() { return userId; } public void setUserId(Integer userId) { this.userId = userId; } public String getUsername() { return username; } @Override public boolean isAccountNonExpired() { return false; } @Override public boolean isAccountNonLocked() { return false; } @Override public boolean isCredentialsNonExpired() { return false; } @Override public boolean isEnabled() { return false; } public void setUsername(String username) { this.username = username; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return null; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } }
Ci-dessous les scripts d’alimentation de notre table dans la base de données embarquée H2. Le script est placé dans le fichier data.sql, les scripts situés dans le fichier data.sql sont exécutés à chaque démarrage de notre application.
INSERT INTO USER(username, password) VALUES('ADMIN', 'ADMIN')
Implémentation de La classe AppAuthenticationProvider
Spring Security fournit une variété d’options pour effectuer l’authentification. Toutes ces options suivent un contrat simple. Une demande d’authentification est traitée par un AuthenticationProvider et un objet entièrement authentifié avec des informations d’identification complètes est renvoyé.
L’implémentation standard et la plus courante est le DaoAuthenticationProvider – qui récupère les détails de l’utilisateur à partir d’un simple DAO utilisateur en lecture seule – le UserDetailsService. Ce service de détails de l’utilisateur a uniquement accès au nom d’utilisateur afin de récupérer l’entité utilisateur complète et dans un grand nombre de scénarios, cela suffit.
public class AppAuthProvider extends DaoAuthenticationProvider { @Autowired UserService userDetailsService; @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { UsernamePasswordAuthenticationToken auth = (UsernamePasswordAuthenticationToken) authentication; String name = auth.getName(); String password = auth.getCredentials() .toString(); UserDetails user = userDetailsService.loadUserByUsername(name); if (user == null) { throw new BadCredentialsException("Username/Password does not match for " + auth.getPrincipal()); } return new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities()); } @Override public boolean supports(Class<?> authentication) { return true; } }
Implémentation du Repository UserRepository
Nous allons créer la classe UserRepository.java, cette classe hérite de la classe JpaRepository. La fonction findUserWithName(String name) permet de récupérer un User à partir de son nom d’utilisateur.
public interface UserRepository extends JpaRepository<User, Integer> { @Query(" select u from User u " + " where u.username = ?1") Optional<User> findUserWithName(String username); }
Implémentation du Service UserService
Nous allons créer la classe UserService.java, cette classe implémente interface UserDetailsService. L’interface UserDetailsService est utilisée pour récupérer les données liées à l’utilisateur. Il a une méthode nommée loadUserByUsername qui trouve une entité utilisateur basée sur le nom d’utilisateur et peut être substituée pour personnaliser le processus de recherche de l’utilisateur. Il est utilisé par DaoAuthenticationProvider pour charger des détails sur l’utilisateur lors de l’authentification.
@Service @Slf4j public class UserService implements UserDetailsService { private final UserRepository userRepository; @Autowired public UserService(UserRepository userRepository) { this.userRepository = userRepository; } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { Objects.requireNonNull(username); User user = userRepository.findUserWithName(username) .orElseThrow(() -> new UsernameNotFoundException("User not found")); return user; } }
Implémentation de la classe de configuration SecurityConfig
Pour personnaliser la configuration nous allons créer une classe qui hérite de WebSecurityConfigurerAdaptater. Cette classe doit avoir les annotations @EnableWebSecurity et @Configuration. Les classes de configuration sont scannées au démarrage de l’application.
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired UserService userDetailsService; @Autowired private AccessDeniedHandler accessDeniedHandler; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService); } @Override protected void configure(HttpSecurity http) throws Exception { http.csrf() .disable() .exceptionHandling() .authenticationEntryPoint(new Http403ForbiddenEntryPoint() { }) .and() .authenticationProvider(getProvider()) .formLogin() .loginProcessingUrl("/login") .successHandler(new AuthentificationLoginSuccessHandler()) .failureHandler(new SimpleUrlAuthenticationFailureHandler()) .and() .logout() .logoutUrl("/logout") .logoutSuccessHandler(new AuthentificationLogoutSuccessHandler()) .invalidateHttpSession(true) .and() .authorizeRequests() .antMatchers("/login").permitAll() .antMatchers("/logout").permitAll() .antMatchers("/user").authenticated() .anyRequest().permitAll(); } private class AuthentificationLoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { response.setStatus(HttpServletResponse.SC_OK); } } private class AuthentificationLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler { @Override public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { response.setStatus(HttpServletResponse.SC_OK); } } @Bean public AuthenticationProvider getProvider() { AppAuthProvider provider = new AppAuthProvider(); provider.setUserDetailsService(userDetailsService); return provider; } }
Nous définissons la manière donc sont gérés les utilisateurs dans la méthode configure(AuthenticationManagerBuilder auth). Ici la gestion des utilisateurs est dynamique, on gère les utilisateurs via le service UserSevice.
La gestion des ressources à protéger se fait au niveau de la fonction configure(HttpSecurity http). Tout accès à url <host>/user/** nécessite d’être authentifié. L’authentification se fait via une requête de type Post à url <host>/login.
Tests Postman
Nous allons tester accès à notre application en utilisant Postman Rest qui est un client pour API web.
En accédant à GET : <host>/user/ nous avons une erreur d’authentification comme ci-dessous :
Accès à GET : <host>/user/ nécessite d’être authentifié au préalable.
Pour nous authentifier nous allons accéder à POST : <host>/login avec un User présent dans notre base de données.
Nous avons un Statut 200, authentification a donc fonctionné.
Nous allons donc à nouveau accéder à GET : <host>/user/
Le mot de la fin
Cet article nous a montré comment utiliser Spring Security pour sécuriser son application Spring Boot. Vous pouvez trouver le code complet sur GitHub.
Assistez à notre webinar d’Introduction à l’auto-configuration de Spring !
Bonjour,
Merci pour ce tuto.
J’ai une question, dans la classe AppAuthProvider on cherche l’utilisateur qu’avec son username:
userDetailsService.loadUserByUsername(name);
Du coup on peut mettre n’importe quel mot de passe, il sera accepter. Est-ce l’effet voulu?
Comment peut-on prendre en compte le mot de passe aussi?
Merci
Bonjour,
On peut pas rentrer n’importe quel mot de passe. Après avoir récupéré le User via la méthode loadUserByUsername Spring Security va comparer le mot de passe du user récupéré de la base avec celui qu’on a rentré. Si les deux mots de passe sont les différents, on aura une erreur de connexion.
Cordialement
Merci pour ce tuto
Bonjour,
Je n’arrive pas à lancer le projet parce que dans la classe de sécurité, “accessDeniedHandler” est en Autowired, j’obtiens une erreur me demandant de définir le bean dans ma configuration. Savez-vous pourquoi ?
Merci
Charline,
Tu dois declarer ton bean dans un fichier applicationContext.xml
Bisous
Bonjour,
Merci beaucoup pour ce tuto complet, je veux faire mon front avec angular et je ne sais pas comment intégrer spring security aves le composant login.
Merci d’avance
Bonjour,
Je récupère un nom d’utilisateur et un mot de passe toujours vides dans le provider. La requête envoyée par le formulaire doit avoir un format particulier ?
Merci
Bonjour,
je reprends le commentaire de vniversum :
quand je fais le test avec mon username et n’importe quel password, ça fonctionne… pas de rejet !
J’ai du mal à voir où est ce que Spring contrôle le password ??
une idée ?
Bonjour ca me fais cette erreur je ne comprend pas
Parameter 0 of constructor in com.webencyclop.demo.UserService required a bean named ‘entityManagerFactory’ that could not be found.
The injection point has the following annotations:
– @org.springframework.beans.factory.annotation.Autowired(required=true)
Action:
Consider defining a bean named ‘entityManagerFactory’ in your configuration.
Bonjour pour la question du mot de passe c’est normale que ça passe avec n’importe quel mot de passe si vous voulez valider aussi avec mot de passe vous pouvez comparez les deux passwords ( celui qui est renvoyé avec la requête disponible à partir de l’objet auth et celui récupéré dans la base de donné disponible dans l’objet user ) après vous faite ce que vous voulez selon le résultat du test de la comparaison
crée un bean PasswordEncoder qui retourne un objet BCryptPasswordEncoder a injecter comme dans userService pour crypter le password dans votre methode de creation de user. Puis utiliser ce meme passwordEncoder dans appAuthProvider pour comparer le password brut et celui crypté avec la methode matches. ensuite comparer au niveau du controle si user==null || resultatComparaison==false. 🙂
Bonjour, merci pour ce tuto bien écrit.
J’écume un peu les différents tutos sur ce sujet et je bloque toujours sur une chose : l’utilisation de spring security avec un tomcat en stand alone.
Je bosse sur une petite API pour un projet personnel. Je souhaite que certaines URI soient disponibles par tous sans authentification, et d’autres soient sécurisées.
Je passe par BasicAuth avec des utilisateurs “fixes” en base.
Depuis IntelliJ, pas de soucis, la sécurité fonctionne très bien.
Par contre toutes mes URI sont protégées lorsque je déploie le .war dans un tomcat.
Le configure :
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests(authorizeRequests ->
authorizeRequests.antMatchers(HttpMethod.POST, “/rest/gender”).anonymous()
.antMatchers(HttpMethod.POST, “/rest/login”).permitAll()
.antMatchers(HttpMethod.POST, “/rest/names”).permitAll()
.anyRequest().hasRole(“ADMIN”)
)
.httpBasic()
.authenticationEntryPoint(authEntryPoint)
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
Une idée ?
Bonjour à tous,
Pour résumer la réponse de ECAUMANO en source code.
Vous avez besoin d’une nouvelle classe pour créer le bean de BCryptPasswordEncoder dans le package configuration.
package com.sip.ams.configuration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Bean
public BCryptPasswordEncoder passwordEncoder() {
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
return bCryptPasswordEncoder;
}
}
Dans votre service on va ajouter deux méthodes :
public User saveUser(User user) {
//Cryptage de mot de pass lors du sauvegarde
user.setPassword(bCryptPasswordEncoder.encode(user.getPassword()));
return userRepository.save(user);
}
//Comparaison entre le mot passwordAuth et le passwordBase chiffré
public Boolean comparePassword(String passwordAuth, String passwordBase) {
return bCryptPasswordEncoder.matches(passwordAuth, passwordBase);
}
Voici l’appel de comparaison dans le code de AppAuthProvider
public class AppAuthProvider extends DaoAuthenticationProvider {
@Autowired
UserService userDetailsService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
UsernamePasswordAuthenticationToken auth = (UsernamePasswordAuthenticationToken) authentication;
String name = auth.getName();
String password = auth.getCredentials().toString();
UserDetails user = userDetailsService.loadUserByUsername(name);
if (userDetailsService.comparePassword(password, user.getPassword()) ) {
return new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
}
throw new BadCredentialsException(“Username/Password does not match for ” + auth.getPrincipal());
}
@Override
public boolean supports(Class authentication) {
return true;
}
Voici l’appel de enregistrement dans le code du controleur
@RestController
@RequestMapping({“/”})
public class LoginController {
@Autowired
private UserService userService;
@PostMapping(“/registration”)
public User createProvider(@Valid @RequestBody User user) {
return userService.saveUser(user);
}
}