(Illustrations récupérées sur le site bezkoder.com)
security.jwt.token.secret-key=03888dd6ceb88c3fee89570802fb93d483fd52d70349d8f7e7581ae346cf658 // security.jwt.token.secret=laposte-promo5 # + les infos pour hibernate ou autres pour se connecter à la base de données
Pensez à ajouter les dépendances pour JWT et la sécurité avec Spring Boot :
compile group: 'io.jsonwebtoken', name: 'jjwt', version: '0.9.0' implementation 'org.springframework.boot:spring-boot-starter-security'
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency>
La structure d’API REST construite avec Spring boot est sécurisée avec Spring Security et JWT.
L’API permet de :
Quelques roles ont été définis dans l’API :
ROLE_ADMIN
ROLE_CREATOR
ROLE_READER
Les accès aux fonctionnalités sont définis de la manière suivante :
Les accès aux pages sont définis de la manière suivante :
En plus des EndPoints relatifs au CRUD pour les Films et les Catégories, nous avons besoin d’enregistrer un nouvel utilisateur, nous connecter et récupérer un jeton (JWT) pour une authentification réussie.
Spring Security est un framework de sécurité léger qui fournit une authentification et un support d’autorisation. Il est livré avec des implémentations d’algorithmes.
Il permet de sécuriser l’interaction entre une application et des utilisateurs
Il y a un Servlet Dispatcher autrement dit un répartiteur de servlet qui attrape les requêtes et les distribue aux différents controlleurs. Ce sont les fameux Servlets Filters ou Filter Chain, une chaîne de filtre qui effectue les tâches suivantes :
Pour information, voici les filtres de la filter chain de Spring Security :
Ne vous inquiétez pas ! Spring fait le job, pas besoin de connaître toutes ces filtres !
Il existe 3 scénarios :
Cette interface que nous implémentons en créant la classe UserDetailsServiceImpl nous permet d’implémenter la méthode loadUserByUsername() :
@Override public UserDetails loadUserByUsername(String username) { }
Pourquoi avons-nous ajouter findByUsername(username) dans notre UserRepository ?
Tout simplement parce-que la méthode loadUserByUsername(String username) en a besoin pour récupérer un uttilisateur à partir de son identifiant.
Si vous jetez un oeil dans le corps de la méthode ci-dessus, vous allez découvrir ce code :
User.withUsername(username) .password(user.get().getPassword()) .authorities(user.get().getRoleList())
le paramètre authorities() permet d’ajouter une collection de rôles qui héritent de GrantedAuthoritiy à notre objet user de type UserDetails. Il en existe d’autres comme LdapAuthority, SwitchUserGrantedAuthority ou OAuth2UserAuthority. Bref, tout dépend des besoins.
Remarque : les rôles ne peuvent pas être nuls.
Finalement, la récupération des chaînes de caractères depuis la base de données et leur identification auprès de Spring Security en tant qu’autorisations est généralement effectuée dans le userDetailsService :
Une Authority n’est qu’une simple chaîne de caractères qui désigne une responsabilité comme READER, ADMIN, BIGBOSS,… On met ce que l’on veut !
Un Role est une Authority précédé du mot clef ROLE_.
En fait, ça veut dire la même chose !! On va dire que le Role est une convention.
Indiquer à Spring Security quelles sont les URLs à protéger et avec quelles restrictions. Certaines URLs pourront êtres accessibles à tout le monde (connecté ou pas). Certaines fonctionnalités ne seront visibles que pour les utilisateurs connectés. Certaines Fonctionnalités (ou pages) ne seront de surcroît accessibles qu’aux utilisateurs disposant d’autorisations spécifiques.
Dans la notre configure(HttpSecurity), le choix des protections derrière lesquelles sécuriser nos URLs s’effectue par le biais du DSL (Domain Specific Language) de Spring Security qui s’avère aisé à mettre en oeuvre et transparent.
Voici les principaux éléments pour configurer vos urls :
Elles sont définies par des antMatchers().
Un antMatcher permet de désigner un pattern selon les règles principales suivantes :
? = n’importe quel caractère non nul
* = n’importe quel caractère (possiblement nul)
** = n’importe que nombre de répertoires dans le chemin de l’URL
{nomVariable:[a-z]+} = la regex [a-z]+, stockée dans une variable nomVariable.
permitAll() : l’URL est accessible sans authentification nécessaire (souvent, seule la page d’authentification est accessible à tout le monde).
anyRequest : définit le comportement par défaut de toute URL non précisée précédemment.
and() : la configuration des requêtes est terminée, mais nous souhaitons configurer d’autres aspects de Spring Security.
hasRole(“ADMIN”) : l’URL est accessible à un utilisateur authentifié qui a le rôle ADMIN, correspondant à la chaîne de caractères “ROLE_ADMIN”. Le contrôle hasRole(“ADMIN”) est strictement équivalent au contrôle hasAuthority(“ROLE_ADMIN”).
hasAuthority(“JE_SUIS_BIENTOT_ADMIN”) : l’URL est accessible à un utilisateur authentifié qui bénéficie de l’autorisation JE_SUIS_BIENTOT_ADMIN, correspondant à la chaîne de caractères “JE_SUIS_BIENTOT_ADMIN”.
hasAnyAuthority(“ROLE_ADMIN”, “JE_SUIS_PRESQUE-ADMIN”) : l’URL est accessible à un utilisateur authentifié qui bénéficie d’au moins l’une des 2 autorisations suivantes : “JE_SUIS_BIENTOT_ADMIN” (chaîne de caractères “JE_SUIS_BIENTOT_ADMIN”), ou ROLE_ADMIN (chaîne de caractères “ROLE_ADMIN”). A noter que le contrôle hasAuthority(“ROLE_ADMIN”) est strictement équivalent au contrôle hasRole(“ADMIN”).
authenticated() : l’URL est accessible à n’importe quel utilisateur authentifié, indépendamment de ses rôles/autorisations.
formLogin() : l’authentification est autorisée via le formulaire de login dans le cas ou on utilise celui par défaut de Spring Security.
httpBasic() : l’authentification est autorisée via un header BasicAuth.
antMatchers("/admin/**") .access("hasRole('ADMIN') and hasIpAddress('192.168.1.10/20')")
Ci-dessus, Spring Security vérifie que le user est authentifié avec le rôle ADMIN et que la requête vient d’une adresse IP comprise entre 192.168.1.10 et 192.168.1.20.
Pour renforcer la sécurité au-delà des pages web et urls, il existe un moyen de contrôler l’accès aux méthodes que l’on peut trouver :
Cette approche est mise en place par le biais d’annotations affectant les méthodes publiques des beans. Pour rendre possible cette sécurité supplémentaire, il est nécessaire de l’autoriser explicitement dans la classe annotée @Configuration que vous avez découvert dans Cinema-back-api en ajoutant l’annotation @EnableGlobalMethodSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
Explications :
Certaines requêtes modifient l’état de l’application.
Remarque : Dans le cadre d’une application reposant sur une API Rest et un client Angular ou JS, le paramétrage csrf est plus délicat : le jeton garantissant l’authenticité de la requête est fournit par l’API et doit être conservé (et envoyé avec chaque requête) dans le header vers le client Angular ou JS.
Pour le TP qui va suivre, il faut penser à ajouter les dépendances suivantes dans votre build.gradle pour faciliter le développement en intégrant les packages de sécurité SpringBoot et d’autres bien pratiques.
dependencies { implementation group: 'io.jsonwebtoken', name: 'jjwt', version: '0.9.1' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-security' developmentOnly 'org.springframework.boot:spring-boot-devtools' runtimeOnly 'mysql:mysql-connector-java' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' }
Projet Client Back-api-JWT à récupérer
implementation group: 'io.jsonwebtoken', name: 'jjwt', version: '0.9.1' implementation 'org.springframework.boot:spring-boot-starter-security' testImplementation 'org.springframework.security:spring-security-test'
Ajouter un User (comme le AppUser dans l’appli Cinéma), vous pouvez le personnaliser.
Ajouter un contrôleur avec les méthodes sign-in et sing-up pour se connecter et s’enregistrer.
Créer les packages manquant et ajouter les classes indispensables.
cinema-back-api-p5.zip exemple
Cette correction ne concerne que la partie Sécurité de l’applicaltion.
import java.util.List; import javax.persistence.Column; import javax.persistence.ElementCollection; import javax.persistence.Entity; import javax.persistence.EnumType; import javax.persistence.Enumerated; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.Table; import com.sun.istack.NotNull; /** * AppUser entity */ @Entity @Table(name="user") public class AppUser { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @NotNull @Column(nullable = false) private String username; @NotNull @Column(nullable = false) private String password; @ElementCollection(fetch = FetchType.EAGER) @Enumerated(EnumType.STRING) private List<Role> roleList; // pour un besoin ultérieur d'oubli ou de modification de mot de passe (optionnel) @Column(name = "resetPasswordToken") private String resetPasswordToken; public AppUser() {} public AppUser(@NotNull String username, @NotNull String password) { this.username = username; this.password = password; } public AppUser(@NotNull String username, @NotNull String password, List<Role> roleList) { this.username = username; this.password = password; this.roleList = roleList; } public Long getId() { return id; } public String getUsername() { return username; } public String getPassword() { return password; } public List<Role> getRoleList() { return roleList; } public void setId(Long id) { this.id = id; } public void setUsername(String username) { this.username = username; } public void setPassword(String password) { this.password = password; } public void setRoleList(List<Role> roleList) { this.roleList = roleList; } public String getResetPasswordToken() { return resetPasswordToken; } public void setResetPasswordToken(String resetPasswordToken) { this.resetPasswordToken = resetPasswordToken; } }
Cela va créer une table pour enregistrer chaque rôle associé à un AppUser. On pourrait le faire différemment en créant une table rôle depuis une Entity. Ici, on simplifie au maximum avec 3 rôles.
Particularité : on implémente l’interface GrantedAuthority. Cela nous oblige a implémenter la méthode getAuthority() pour obtenir les la liste des rôles pour un utilisation donné.
import org.springframework.security.core.GrantedAuthority; /** * Les 3 rôles possibles pour un AppUser. */ public enum Role implements GrantedAuthority { ROLE_ADMIN, ROLE_CREATOR, ROLE_READER; @Override public String getAuthority() { return name(); } }
/** * Specifique : AppUser DTO permet de renvoyer un User sans le mot de passe (REST response). */ public class AppUserDto { private Long id; private String username; private List<Role> roleList; public AppUserDto() { } public AppUserDto(@NotNull String username) { this(username,null); } public AppUserDto(@NotNull String username, List<Role> roleList) { this.username = username; this.roleList = roleList; } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public List<Role> getRoleList() { return roleList; } public void setRoleList(List<Role> roleList) { this.roleList = roleList; } }
Pour nous permettre d’effectuer des actions sur notre table User de notre BD.
public interface AppUserRepository extends JpaRepository<AppUser, Long> { Optional<AppUser> findByUsername(String username); boolean existsByUsername(String username); void deleteByUsername(String username); Optional<AppUser> findByResetPasswordToken(String token); }
On pourrait s’en passer mais c’est préférable d’ajouter une couche service pour la partie maintenance du code. Il suffit d’ajouter l’annotation @Service à notre interface.
@Service public interface AppUserService { /** * Methode qui permet à un utilisateur de se connecter. * @param username : nom de l'utilisateur. * @param password : mot de passe de l'utilisateur. * @returnun JWT si credentials est valide, throws InvalidCredentialsException otherwise. * @throws InvalidCredentialsException */ String signin(String username, String password) throws InvalidCredentialsException; /** * Methode qui permet de s'inscrire. * @param user nouvel utilisateur. * @return un JWT si user n'existe pas déjà ! * @throws ExistingUsernameException */ String signup(AppUser user) throws ExistingUsernameException; /** * Methode qui retourne tous les utilisateurs de la bd * @return the list of all application users. */ List<AppUser> findAllUsers(); /** * Methode qui retourne un utilisateur à partir de son username * @param username the username to look for. * @return an Optional object containing user if found, empty otherwise. */ Optional<AppUser> findUserByUserName(String username); /** * Methode qui retourne un utilisateur à partir de son username * @param username nom à effacer dans la base * @return une chaîne correspondant à une réponse si ok ou ko * @throws NotExistingUsernameException */ String deleteByUserName(String username) throws NotExistingUsernameException; /** * Méthode pour retourner un utilisateur en fonction du token * @param token * @return un utilisateur */ public Optional<AppUser> findByResetPasswordToken(String token); }
La partie implémentation de l’interface toujours avec l’annotation @Service :
@Service public class AppUserServiceImpl implements AppUserService { @Autowired private AppUserRepository appUserRepository; // permet communication avec la BD @Autowired private BCryptPasswordEncoder passwordEncoder; // permet l'encodage du mot de passe @Autowired private JwtTokenProvider jwtTokenProvider; // permet la fourniture du Jeton (Token) @Autowired private AuthenticationManager authenticationManager; // gestionnaire d'authentification /** * Permet de se connecter en encodant le mot de passe avec génération du token. */ @Override public String signin(String username, String password) throws InvalidCredentialsException { try { authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password)); return jwtTokenProvider.createToken(username, appUserRepository.findByUsername(username).get().getRoleList()); } catch (AuthenticationException e) { throw new InvalidCredentialsException(); } } @Override public String signup(AppUser user) throws ExistingUsernameException { if (!appUserRepository.existsByUsername(user.getUsername())) { AppUser userToSave = new AppUser(user.getUsername(), passwordEncoder.encode(user.getPassword()), user.getRoleList()); appUserRepository.save(userToSave); return jwtTokenProvider.createToken(user.getUsername(), user.getRoleList()); } else { throw new ExistingUsernameException(); } } @Override public List<AppUser> findAllUsers() { return appUserRepository.findAll(); } @Override public Optional<AppUser> findUserByUserName(String username) { return appUserRepository.findByUsername(username); } @Override public String deleteByUserName(String username) throws NotExistingUsernameException { if (appUserRepository.existsByUsername(username)) { appUserRepository.deleteByUsername(username); return "Utilisateur effacé !"; } else { throw new NotExistingUsernameException(); } } @Override public Optional<AppUser> findByResetPasswordToken(String token) { return null; } /** * Optionnelle pour utilisation ultérieure * **/ public Optional<AppUser> getByResetPasswordToken(String token) { return appUserRepository.findByResetPasswordToken(token); } public void updatePassword(AppUser appUser, String newPassword) { appUser.setPassword(passwordEncoder.encode(newPassword)); appUser.setResetPasswordToken(null); appUserRepository.save(appUser); } }
Cette implémentation nous oblige à implémenter la méthode loadUserByUsername().
@Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private AppUserRepository userRepository; @Override public UserDetails loadUserByUsername(String username) { final Optional<AppUser> user = userRepository.findByUsername(username); if (!user.isPresent()) { throw new UsernameNotFoundException("utilisateur '" + username + "' introuvable"); } return User .withUsername(username) .password(user.get().getPassword()) .authorities(user.get().getRoleList()) .build(); // cette dernière méthode permet la construction d'un objet UserDetails } }
Classe avec une seule méthode pour renvoyer une chaîne de caractère qui correspond au token (et rien d’autre comme une classe DTO). Vous pouvez la nommez comme bon vous semble !
public class JsonWebToken { private final String token; public JsonWebToken(String token) { this.token = token; } public String getToken() { return token; } }
Cette classe utilitaire est chargée de fournir le Jeton (Token) et les vérifications. On peut lui mettre l’annotation @Component.
@Component public class JwtTokenProvider { // on récupère le secret dans notre fichier application.properties // on peut aussi le mettre en dur ici genre "monMotSecret123456789" @Value("${security.jwt.token.secret-key:secret-key}") private String secretKey; // ici on met la valeur par défaut de la durée du jeton @Value("${security.jwt.token.expire-length:3600000}") private long validityInMilliseconds = 3600000; // 1h pour être pénard @Autowired private UserDetailsService userDetailsService; /** * Cette méthode d'initialisation s'exécute avant le constructeur * Elle encode notre code secret en base64 pour la transmission dans le header. */ @PostConstruct protected void init() { secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes()); } /** * @param username le username. * @param roles les roles. * @return une chaîne de caractères du JWT créé. * @throws JsonProcessingException */ public String createToken(String username, List<Role> roles){ Claims claims = Jwts.claims().setSubject(username); claims.put("auth", roles.stream().map(s -> new SimpleGrantedAuthority(s.getAuthority())).filter(Objects::nonNull).collect(Collectors.toList())); System.out.println("claims = "+claims); // claims = {sub=pbouget, auth=[ROLE_ADMIN, ROLE_CREATOR, ROLE_READER]} Date now = new Date(); Date validity = new Date(now.getTime() + validityInMilliseconds); String leToken = Jwts.builder()// .setClaims(claims) // le username avec les roles ou setPayload() .setIssuedAt(now) // 1589817421 pour le 18 mai 2020 à 17 heure 57 .setExpiration(validity) // 1589821021 même date avec 1 heure de plus .signWith(SignatureAlgorithm.HS256, secretKey) // la signature avec la clef secrête. .compact(); // concatène l'ensemble pour construire une chaîne return leToken; } /** * Methode qui retourne un objet Authentication basé sur JWT. * @param token : le token pour l'authentification. * @return l'objet Authentication si le Username est trouvé. */ public Authentication getAuthentication(String token) { UserDetails userDetails = userDetailsService.loadUserByUsername(getUsername(token)); return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities()); } /** * Methode qui extrait le userName (le sub ou subject) du JWT. * @param token : Token à analyser. * @return le UserName comme chaîne de caractères. */ public String getUsername(String token) { return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject(); } /** * Méthode qui récupère la requete HTTP et retourne une chaîne. * L'entête doit contenir un champ d'autorisation ou JWT ajoute le token après le mot clef Bearer. * @param requete : la requête à tester. * @return le JWT depuis l'entête HTTP. */ public String resolveToken(HttpServletRequest requeteHttp) { String bearerToken = requeteHttp.getHeader("Authorization"); if (bearerToken != null && bearerToken.startsWith("Bearer ")) { return bearerToken.substring(7); } return null; } /** * Methode qui vérifie la validité du token. * La signature doit être correcte et la durée de validité du Token doit être après "now" (maintenant) * @param token : Token à valider * @return True si le Token est valide sinon on lance l'exception InvalidJWTException. * @throws InvalidJWTException */ public boolean validateToken(String token) throws InvalidJWTException { try { Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token); return true; } catch (JwtException | IllegalArgumentException e) { throw new InvalidJWTException(); } } }
Filtre specifique en charge d’analyser la requête HTTP qui arrive vers notre Serveur et qui doit contenir un JWT valide. Ce filtre ne sera exécuté qu’une seule fois.
public class JwtTokenFilter extends OncePerRequestFilter { private JwtTokenProvider jwtTokenProvider; public JwtTokenFilter(JwtTokenProvider jwtTokenProvider) { this.jwtTokenProvider = jwtTokenProvider; } @Override protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException { String token = jwtTokenProvider.resolveToken(httpServletRequest); try { if (token != null && jwtTokenProvider.validateToken(token)) { Authentication auth = jwtTokenProvider.getAuthentication(token); SecurityContextHolder.getContext().setAuthentication(auth); } } catch (InvalidJWTException ex) { // permet de garantir que le AppUser n'est pas authentifié SecurityContextHolder.clearContext(); httpServletResponse.sendError(HttpStatus.BAD_REQUEST.value(), "JWT invalide !"); return; } filterChain.doFilter(httpServletRequest, httpServletResponse); } }
Classe qui permet de configurer la sécurité plutôt que de laisser faire Spring (avant Spring Boot 2.7).
/** * Configuration de Sécurité globale pour notre REST API. * L'annotation @Configuration permet d'être scannée au démarrage de l'application */ @Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private JwtTokenProvider jwtTokenProvider; @Bean(name = BeanIds.AUTHENTICATION_MANAGER) @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } /** * Methode qui configure la sécurité HTTP. Version simplifiée * @param http HttpSecurity object to configure. * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { http.cors(); http.csrf().disable() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/client/**").permitAll() // accessible sans besoin de s'authentifier .antMatchers("/api/user/sign-in").permitAll() // se connecter .antMatchers("/api/user/sign-up").permitAll() // s'inscrire et accessible sans besoin de s'authentifier .antMatchers("api/user/all").hasAuthority("ROLE_ADMIN") // uniquement pour le rôle admin .anyRequest().authenticated(); // tout le reste est autorisé par un utilisateur authentifié // Appliquer un filtre avec le token pour toutes requêtes HTTP http.addFilterBefore(new JwtTokenFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class); } /** * Methode qui configure la sécurité web. * Pas besoin d'autorisation pour les fichiers dans resources * @param web : WebSecurity * @throws Exception */ @Override public void configure(WebSecurity websecurity) throws Exception { websecurity.ignoring().antMatchers("/resources/**"); } }
Classe qui permet de configurer la sécurité plutôt que de laisser faire Spring (après Spring Boot 2.7).
/** * Configuration de Sécurité globale pour notre REST API. * L'annotation @Configuration permet d'être scannée au démarrage de l'application * L'annotation @EnableGlobalMethodSecurity(prePostEnabled = true) permet d'autoriser les méthodes à gérer les autorisations */ @Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfig { @Autowired UserDetailsServiceImpl userDetailsService; @Autowired private AuthEntryPointJwt unauthorizedHandler; @Bean public AuthTokenFilter authenticationJwtTokenFilter() { return new AuthTokenFilter(); } @Bean public DaoAuthenticationProvider authenticationProvider() { DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); authProvider.setUserDetailsService(userDetailsService); authProvider.setPasswordEncoder(passwordEncoder()); return authProvider; } @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception { return authConfig.getAuthenticationManager(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.cors().and().csrf().disable() .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() .authorizeRequests() .antMatchers("/client/**").permitAll() // accessible sans besoin de s'authentifier .antMatchers("/api/user/sign-in").permitAll() // se connecter .antMatchers("/api/user/sign-up").permitAll() // s'inscrire et accessible sans besoin de s'authentifier .antMatchers("api/user/all").hasAuthority("ROLE_ADMIN") // uniquement pour le rôle admin .anyRequest().authenticated(); // tout le reste est autorisé par un utilisateur authentifié http.authenticationProvider(authenticationProvider()); http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class); return http.build(); } }
Cela va vous permettre d’afficher des messages compréhensibles.
Voici la liste des noms des classes d’exceptions personnalisées :
Je vous laisse les écrire…
pour pouvoir gérer l’inscription, la connection et d’autres actions propre à l’utilisateur…
@RestController @RequestMapping("/api/user") public class AppUserController { @Autowired private AppUserService appUserService; /** * Methode pour enregistrer un nouvel utilisateur dans la BD. * @param user utiliateur. * @return un JWT si la connection est OK sinon une mauvaise réponse */ @PostMapping("/sign-up") public ResponseEntity<JsonWebToken> signUp(@RequestBody AppUser user) { try { return ResponseEntity.ok(new JsonWebToken(appUserService.signup(user))); } catch (ExistingUsernameException ex) { return ResponseEntity.badRequest().build(); } } /** * Methode pour se connecter (le user existe déjà). * @param user : utilisateur qui doit se connecter. * @return un JWT si la connection est OK sinon une mauvaise réponse. */ @PostMapping("/sign-in") public ResponseEntity<JsonWebToken> signIn(@RequestBody AppUser user) { try { // ici on créé un JWT en passant l'identifiant et le mot de passe // récupéré de l'objet user passé en paramètre. return ResponseEntity.ok(new JsonWebToken(appUserService.signin(user.getUsername(), user.getPassword()))); } catch (InvalidCredentialsException ex) { // on renvoie une réponse négative return ResponseEntity.badRequest().build(); } } /** * Methode pour retourner tous les utilisateurs de la BD. * Cette méthode est accesible pour les utilisateurs ayant le rôle ROLE_ADMIN. * @return liste de tous les utilisateurs enregistrés en BD. */ @GetMapping("/all") @PreAuthorize("hasRole('ROLE_ADMIN')") public List<AppUser> getAllUsers() { return appUserService.findAllUsers(); } /** * Permet de retourner les infos sans les mots de passe en passant par DTO * C'est mieux et plus sécurisant ! * @return */ @GetMapping("/admin/all") @PreAuthorize("hasRole('ROLE_ADMIN')") public List<AppUserDto> getAllAdminUsers() { return appUserService.findAllUsers().stream().map(appUser -> new AppUserDto(appUser.getUsername(), appUser.getRoleList())).collect(Collectors.toList()); } @DeleteMapping("/delete/{appUserName}") @PreAuthorize("hasRole('ROLE_ADMIN')") public ResponseEntity<?> deleteUser(@PathVariable String appUserName) { try { return ResponseEntity.ok(appUserService.deleteByUserName(appUserName)); } catch (NotExistingUsernameException ex) { return ResponseEntity.ok(ex.getMessage()); } } /** * Methode pour récupérer le user dans la bd à partir de son username. * Cette méthode est uniquement accessible par un user Admin. * @param appUserName à chercher. * @return un User est trouvé sinon réponse non trouvée. */ @GetMapping("/{appUserName}") @PreAuthorize("hasRole('ROLE_ADMIN')") public ResponseEntity<AppUserDto> getOneUser(@PathVariable String appUserName) { Optional<AppUser> appUser = appUserService.findUserByUserName(appUserName); if (appUser.isPresent()) { return ResponseEntity.ok(new AppUserDto(appUser.get().getUsername(), appUser.get().getRoleList())); } else { return ResponseEntity.notFound().build(); } } }
ajouter un bean pour l’encodage. Vous pouvez le mettre dans votre classe de lancement de SpringBoot ou ailleurs
/** * Ceci est un Bean, un composant * Méthode de Hachage * Bcrypt est un algorithme de hachage considéré comme le plus sûr. * Bcrypt est un algorithme de hashage unidirectionnel, * vous ne pourrez jamais retrouver le mot de passe sans connaitre à la fois le grain de sel, * la clé et les différentes passes que l'algorithme à utiliser. * Voir le <a href="https://bcrypt-generator.com/"> site pour effectuer un test</a> * @return */ @Bean public BCryptPasswordEncoder bCryptPasswordEncoder() { return new BCryptPasswordEncoder(); }
Il vous reste à ajouter 2 lignes dans votre fichier applications.properties.
security.jwt.token.secret-key=LaPoste-The-Best-Promo5 spring.main.allow-circular-references=true
Vous pouvez utiliser ces classes pour implémenter la partie sécurité de n’importe quelle application spring boot en ajoutant vos classes contrôleurs, service, repository, model et autres spécifiques à votre application. Vous pouvez aussi modifier les codes des différentes classes selon vos besoins. Tout réécrire n’est pas forcément indispensable, par contre, bien prendre le temps de lire le code, regarder la documentation de Spring Security.
https://docs.spring.io/spring-security/site/docs/5.4.6/reference/html5/
Généralités sur le métier de développeur.euse en anglais : Pragmatic-Programmer-journey-mastery-Anniversary
Livre de bonnes pratiques Java (avancé) : Effective-Java-
Java (mon préféré !) : baeldung
Conventions de nommage Rest : https://restfulapi.net/resource-naming/
JSON : https://fr.wikipedia.org/wiki/JavaScript_Object_Notation
Conventions de nommage Java : https://www.oracle.com/java/technologies/javase/codeconventions-namingconventions.html