From d14ae93f9501ab9cf3a10a9a9cca48818df622a3 Mon Sep 17 00:00:00 2001 From: Guams Date: Fri, 20 Dec 2024 14:28:26 +0100 Subject: [PATCH] =?UTF-8?q?Impl=C3=A9mentation=20de=20JWT=20et=20Authent?= =?UTF-8?q?=20pour=20toute=20la=20partie=20author?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../guams/review/configuration/Advice.java | 6 ++ .../JwtAuthenticationFilter.java | 56 ++++++++++++++++++ .../review/configuration/JwtTokenUtil.java | 50 ++++++++++++++++ .../configuration/SpringSecurityConfig.java | 14 +++-- .../review/controller/AuthorController.java | 57 +++++++++++++------ .../exception/UnauthorizedExecption.java | 7 +++ .../com/guams/review/model/dao/Author.java | 13 +++-- .../guams/review/service/AuthorService.java | 24 ++++++-- ...rnableAuthor.java => AuthorWithToken.java} | 4 +- .../guams/review/service/mapper/Mapper.java | 5 +- src/main/resources/script.sql | 11 ++-- 11 files changed, 210 insertions(+), 37 deletions(-) create mode 100644 src/main/java/com/guams/review/configuration/JwtAuthenticationFilter.java create mode 100644 src/main/java/com/guams/review/configuration/JwtTokenUtil.java create mode 100644 src/main/java/com/guams/review/exception/UnauthorizedExecption.java rename src/main/java/com/guams/review/service/mapper/{ReturnableAuthor.java => AuthorWithToken.java} (76%) diff --git a/src/main/java/com/guams/review/configuration/Advice.java b/src/main/java/com/guams/review/configuration/Advice.java index 1f398f1..6e12a5d 100644 --- a/src/main/java/com/guams/review/configuration/Advice.java +++ b/src/main/java/com/guams/review/configuration/Advice.java @@ -3,6 +3,7 @@ package com.guams.review.configuration; import com.guams.review.exception.AlreadyExistsException; import com.guams.review.exception.InvalidNameOrPasswordException; import com.guams.review.exception.NotFoundException; +import com.guams.review.exception.UnauthorizedExecption; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ControllerAdvice; @@ -25,4 +26,9 @@ public class Advice { public ResponseEntity handleInvalidNameOrPassword(InvalidNameOrPasswordException exception) { return new ResponseEntity<>(exception.getMessage(), HttpStatus.UNAUTHORIZED); } + + @ExceptionHandler(value = UnauthorizedExecption.class) + public ResponseEntity handleUnauthorizedExecption(UnauthorizedExecption exception) { + return new ResponseEntity<>(exception.getMessage(), HttpStatus.UNAUTHORIZED); + } } diff --git a/src/main/java/com/guams/review/configuration/JwtAuthenticationFilter.java b/src/main/java/com/guams/review/configuration/JwtAuthenticationFilter.java new file mode 100644 index 0000000..25dd174 --- /dev/null +++ b/src/main/java/com/guams/review/configuration/JwtAuthenticationFilter.java @@ -0,0 +1,56 @@ +package com.guams.review.configuration; +import com.guams.review.model.dao.Author; +import com.guams.review.service.AuthorService; +import io.jsonwebtoken.JwtException; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; +import java.io.IOException; + +@Component +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtTokenUtil jwtTokenUtil; + private final AuthorService authorService; + + public JwtAuthenticationFilter(JwtTokenUtil jwtTokenUtil, AuthorService authorService) { + this.jwtTokenUtil = jwtTokenUtil; + this.authorService = authorService; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + final String authHeader = request.getHeader("Authorization"); + + if (authHeader != null && authHeader.startsWith("Bearer ")) { + String token = authHeader.substring(7); // Retire le préfixe "Bearer " + try { + String username = jwtTokenUtil.extractUsername(token); // Récupère l'utilisateur du token + + if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { + UserDetails userDetails = authorService.loadUserByUsername(username); + + if (jwtTokenUtil.validateToken(token, userDetails)) { + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authentication); // Met à jour le contexte + } + } + } catch (JwtException e) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return; + } + } + + filterChain.doFilter(request, response); // Continue le traitement + } +} diff --git a/src/main/java/com/guams/review/configuration/JwtTokenUtil.java b/src/main/java/com/guams/review/configuration/JwtTokenUtil.java new file mode 100644 index 0000000..7855e08 --- /dev/null +++ b/src/main/java/com/guams/review/configuration/JwtTokenUtil.java @@ -0,0 +1,50 @@ +package com.guams.review.configuration; + +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; + +import java.util.Date; + +@Component +public class JwtTokenUtil { + + private final String SECRET_KEY = "9f87d7a0eb7dea860b98adf6bb94feefe4a33698022733bb012d662d92db8081"; + private final long EXPIRATION_TIME = 1000 * 60 * 60 * 10; // 10 heures + + public String generateToken(String username) { + return Jwts.builder() + .setSubject(username) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME)) + .signWith(SignatureAlgorithm.HS256, SECRET_KEY) + .compact(); + } + + public String extractUsername(String token) { + return Jwts.parser() + .setSigningKey(SECRET_KEY) + .parseClaimsJws(token) + .getBody() + .getSubject(); + } + + public boolean validateToken(String token, UserDetails userDetails) { + String username = extractUsername(token); + return username.equals(userDetails.getUsername()) && !isTokenExpired(token); + } + + private boolean isTokenExpired(String token) { + return extractExpiration(token).before(new Date()); + } + + private Date extractExpiration(String token) { + return Jwts.parser() + .setSigningKey(SECRET_KEY) + .parseClaimsJws(token) + .getBody() + .getExpiration(); + } + +} diff --git a/src/main/java/com/guams/review/configuration/SpringSecurityConfig.java b/src/main/java/com/guams/review/configuration/SpringSecurityConfig.java index c50c2bb..0ab54af 100644 --- a/src/main/java/com/guams/review/configuration/SpringSecurityConfig.java +++ b/src/main/java/com/guams/review/configuration/SpringSecurityConfig.java @@ -1,5 +1,4 @@ package com.guams.review.configuration; - import com.guams.review.service.AuthorService; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; @@ -12,6 +11,7 @@ import org.springframework.security.config.annotation.web.configurers.AbstractHt import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration @@ -22,12 +22,16 @@ public class SpringSecurityConfig { private final AuthorService authorService; @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtAuthenticationFilter jwtAuthenticationFilter) throws Exception { return http .csrf(AbstractHttpConfigurer::disable) - .authorizeHttpRequests(auth -> - auth.requestMatchers("/api/authors/login", "/api/authors/register").permitAll() - .anyRequest().authenticated()).build(); + .authorizeHttpRequests(auth -> auth + .requestMatchers("/api/authors/login", "/api/authors/register").permitAll() + .requestMatchers("/api/authors/me").authenticated() // Autorise les utilisateurs authentifiés + .anyRequest().authenticated() + ) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) // Ajoute le filtre JWT + .build(); } @Bean diff --git a/src/main/java/com/guams/review/controller/AuthorController.java b/src/main/java/com/guams/review/controller/AuthorController.java index b6f3c66..dd67868 100644 --- a/src/main/java/com/guams/review/controller/AuthorController.java +++ b/src/main/java/com/guams/review/controller/AuthorController.java @@ -1,20 +1,23 @@ package com.guams.review.controller; +import com.guams.review.configuration.JwtTokenUtil; import com.guams.review.exception.AlreadyExistsException; import com.guams.review.exception.InvalidNameOrPasswordException; import com.guams.review.exception.NotFoundException; +import com.guams.review.exception.UnauthorizedExecption; import com.guams.review.model.AuthorRepository; import com.guams.review.model.dao.Author; import com.guams.review.model.dao.Post; import com.guams.review.service.AuthorService; import com.guams.review.service.mapper.Mapper; -import com.guams.review.service.mapper.ReturnableAuthor; +import com.guams.review.service.mapper.AuthorWithToken; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.util.Assert; import org.springframework.web.bind.annotation.*; @@ -33,41 +36,42 @@ public class AuthorController { private final PasswordEncoder passwordEncoder; private final AuthenticationManager authenticationManager; private final AuthorService authorService; + private final JwtTokenUtil jwtTokenUtil; private final AuthorRepository authorRepository; private final Mapper mapper; @GetMapping - public List getUsers() { + public List getUsers() { return authorService.list(); } @GetMapping("/{id}") - public ReturnableAuthor findUser(@PathVariable UUID id) { + public AuthorWithToken findUser(@PathVariable UUID id) { Author author = authorService.findById(id).orElseThrow(() -> new NotFoundException("Author not found")); return mapper.mapAuthor(author); } @PutMapping("/{id}") - public void updateUser(@PathVariable UUID id, @RequestBody Author updatedAuthor) { - Author authorToUpdate = authorService.findById(id).orElseThrow(() -> new NotFoundException("Author not found")); + public void updateUser(@PathVariable UUID id, @RequestBody Author updatedAuthor, Authentication authentication) { + Author authorToUpdate = authorService.verifyIfUserIsAuthorized(authentication, id); authorService.insert(updatedAuthor.setId(authorToUpdate.getId())); } @PutMapping(value = "{id}/avatar", consumes = {MediaType.MULTIPART_FORM_DATA_VALUE}) - public void updateUserAvatar(@PathVariable UUID id, @RequestPart MultipartFile avatar) throws IOException { - Author authorToUpdate = authorService.findById(id).orElseThrow(() -> new NotFoundException("Author not found")); + public void updateUserAvatar(@PathVariable UUID id, @RequestPart MultipartFile avatar, Authentication authentication) throws IOException { + Author authorToUpdate = authorService.verifyIfUserIsAuthorized(authentication, id); authorService.insert(authorToUpdate.setProfilePicture(avatar.getBytes())); } @DeleteMapping("/{id}") - public void deleteUser(@PathVariable UUID id) { - Author authorToDelete = authorService.findById(id).orElseThrow(() -> new NotFoundException("Author not found")); + public void deleteUser(@PathVariable UUID id, Authentication authentication) { + Author authorToDelete = authorService.verifyIfUserIsAuthorized(authentication, id); authorService.delete(authorToDelete); } @PutMapping("/{id}/posts") - public void updateUserPosts(@PathVariable("id") UUID authorId, @RequestBody List postIds) { - Author author = authorService.findById(authorId).orElseThrow(() -> new NotFoundException("Author not found")); + public void updateUserPosts(@PathVariable("id") UUID authorId, @RequestBody List postIds, Authentication authentication) { + Author author = authorService.verifyIfUserIsAuthorized(authentication, authorId); authorService.insertPublications(author.getId(), postIds); } @@ -78,11 +82,15 @@ public class AuthorController { } @PostMapping("/login") - public void authorLogin(@RequestBody Author author) { + public AuthorWithToken authorLogin(@RequestBody Author author) { try { - authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(author.getName(), author.getPassword())); - } - catch (Exception e) { + authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(author.getName(), author.getPassword()) + ); + + String token = jwtTokenUtil.generateToken(author.getName()); + return mapper.mapAuthor(author).setToken(token); + } catch (Exception e) { throw new InvalidNameOrPasswordException(e.getMessage()); } } @@ -94,6 +102,23 @@ public class AuthorController { throw new AlreadyExistsException("Author already exists"); } author.setPassword(passwordEncoder.encode(author.getPassword())); - return new ResponseEntity<>(authorRepository.save(author).setPassword(""), HttpStatus.CREATED); + return new ResponseEntity<>(authorRepository.save(author.setRole("USER")).setPassword(""), HttpStatus.CREATED); } + + @GetMapping("/me") + public Author getAuthenticatedUser(Authentication authentication) { + if (authentication == null || !authentication.isAuthenticated()) { + throw new UnauthorizedExecption("You are not authorized to access this resource"); + } + + String username = authentication.getName(); + Author author = authorRepository.findByName(username); + + if (author == null) { + throw new NotFoundException("Author not found"); + } + + return author.setPassword(""); + } + } diff --git a/src/main/java/com/guams/review/exception/UnauthorizedExecption.java b/src/main/java/com/guams/review/exception/UnauthorizedExecption.java new file mode 100644 index 0000000..579779a --- /dev/null +++ b/src/main/java/com/guams/review/exception/UnauthorizedExecption.java @@ -0,0 +1,7 @@ +package com.guams.review.exception; + +public class UnauthorizedExecption extends RuntimeException { + public UnauthorizedExecption(String message) { + super(message); + } +} diff --git a/src/main/java/com/guams/review/model/dao/Author.java b/src/main/java/com/guams/review/model/dao/Author.java index 621241e..b783802 100644 --- a/src/main/java/com/guams/review/model/dao/Author.java +++ b/src/main/java/com/guams/review/model/dao/Author.java @@ -7,7 +7,11 @@ import lombok.experimental.Accessors; import org.springframework.data.annotation.Id; import org.springframework.data.relational.core.mapping.Column; import org.springframework.data.relational.core.mapping.Table; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import java.util.Collection; +import java.util.List; import java.util.UUID; @@ -26,9 +30,10 @@ public class Author { @Column("password") String password; -// @Column("role") -// String role; - @Column("profile_picture") byte[] profilePicture; -} \ No newline at end of file + + @Column("role") + String role; + +} diff --git a/src/main/java/com/guams/review/service/AuthorService.java b/src/main/java/com/guams/review/service/AuthorService.java index fef014e..506247c 100644 --- a/src/main/java/com/guams/review/service/AuthorService.java +++ b/src/main/java/com/guams/review/service/AuthorService.java @@ -1,12 +1,15 @@ package com.guams.review.service; +import com.guams.review.exception.NotFoundException; +import com.guams.review.exception.UnauthorizedExecption; import com.guams.review.model.AuthorRepository; import com.guams.review.model.PostRepository; import com.guams.review.model.dao.Author; import com.guams.review.model.dao.Post; import com.guams.review.service.mapper.Mapper; -import com.guams.review.service.mapper.ReturnableAuthor; +import com.guams.review.service.mapper.AuthorWithToken; import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; @@ -25,7 +28,7 @@ public class AuthorService implements UserDetailsService private final AuthorRepository authorRepository; private final PostRepository postRepository; - public List list() { + public List list() { return authorRepository.findAll().stream() .map(mapper::mapAuthor) .toList(); @@ -39,7 +42,7 @@ public class AuthorService implements UserDetailsService return authorRepository.findById(id); } - public ReturnableAuthor insert(Author author) { + public AuthorWithToken insert(Author author) { return mapper.mapAuthor(authorRepository.save(author)); } @@ -53,6 +56,19 @@ public class AuthorService implements UserDetailsService } } + public Author verifyIfUserIsAuthorized(Authentication authentication, UUID id) { + if (authentication == null || !authentication.isAuthenticated()) { + throw new UnauthorizedExecption("You have to login first"); + } + Author authorToUpdate = findById(id).orElseThrow(() -> new NotFoundException("Author not found")); + String username = authentication.getName(); + Author authorAuthenticated = authorRepository.findByName(username); + if (authorAuthenticated.getId() != authorToUpdate.getId() && !authorAuthenticated.getRole().equals("ADMIN")) { + throw new UnauthorizedExecption("Specified Author is not authorized to do that"); + } + return authorToUpdate; + } + public void delete(Author author) { authorRepository.delete(author); } @@ -63,6 +79,6 @@ public class AuthorService implements UserDetailsService if (author == null) { throw new UsernameNotFoundException(username); } - return new User(author.getName(), author.getPassword(), Collections.singletonList(new SimpleGrantedAuthority("USER"))); // temporaire pour le role + return new User(author.getName(), author.getPassword(), Collections.singletonList(new SimpleGrantedAuthority(author.getRole()))); } } diff --git a/src/main/java/com/guams/review/service/mapper/ReturnableAuthor.java b/src/main/java/com/guams/review/service/mapper/AuthorWithToken.java similarity index 76% rename from src/main/java/com/guams/review/service/mapper/ReturnableAuthor.java rename to src/main/java/com/guams/review/service/mapper/AuthorWithToken.java index cbffc71..c7e8587 100644 --- a/src/main/java/com/guams/review/service/mapper/ReturnableAuthor.java +++ b/src/main/java/com/guams/review/service/mapper/AuthorWithToken.java @@ -9,8 +9,10 @@ import java.util.UUID; @Getter @Setter @Accessors(chain = true) -public class ReturnableAuthor { +public class AuthorWithToken { private UUID id; private String name; private byte[] profilePicture; + private String role; + private String token; } diff --git a/src/main/java/com/guams/review/service/mapper/Mapper.java b/src/main/java/com/guams/review/service/mapper/Mapper.java index 4fb0c1a..341f671 100644 --- a/src/main/java/com/guams/review/service/mapper/Mapper.java +++ b/src/main/java/com/guams/review/service/mapper/Mapper.java @@ -8,10 +8,11 @@ public class Mapper { public Mapper() {} - public ReturnableAuthor mapAuthor(Author author) { - return new ReturnableAuthor() + public AuthorWithToken mapAuthor(Author author) { + return new AuthorWithToken() .setId(author.getId()) .setName(author.getName()) + .setRole(author.getRole()) .setProfilePicture(author.getProfilePicture()); } } diff --git a/src/main/resources/script.sql b/src/main/resources/script.sql index 0c3bf9e..9ec81f0 100644 --- a/src/main/resources/script.sql +++ b/src/main/resources/script.sql @@ -4,10 +4,11 @@ drop table if exists author; create table author ( - id uuid default gen_random_uuid() primary key, - name varchar(255) not null, - password text not null, - profile_picture bytea + id uuid default gen_random_uuid() primary key, + name varchar(255) unique not null, + password text not null, + profile_picture bytea, + role varchar(50) default 'USER' not null ); create table post @@ -18,7 +19,7 @@ create table post title varchar(50) not null, body text not null, category varchar(50) not null, - publication_date date not null + publication_date date not null ); create table publication