From c8d9cd4db88d54fc887a49c5e832fadecab7552c Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 28 Feb 2022 14:10:36 +0100 Subject: [PATCH] secu --- build.gradle | 13 +- settings.gradle | 2 +- .../cardon/simpleat/SimpleatApplication.java | 20 ++ .../controller/PersonneController.java | 36 ++++ .../controller/RestaurantController.java | 3 + .../fr/cardon/simpleat/dto/JsonWebToken.java | 21 ++ .../fr/cardon/simpleat/dto/PersonneDto.java | 55 ++++++ .../exception/ExistingUsernameException.java | 15 ++ .../InvalidCredentialsException.java | 15 ++ .../exception/InvalidJWTException.java | 16 ++ .../fr/cardon/simpleat/model/EnumRole.java | 14 ++ .../fr/cardon/simpleat/model/Personne.java | 46 ++++- .../repository/PersonneRepository.java | 8 + .../simpleat/security/JwtTokenFilter.java | 47 +++++ .../simpleat/security/JwtTokenProvider.java | 184 ++++++++++++++++++ .../simpleat/security/WebSecurityConfig.java | 78 ++++++++ .../simpleat/service/PersonneService.java | 48 +++++ .../simpleat/service/PersonneServiceImpl.java | 74 +++++++ .../service/UserDetailsServiceImpl.java | 42 ++++ src/main/resources/application.properties | 12 +- src/main/resources/data.sql | 44 +++-- 21 files changed, 764 insertions(+), 29 deletions(-) create mode 100644 src/main/java/fr/cardon/simpleat/dto/JsonWebToken.java create mode 100644 src/main/java/fr/cardon/simpleat/dto/PersonneDto.java create mode 100644 src/main/java/fr/cardon/simpleat/exception/ExistingUsernameException.java create mode 100644 src/main/java/fr/cardon/simpleat/exception/InvalidCredentialsException.java create mode 100644 src/main/java/fr/cardon/simpleat/exception/InvalidJWTException.java create mode 100644 src/main/java/fr/cardon/simpleat/model/EnumRole.java create mode 100644 src/main/java/fr/cardon/simpleat/security/JwtTokenFilter.java create mode 100644 src/main/java/fr/cardon/simpleat/security/JwtTokenProvider.java create mode 100644 src/main/java/fr/cardon/simpleat/security/WebSecurityConfig.java create mode 100644 src/main/java/fr/cardon/simpleat/service/PersonneService.java create mode 100644 src/main/java/fr/cardon/simpleat/service/PersonneServiceImpl.java create mode 100644 src/main/java/fr/cardon/simpleat/service/UserDetailsServiceImpl.java diff --git a/build.gradle b/build.gradle index 1474fba..2570a80 100644 --- a/build.gradle +++ b/build.gradle @@ -13,11 +13,14 @@ repositories { } dependencies { - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - implementation 'org.springframework.boot:spring-boot-starter-web' - runtimeOnly 'mysql:mysql-connector-java' - testImplementation 'org.springframework.boot:spring-boot-starter-test' - developmentOnly 'org.springframework.boot:spring-boot-devtools' + implementation group: 'io.jsonwebtoken', name: 'jjwt', version: '0.9.0' + 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' } test { diff --git a/settings.gradle b/settings.gradle index c74741c..e76d701 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -rootProject.name = 'simpleat' +rootProject.name = 'simpleat-back' diff --git a/src/main/java/fr/cardon/simpleat/SimpleatApplication.java b/src/main/java/fr/cardon/simpleat/SimpleatApplication.java index ebaf5a7..37dea51 100644 --- a/src/main/java/fr/cardon/simpleat/SimpleatApplication.java +++ b/src/main/java/fr/cardon/simpleat/SimpleatApplication.java @@ -1,13 +1,33 @@ package fr.cardon.simpleat; +import java.util.ArrayList; +import java.util.Arrays; + import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +import fr.cardon.simpleat.exception.ExistingUsernameException; +import fr.cardon.simpleat.model.EnumRole; +import fr.cardon.simpleat.model.Personne; +import fr.cardon.simpleat.service.PersonneService; @SpringBootApplication public class SimpleatApplication { + + public static void main(String[] args) { SpringApplication.run(SimpleatApplication.class, args); } + @Bean + public BCryptPasswordEncoder bCryptPasswordEncoder() { + return new BCryptPasswordEncoder(); + } + + + + } diff --git a/src/main/java/fr/cardon/simpleat/controller/PersonneController.java b/src/main/java/fr/cardon/simpleat/controller/PersonneController.java index 57c1eb4..360e75d 100644 --- a/src/main/java/fr/cardon/simpleat/controller/PersonneController.java +++ b/src/main/java/fr/cardon/simpleat/controller/PersonneController.java @@ -5,6 +5,7 @@ import java.util.Collection; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -12,13 +13,18 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; +import fr.cardon.simpleat.dto.JsonWebToken; +import fr.cardon.simpleat.exception.ExistingUsernameException; +import fr.cardon.simpleat.exception.InvalidCredentialsException; import fr.cardon.simpleat.model.Personne; import fr.cardon.simpleat.model.Role; import fr.cardon.simpleat.repository.PersonneRepository; import fr.cardon.simpleat.repository.RoleRepository; +import fr.cardon.simpleat.service.PersonneService; @RestController @CrossOrigin("*") @@ -31,6 +37,11 @@ public class PersonneController { @Autowired private RoleRepository roleRepository; + @Autowired + private PersonneService personneService; + + + @GetMapping("/") @ResponseBody public String home(){ @@ -57,18 +68,21 @@ public class PersonneController { @GetMapping("/users") + @PreAuthorize("hasRole('ROLE_ADMIN')") public Collection findAll(){ return personneRepository.findAll(); } @GetMapping("/user/{id}") + @PreAuthorize("hasRole('ROLE_ADMIN')") public Personne findPersonneById(@PathVariable int id){ return personneRepository.findById(id); } @PostMapping("/add-user") + @PreAuthorize("hasRole('ROLE_ADMIN')") public ResponseEntity ajoutPersonne(@RequestBody Personne personne){ return ResponseEntity.status(HttpStatus.OK).body(personneRepository.save(personne)); } @@ -99,6 +113,28 @@ public class PersonneController { return roleRepository.findCollectionById(idRole); } + @PostMapping("/sign-in") + public ResponseEntity signIn(@RequestBody Personne personne) { + try { + // ici on créé un JWT en passant l'email et le mot de passe + // récupéré de l'objet user passé en paramètre. + return ResponseEntity.ok(new JsonWebToken(personneService.signin(personne.getEmail(), personne.getPassword()))); + } catch (InvalidCredentialsException ex) { + // on renvoie une réponse négative + return ResponseEntity.badRequest().build(); + } + } + + @PostMapping("/sign-up") + public ResponseEntity signUp(@RequestBody Personne personne) { + try { + return ResponseEntity.ok(new JsonWebToken(personneService.signup(personne))); + } catch (ExistingUsernameException ex) { + return ResponseEntity.badRequest().build(); + } + } + + // public Personne findById(int id) { // return personneRepository.getById(id); // } diff --git a/src/main/java/fr/cardon/simpleat/controller/RestaurantController.java b/src/main/java/fr/cardon/simpleat/controller/RestaurantController.java index a5653ae..be17291 100644 --- a/src/main/java/fr/cardon/simpleat/controller/RestaurantController.java +++ b/src/main/java/fr/cardon/simpleat/controller/RestaurantController.java @@ -5,6 +5,7 @@ import java.util.Collection; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -37,12 +38,14 @@ public class RestaurantController { } @PostMapping("/add-restaurant") + @PreAuthorize("hasRole('ROLE_ADMIN')") public ResponseEntity ajoutRestaurant(@RequestBody Restaurant personne){ return ResponseEntity.status(HttpStatus.OK).body(restaurantRepository.save(personne)); } @PutMapping(value = "/update-restaurant/{id}") + public ResponseEntity modifRestaurant(@PathVariable int id, @RequestBody Restaurant personne){ return ResponseEntity.status(HttpStatus.OK).body(restaurantRepository.save(personne)); } diff --git a/src/main/java/fr/cardon/simpleat/dto/JsonWebToken.java b/src/main/java/fr/cardon/simpleat/dto/JsonWebToken.java new file mode 100644 index 0000000..021837a --- /dev/null +++ b/src/main/java/fr/cardon/simpleat/dto/JsonWebToken.java @@ -0,0 +1,21 @@ +package fr.cardon.simpleat.dto; + +/** + * Classe spécifique DTO (Data Transfer Object) qui retourne un Jeton au format JSON (REST response) + * + */ + +public class JsonWebToken { + + private final String token; + + public JsonWebToken(String token) { + this.token = token; + } + + public String getToken() { + return token; + } +} + + diff --git a/src/main/java/fr/cardon/simpleat/dto/PersonneDto.java b/src/main/java/fr/cardon/simpleat/dto/PersonneDto.java new file mode 100644 index 0000000..c078b59 --- /dev/null +++ b/src/main/java/fr/cardon/simpleat/dto/PersonneDto.java @@ -0,0 +1,55 @@ +package fr.cardon.simpleat.dto; + +import java.util.List; + + +import com.sun.istack.NotNull; + +import fr.cardon.simpleat.model.Role; + +/** + * Specifique : AppUser DTO permet de renvoyer un User sans le mot de passe (REST response). + */ +public class PersonneDto { + + private Long id; + private String email; + private List roleList; + + public PersonneDto() { } + + public PersonneDto(@NotNull String email) { + this(email,null); + } + + public PersonneDto(@NotNull String email, List roleList) { + this.email = email; + this.roleList = roleList; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public List getRoleList() { + return roleList; + } + + public void setRoleList(List roleList) { + this.roleList = roleList; + } + + +} diff --git a/src/main/java/fr/cardon/simpleat/exception/ExistingUsernameException.java b/src/main/java/fr/cardon/simpleat/exception/ExistingUsernameException.java new file mode 100644 index 0000000..4ca7c65 --- /dev/null +++ b/src/main/java/fr/cardon/simpleat/exception/ExistingUsernameException.java @@ -0,0 +1,15 @@ +package fr.cardon.simpleat.exception; + +/** + * Classe personnalisée pour gérer un message si l'utilisateur (User) existe en Base de données + */ +public class ExistingUsernameException extends Exception { + + private static final long serialVersionUID = 1L; + + @Override + public String getMessage() + { + return "Désolé, l'utilisateur existe déjà en Base de données !"; + } +} diff --git a/src/main/java/fr/cardon/simpleat/exception/InvalidCredentialsException.java b/src/main/java/fr/cardon/simpleat/exception/InvalidCredentialsException.java new file mode 100644 index 0000000..82cf0cc --- /dev/null +++ b/src/main/java/fr/cardon/simpleat/exception/InvalidCredentialsException.java @@ -0,0 +1,15 @@ +package fr.cardon.simpleat.exception; + +/** + * Specific exception that should be thrown when user credentials are not valid. + */ +public class InvalidCredentialsException extends Exception { + + private static final long serialVersionUID = -6483691380297851921L; + + @Override + public String getMessage() + { + return "L'accréditation est invalide !"; + } +} diff --git a/src/main/java/fr/cardon/simpleat/exception/InvalidJWTException.java b/src/main/java/fr/cardon/simpleat/exception/InvalidJWTException.java new file mode 100644 index 0000000..82036fc --- /dev/null +++ b/src/main/java/fr/cardon/simpleat/exception/InvalidJWTException.java @@ -0,0 +1,16 @@ +package fr.cardon.simpleat.exception; + +/** + * Specific exception that should be thrown when a JWT has an invalid format. + */ +public class InvalidJWTException extends Exception { + + private static final long serialVersionUID = -6546999838071338632L; + + @Override + public String getMessage() + { + return "Le format JWT est invalide !"; + } + +} diff --git a/src/main/java/fr/cardon/simpleat/model/EnumRole.java b/src/main/java/fr/cardon/simpleat/model/EnumRole.java new file mode 100644 index 0000000..29efe61 --- /dev/null +++ b/src/main/java/fr/cardon/simpleat/model/EnumRole.java @@ -0,0 +1,14 @@ +package fr.cardon.simpleat.model; + +import org.springframework.security.core.GrantedAuthority; + +public enum EnumRole implements GrantedAuthority { + + ROLE_ADMIN, ROLE_CREATOR, ROLE_READER; + + @Override + public String getAuthority() { + return name(); + } + +} diff --git a/src/main/java/fr/cardon/simpleat/model/Personne.java b/src/main/java/fr/cardon/simpleat/model/Personne.java index 68c58a8..d0534bc 100644 --- a/src/main/java/fr/cardon/simpleat/model/Personne.java +++ b/src/main/java/fr/cardon/simpleat/model/Personne.java @@ -3,10 +3,15 @@ package fr.cardon.simpleat.model; import java.util.ArrayList; import java.util.Collection; +import java.util.List; import javax.persistence.CascadeType; 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; @@ -18,6 +23,8 @@ import javax.persistence.OneToMany; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + + @Entity public class Personne { @@ -28,6 +35,7 @@ public class Personne { private String password; private Collection roles = new ArrayList(); private Collection preference = new ArrayList(); + private List roleList; public Personne() { @@ -35,6 +43,18 @@ public class Personne { // TODO Auto-generated constructor stub } + + + +public Personne(String email, String password, List roleList) { + super(); + this.email = email; + this.password = password; + this.roleList = roleList; + } + + + public Personne(String nom, String prenom, String email, String password) { super(); @@ -45,17 +65,20 @@ public class Personne { } - - - public Personne(String nom, String prenom, String email, String password, Collection roles) { + public Personne(String nom, String prenom, String email, String password, + Collection roles, Collection preference, + List roleList) { super(); this.nom = nom; this.prenom = prenom; this.email = email; this.password = password; this.roles = roles; + this.preference = preference; + this.roleList = roleList; } + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id_personne") @@ -123,4 +146,21 @@ public class Personne { this.preference = preference; } + @ElementCollection(fetch = FetchType.EAGER) + @Enumerated(EnumType.STRING) + public List getRoleList() { + return roleList; + } + + + public void setRoleList(List roleList) { + this.roleList = roleList; + } + + + + + + + } diff --git a/src/main/java/fr/cardon/simpleat/repository/PersonneRepository.java b/src/main/java/fr/cardon/simpleat/repository/PersonneRepository.java index 3089bd7..0df6572 100644 --- a/src/main/java/fr/cardon/simpleat/repository/PersonneRepository.java +++ b/src/main/java/fr/cardon/simpleat/repository/PersonneRepository.java @@ -1,5 +1,7 @@ package fr.cardon.simpleat.repository; +import java.util.Optional; + import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @@ -9,5 +11,11 @@ import fr.cardon.simpleat.model.Personne; public interface PersonneRepository extends JpaRepository { Personne findById(int id); + + Optional findByEmail(String email); + + boolean existsByEmail(String email); + + } diff --git a/src/main/java/fr/cardon/simpleat/security/JwtTokenFilter.java b/src/main/java/fr/cardon/simpleat/security/JwtTokenFilter.java new file mode 100644 index 0000000..bd0e495 --- /dev/null +++ b/src/main/java/fr/cardon/simpleat/security/JwtTokenFilter.java @@ -0,0 +1,47 @@ +package fr.cardon.simpleat.security; + +import java.io.IOException; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.http.HttpStatus; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.filter.OncePerRequestFilter; + +import fr.cardon.simpleat.exception.InvalidJWTException; + + + +/** + * Filtre specifique en charge d'analyser la requête HTTP qui arrive vers notre Serveur et qui doit + * contenir un JWT valide. + */ +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); + } +} diff --git a/src/main/java/fr/cardon/simpleat/security/JwtTokenProvider.java b/src/main/java/fr/cardon/simpleat/security/JwtTokenProvider.java new file mode 100644 index 0000000..2e39ba4 --- /dev/null +++ b/src/main/java/fr/cardon/simpleat/security/JwtTokenProvider.java @@ -0,0 +1,184 @@ +package fr.cardon.simpleat.security; + +import java.util.Base64; +import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import javax.annotation.PostConstruct; +import javax.servlet.http.HttpServletRequest; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.stereotype.Component; + +import com.fasterxml.jackson.core.JsonProcessingException; + +import fr.cardon.simpleat.exception.InvalidJWTException; +import fr.cardon.simpleat.model.EnumRole; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; + +/** + * JWT : classe utilitaire chargée de fournir le Jeton (Token) et les vérifications + */ +@Component +public class JwtTokenProvider { + + // on récupère le secret dans notre fichier application.properties + @Value("${security.jwt.token.secret-key:secret-key}") + private String secretKey; + + // ici on met la valeur par défaut + @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()); + } + + /** + * Methode qui crée le Token avec : + * username comme un champ "sub", + * User Role comme champ "auth" + * "iat" comme date du jour , + * "exp" as now date + validity time. + * claims = les droits + struture : + HEADER : Algo + Type de Token + { + "alg": "HS256", + "typ": "JWT" + } + + PAYLOAD : data + { + "sub": "pbouget", + "auth": [ + "ROLE_ADMIN", + "ROLE_CREATOR", + "ROLE_READER" + ], + "iat": 1589817421, + "exp": 1589821021 + } + + Signature : + + Signature avec code secret : + + HMACSHA256( + base64UrlEncode(header) + "." + + base64UrlEncode(payload), + 03888dd6ceb88c3fee410a70802fb93d483fd52d70349d8f7e7581ae346cf658 + ) + + JWT génèrer avec cette info : + header = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. + payload = eyJzdWIiOiJwYm91Z2V0IiwiYXV0aCI6WyJST0xFX0FETUlOIiwiUk9MRV9DUkVBVE9SIiwiUk9MRV9SRUFERVIiXSwiaWF0IjoxNTg5ODE3NDIxLCJleHAiOjE1ODk4MjEwMjF9. + signature = lrKQIkrCzNMwzTN-hs_EdoYYxrb59sAlku7nmaml0vk + + vérifier sur https://jwt.io + + * @param username the user username. + * @param roles the user roles. + * @return the created JWT as String. + * @throws JsonProcessingException + */ + public String createToken(String email, List roleList){ + + Claims claims = Jwts.claims().setSubject(email); + claims.put("auth", roleList.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 + System.out.println(leToken); // pour test cela donne ceci + /* + site pour convertir une date en millisecondes : http://timestamp.fr/? + site structure du jeton : https://www.vaadata.com/blog/fr/jetons-jwt-et-securite-principes-et-cas-dutilisation/ + site jwt encoder / décoder : https://jwt.io/ + eyJhbGciOiJIUzI1NiJ9. + eyJzdWIiOiJwYm91Z2V0IiwiYXV0aCI6W3siYXV0aG9yaXR5IjoiUk9MRV9BRE1JTiJ9LHsiYXV0aG9yaXR5IjoiUk9MRV9DUkVBVE9SIn0seyJhdXRob3JpdHkiOiJST0xFX1JFQURFUiJ9XSwiaWF0IjoxNTg5ODE2OTIyLCJleHAiOjE1ODk4MjA1MjJ9. + Cn4_UTjZ2UpJ32FVT3Bd1-VN8K62DVBHQbWiK6MNZ04 + + */ + // https://www.codeflow.site/fr/article/java__how-to-convert-java-object-to-from-json-jackson + + return leToken; + } + + /** + * Methode qui retourne un objet Authentication basé sur JWT. + * @param token : le token pour l'authentification. + * @return the authentication si 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 du JWT. + * @param token : Token a 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. + * 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 que JWT est valide. + * 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(); + } + } +} diff --git a/src/main/java/fr/cardon/simpleat/security/WebSecurityConfig.java b/src/main/java/fr/cardon/simpleat/security/WebSecurityConfig.java new file mode 100644 index 0000000..07a43c4 --- /dev/null +++ b/src/main/java/fr/cardon/simpleat/security/WebSecurityConfig.java @@ -0,0 +1,78 @@ +package fr.cardon.simpleat.security; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.BeanIds; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.builders.WebSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +/** + * Configuration de Sécurité globale pour notre REST API. + */ +@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. + * @param http the HttpSecurity object to configure. + * @throws Exception + */ + @Override + protected void configure(HttpSecurity http) throws Exception { + + // Disable CSRF (Cross Site Request Forgery comme votre Token sera stocké dans le session storage) + http.cors(); + + http.csrf().disable() + .sessionManagement() + // Les sessions sont sans états et non créés ni utilisées par Spring security + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + // nos endpoints (points d'entrée de notre API) + .authorizeRequests() + // .anyRequest().authenticated() + .antMatchers("/v2/api-docs", "/configuration/**", "/swagger*/**", "/webjars/**") + .permitAll() + .antMatchers("**").permitAll() // se connecter + .antMatchers("**").permitAll() // s'inscrire + .antMatchers("api/user/all").hasAuthority("ROLE_ADMIN") // que pour le rôle admin + .antMatchers("/v2/api-docs", "/webjars/**", "/swagger-resources/**", "/configuration/**", "/swagger-ui.html/**").permitAll() + // .antMatchers("/api/admin/**").hasAuthority("ROLE_ADMIN") // que pour le rôle admin + // on désactive le reste... + .anyRequest().authenticated(); // tout le reste est autorisé. + // Appliquer JWT + http.addFilterBefore(new JwtTokenFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class); + + } + + /** + * Methode qui configure la sécurité web. + * Utilisé pour interdire l'accès à certains répertoires. + * @param web : WebSecurity + * @throws Exception + */ + @Override + public void configure(WebSecurity web) throws Exception { + web.ignoring().antMatchers("/resources/**"); + } +} + + diff --git a/src/main/java/fr/cardon/simpleat/service/PersonneService.java b/src/main/java/fr/cardon/simpleat/service/PersonneService.java new file mode 100644 index 0000000..99a7a4f --- /dev/null +++ b/src/main/java/fr/cardon/simpleat/service/PersonneService.java @@ -0,0 +1,48 @@ +package fr.cardon.simpleat.service; + +import java.util.List; +import java.util.Optional; + +import org.springframework.stereotype.Service; + +import fr.cardon.simpleat.exception.ExistingUsernameException; +import fr.cardon.simpleat.exception.InvalidCredentialsException; +import fr.cardon.simpleat.model.Personne; + + + +@Service +public interface PersonneService { + + /** + * Methode qui permet à un utilisateur de se connecter. + * @param email : mail 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 email, 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(Personne personne) throws ExistingUsernameException; + + /** + * Methode qui retourne tous les utilisateurs de la bd + * @return the list of all application users. + */ + List 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 findUserByEmail(String email); +} diff --git a/src/main/java/fr/cardon/simpleat/service/PersonneServiceImpl.java b/src/main/java/fr/cardon/simpleat/service/PersonneServiceImpl.java new file mode 100644 index 0000000..e569d34 --- /dev/null +++ b/src/main/java/fr/cardon/simpleat/service/PersonneServiceImpl.java @@ -0,0 +1,74 @@ +package fr.cardon.simpleat.service; + +import java.util.List; +import java.util.Optional; + + + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.stereotype.Service; + + + +import fr.cardon.simpleat.exception.ExistingUsernameException; +import fr.cardon.simpleat.exception.InvalidCredentialsException; +import fr.cardon.simpleat.model.Personne; +import fr.cardon.simpleat.repository.PersonneRepository; +import fr.cardon.simpleat.security.JwtTokenProvider; + + + +@Service +public class PersonneServiceImpl implements PersonneService { + + @Autowired + private PersonneRepository personneRepository; // 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 email, String password) throws InvalidCredentialsException { + try { + authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(email, password)); + return jwtTokenProvider.createToken(email, personneRepository.findByEmail(email).get().getRoleList()); + } catch (AuthenticationException e) { + throw new InvalidCredentialsException(); + } + } + + @Override + public String signup(Personne personne) throws ExistingUsernameException { + if (!personneRepository.existsByEmail(personne.getEmail())) { + Personne personneToSave = new Personne(personne.getEmail(), passwordEncoder.encode(personne.getPassword()), personne.getRoleList()); + personneRepository.save(personneToSave); + return jwtTokenProvider.createToken(personne.getEmail(), personne.getRoleList()); + } else { + throw new ExistingUsernameException(); + } + } + + @Override + public List findAllUsers() { + return personneRepository.findAll(); + } + + @Override + public Optional findUserByEmail(String email) { + return personneRepository.findByEmail(email); + } +} diff --git a/src/main/java/fr/cardon/simpleat/service/UserDetailsServiceImpl.java b/src/main/java/fr/cardon/simpleat/service/UserDetailsServiceImpl.java new file mode 100644 index 0000000..25129e1 --- /dev/null +++ b/src/main/java/fr/cardon/simpleat/service/UserDetailsServiceImpl.java @@ -0,0 +1,42 @@ +package fr.cardon.simpleat.service; + +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.GrantedAuthority; +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.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +import fr.cardon.simpleat.model.Personne; + +import fr.cardon.simpleat.repository.PersonneRepository; + + +@Service +public class UserDetailsServiceImpl implements UserDetailsService { + + @Autowired + private PersonneRepository personneRepository; + + @Override + public UserDetails loadUserByUsername(String email) { + final Optional personne = personneRepository.findByEmail(email); + + if (!personne.isPresent()) { + throw new UsernameNotFoundException("utilisateur '" + email + "' introuvable"); + } + + return User + .withUsername(email) + .password(personne.get().getPassword()) + .authorities(personne.get().getRoleList()) + .accountExpired(false) + .accountLocked(false) + .credentialsExpired(false) + .disabled(false) + .build(); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 875e116..30cfcf0 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,9 +1,14 @@ +#security.jwt.token.secret-key=03888dd6ceb88c3fee410a70802fb93d483fd52d70349d8f7e7581ae346cf658 +security.jwt.token.secret-key=simpleat +spring.main.allow-circular-references=true +# pour comprendre le but de cette ligne ci-dessus : https://www.baeldung.com/circular-dependencies-in-spring +# =============================== # =============================== # base de données MySQL # =============================== -spring.datasource.url=jdbc:mysql://localhost:3308/simpleat?useSSL=false&zeroDateTimeBehavior=CONVERT_TO_NULL&serverTimezone=CET +spring.datasource.url=jdbc:mysql://localhost:3306/simpleat?useSSL=false&zeroDateTimeBehavior=CONVERT_TO_NULL&serverTimezone=CET spring.datasource.username=root -spring.datasource.password=root +spring.datasource.password=BOHmilaresol02 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver # log #logging.level.root=INFO @@ -20,4 +25,5 @@ spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5InnoDBDial # Permet d'exécuter le data.sql # =============================== spring.sql.init.mode=always -spring.jpa.defer-datasource-initialization=true \ No newline at end of file +spring.jpa.defer-datasource-initialization=true +server.port=8081 \ No newline at end of file diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index 6c955fa..6126ca4 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -1,14 +1,17 @@ - -insert into personne (nom, prenom, email, password) values ('Harmond', 'Ludvig', 'lharmond0@moonfruit.com', 'pjKxIN'); -insert into personne (nom, prenom, email, password) values ('Fisbey', 'Jammal', 'jfisbey1@apache.org', 'YY0TuY6JH0di'); -insert into personne (nom, prenom, email, password) values ('Scrivener', 'Anatol', 'ascrivener2@jugem.jp', 'C4sfAW4'); -insert into personne (nom, prenom, email, password) values ('Berthelet', 'Oralla', 'oberthelet3@cnbc.com', 'ACdXxMr'); -insert into personne (nom, prenom, email, password) values ('Towe', 'Homerus', 'htowe4@home.pl', 'pQGi41q5JHY'); -insert into personne (nom, prenom, email, password) values ('Viggers', 'Gaby', 'gviggers5@xing.com', 'Gbr2M5UU'); -insert into personne (nom, prenom, email, password) values ('Willshere', 'Sheba', 'swillshere6@tinyurl.com', 'yVJmjda'); -insert into personne (nom, prenom, email, password) values ('Docksey', 'Eula', 'edocksey7@mozilla.com', '6yS7lkFpuY'); -insert into personne (nom, prenom, email, password) values ('Iglesias', 'Christen', 'ciglesias8@ebay.com', 'ottn7Qb'); -insert into personne (nom, prenom, email, password) values ('Crick', 'Andris', 'acrick9@etsy.com', 'nkmUVBeKr'); +insert into personne (nom, prenom, email, password) values ('Cardon', 'Thomas', 'thomas.cardon@gmail.com', 'pjKxIN'); +insert into personne (nom, prenom, email, password) values ('Ramiere', 'Vincent', 'vincent.ramiere@gmail.com', 'YY0TuY6JH0di'); +insert into personne (nom, prenom, email, password) values ('Verger', 'Romain', 'romain.verger@gmail.com', 'C4sfAW4'); +insert into personne (nom, prenom, email, password) values ('Ribardiere', 'Paul-Emmanuel', 'paul.ribardiere@gmail.com', 'ACdXxMr'); +insert into personne (nom, prenom, email, password) values ('Noris', 'William', 'william.noris@gmail.com', 'pjKxIN'); +insert into personne (nom, prenom, email, password) values ('Harmand', 'Isabelle', 'isabelle.harmand@gmail.com', 'YY0TuY6JH0di'); +insert into personne (nom, prenom, email, password) values ('Bajard', 'Blandine', 'blandine.bajard@gmail.com', 'C4sfAW4'); +insert into personne (nom, prenom, email, password) values ('El hiri', 'Sana', 'sana.el-hiri@gmail.com', 'ACdXxMr'); +insert into personne (nom, prenom, email, password) values ('Lucas', 'Cecile', 'cecile.lucas@gmail.com', 'pjKxIN'); +insert into personne (nom, prenom, email, password) values ('Kerkeb', 'Mohamed', 'mohamed.kerkeb@gmail.com', 'YY0TuY6JH0di'); +insert into personne (nom, prenom, email, password) values ('Rinquin', 'Aline', 'aline.rinquin@gmail.com', 'C4sfAW4'); +insert into personne (nom, prenom, email, password) values ('Keddar', 'Noreddine', 'noredinne.keddar@gmail.com', 'ACdXxMr'); +insert into personne (nom, prenom, email, password) values ('Tomczyk', 'Julian', 'julian.tomczyk@gmail.com', 'pjKxIN'); +insert into personne (nom, prenom, email, password) values ('MJID', 'Hedi', 'hedi.mjid@gmail.com', 'YY0TuY6JH0di'); insert into role (id_role, intitule) values (1, 'Admin'); insert into role (id_role, intitule) values (2, 'User'); @@ -93,11 +96,18 @@ insert into type (id_type, libelle) values (17, 'Marocain'); insert into type (id_type, libelle) values (18, 'Fruits de Mer'); insert into type (id_type, libelle) values (19, 'Steack'); insert into type (id_type, libelle) values (20, 'Vietnamien'); +insert into type (id_type, libelle) values (21, 'Bistrot'); +insert into type (id_type, libelle) values (22, 'Poulet'); -insert into type_restau (id_restau, id_type) values (1, 3); -insert into type_restau (id_restau, id_type) values (2, 1); -insert into type_restau (id_restau, id_type) values (3, 1); -insert into type_restau (id_restau, id_type) values (4, 2); -insert into type_restau (id_restau, id_type) values (5, 4); -insert into type_restau (id_restau, id_type) values (1, 2); \ No newline at end of file +insert into type_restau (id_restau, id_type) values (1, 5); +insert into type_restau (id_restau, id_type) values (2, 5); +insert into type_restau (id_restau, id_type) values (3, 5); +insert into type_restau (id_restau, id_type) values (4, 5); +insert into type_restau (id_restau, id_type) values (5, 5); +insert into type_restau (id_restau, id_type) values (6, 8); +insert into type_restau (id_restau, id_type) values (6, 22); +insert into type_restau (id_restau, id_type) values (7, 7); +insert into type_restau (id_restau, id_type) values (8, 5); +insert into type_restau (id_restau, id_type) values (9, 21); +insert into type_restau (id_restau, id_type) values (10, 7); \ No newline at end of file