Implémentation de JWT et Authent pour toute la partie author

This commit is contained in:
Guams 2024-12-20 14:28:26 +01:00
parent b39307f7d9
commit d14ae93f95
11 changed files with 210 additions and 37 deletions

View File

@ -3,6 +3,7 @@ package com.guams.review.configuration;
import com.guams.review.exception.AlreadyExistsException; import com.guams.review.exception.AlreadyExistsException;
import com.guams.review.exception.InvalidNameOrPasswordException; import com.guams.review.exception.InvalidNameOrPasswordException;
import com.guams.review.exception.NotFoundException; import com.guams.review.exception.NotFoundException;
import com.guams.review.exception.UnauthorizedExecption;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ControllerAdvice;
@ -25,4 +26,9 @@ public class Advice {
public ResponseEntity<Object> handleInvalidNameOrPassword(InvalidNameOrPasswordException exception) { public ResponseEntity<Object> handleInvalidNameOrPassword(InvalidNameOrPasswordException exception) {
return new ResponseEntity<>(exception.getMessage(), HttpStatus.UNAUTHORIZED); return new ResponseEntity<>(exception.getMessage(), HttpStatus.UNAUTHORIZED);
} }
@ExceptionHandler(value = UnauthorizedExecption.class)
public ResponseEntity<Object> handleUnauthorizedExecption(UnauthorizedExecption exception) {
return new ResponseEntity<>(exception.getMessage(), HttpStatus.UNAUTHORIZED);
}
} }

View File

@ -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
}
}

View File

@ -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();
}
}

View File

@ -1,5 +1,4 @@
package com.guams.review.configuration; package com.guams.review.configuration;
import com.guams.review.service.AuthorService; import com.guams.review.service.AuthorService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean; 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.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration @Configuration
@ -22,12 +22,16 @@ public class SpringSecurityConfig {
private final AuthorService authorService; private final AuthorService authorService;
@Bean @Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtAuthenticationFilter jwtAuthenticationFilter) throws Exception {
return http return http
.csrf(AbstractHttpConfigurer::disable) .csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> .authorizeHttpRequests(auth -> auth
auth.requestMatchers("/api/authors/login", "/api/authors/register").permitAll() .requestMatchers("/api/authors/login", "/api/authors/register").permitAll()
.anyRequest().authenticated()).build(); .requestMatchers("/api/authors/me").authenticated() // Autorise les utilisateurs authentifiés
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) // Ajoute le filtre JWT
.build();
} }
@Bean @Bean

View File

@ -1,20 +1,23 @@
package com.guams.review.controller; package com.guams.review.controller;
import com.guams.review.configuration.JwtTokenUtil;
import com.guams.review.exception.AlreadyExistsException; import com.guams.review.exception.AlreadyExistsException;
import com.guams.review.exception.InvalidNameOrPasswordException; import com.guams.review.exception.InvalidNameOrPasswordException;
import com.guams.review.exception.NotFoundException; import com.guams.review.exception.NotFoundException;
import com.guams.review.exception.UnauthorizedExecption;
import com.guams.review.model.AuthorRepository; import com.guams.review.model.AuthorRepository;
import com.guams.review.model.dao.Author; import com.guams.review.model.dao.Author;
import com.guams.review.model.dao.Post; import com.guams.review.model.dao.Post;
import com.guams.review.service.AuthorService; import com.guams.review.service.AuthorService;
import com.guams.review.service.mapper.Mapper; 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 lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@ -33,41 +36,42 @@ public class AuthorController {
private final PasswordEncoder passwordEncoder; private final PasswordEncoder passwordEncoder;
private final AuthenticationManager authenticationManager; private final AuthenticationManager authenticationManager;
private final AuthorService authorService; private final AuthorService authorService;
private final JwtTokenUtil jwtTokenUtil;
private final AuthorRepository authorRepository; private final AuthorRepository authorRepository;
private final Mapper mapper; private final Mapper mapper;
@GetMapping @GetMapping
public List<ReturnableAuthor> getUsers() { public List<AuthorWithToken> getUsers() {
return authorService.list(); return authorService.list();
} }
@GetMapping("/{id}") @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")); Author author = authorService.findById(id).orElseThrow(() -> new NotFoundException("Author not found"));
return mapper.mapAuthor(author); return mapper.mapAuthor(author);
} }
@PutMapping("/{id}") @PutMapping("/{id}")
public void updateUser(@PathVariable UUID id, @RequestBody Author updatedAuthor) { public void updateUser(@PathVariable UUID id, @RequestBody Author updatedAuthor, Authentication authentication) {
Author authorToUpdate = authorService.findById(id).orElseThrow(() -> new NotFoundException("Author not found")); Author authorToUpdate = authorService.verifyIfUserIsAuthorized(authentication, id);
authorService.insert(updatedAuthor.setId(authorToUpdate.getId())); authorService.insert(updatedAuthor.setId(authorToUpdate.getId()));
} }
@PutMapping(value = "{id}/avatar", consumes = {MediaType.MULTIPART_FORM_DATA_VALUE}) @PutMapping(value = "{id}/avatar", consumes = {MediaType.MULTIPART_FORM_DATA_VALUE})
public void updateUserAvatar(@PathVariable UUID id, @RequestPart MultipartFile avatar) throws IOException { public void updateUserAvatar(@PathVariable UUID id, @RequestPart MultipartFile avatar, Authentication authentication) throws IOException {
Author authorToUpdate = authorService.findById(id).orElseThrow(() -> new NotFoundException("Author not found")); Author authorToUpdate = authorService.verifyIfUserIsAuthorized(authentication, id);
authorService.insert(authorToUpdate.setProfilePicture(avatar.getBytes())); authorService.insert(authorToUpdate.setProfilePicture(avatar.getBytes()));
} }
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public void deleteUser(@PathVariable UUID id) { public void deleteUser(@PathVariable UUID id, Authentication authentication) {
Author authorToDelete = authorService.findById(id).orElseThrow(() -> new NotFoundException("Author not found")); Author authorToDelete = authorService.verifyIfUserIsAuthorized(authentication, id);
authorService.delete(authorToDelete); authorService.delete(authorToDelete);
} }
@PutMapping("/{id}/posts") @PutMapping("/{id}/posts")
public void updateUserPosts(@PathVariable("id") UUID authorId, @RequestBody List<Long> postIds) { public void updateUserPosts(@PathVariable("id") UUID authorId, @RequestBody List<Long> postIds, Authentication authentication) {
Author author = authorService.findById(authorId).orElseThrow(() -> new NotFoundException("Author not found")); Author author = authorService.verifyIfUserIsAuthorized(authentication, authorId);
authorService.insertPublications(author.getId(), postIds); authorService.insertPublications(author.getId(), postIds);
} }
@ -78,11 +82,15 @@ public class AuthorController {
} }
@PostMapping("/login") @PostMapping("/login")
public void authorLogin(@RequestBody Author author) { public AuthorWithToken authorLogin(@RequestBody Author author) {
try { try {
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(author.getName(), author.getPassword())); authenticationManager.authenticate(
} new UsernamePasswordAuthenticationToken(author.getName(), author.getPassword())
catch (Exception e) { );
String token = jwtTokenUtil.generateToken(author.getName());
return mapper.mapAuthor(author).setToken(token);
} catch (Exception e) {
throw new InvalidNameOrPasswordException(e.getMessage()); throw new InvalidNameOrPasswordException(e.getMessage());
} }
} }
@ -94,6 +102,23 @@ public class AuthorController {
throw new AlreadyExistsException("Author already exists"); throw new AlreadyExistsException("Author already exists");
} }
author.setPassword(passwordEncoder.encode(author.getPassword())); 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("");
}
} }

View File

@ -0,0 +1,7 @@
package com.guams.review.exception;
public class UnauthorizedExecption extends RuntimeException {
public UnauthorizedExecption(String message) {
super(message);
}
}

View File

@ -7,7 +7,11 @@ import lombok.experimental.Accessors;
import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Column; import org.springframework.data.relational.core.mapping.Column;
import org.springframework.data.relational.core.mapping.Table; 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; import java.util.UUID;
@ -26,9 +30,10 @@ public class Author {
@Column("password") @Column("password")
String password; String password;
// @Column("role")
// String role;
@Column("profile_picture") @Column("profile_picture")
byte[] profilePicture; byte[] profilePicture;
}
@Column("role")
String role;
}

View File

@ -1,12 +1,15 @@
package com.guams.review.service; 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.AuthorRepository;
import com.guams.review.model.PostRepository; import com.guams.review.model.PostRepository;
import com.guams.review.model.dao.Author; import com.guams.review.model.dao.Author;
import com.guams.review.model.dao.Post; import com.guams.review.model.dao.Post;
import com.guams.review.service.mapper.Mapper; 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 lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;
@ -25,7 +28,7 @@ public class AuthorService implements UserDetailsService
private final AuthorRepository authorRepository; private final AuthorRepository authorRepository;
private final PostRepository postRepository; private final PostRepository postRepository;
public List<ReturnableAuthor> list() { public List<AuthorWithToken> list() {
return authorRepository.findAll().stream() return authorRepository.findAll().stream()
.map(mapper::mapAuthor) .map(mapper::mapAuthor)
.toList(); .toList();
@ -39,7 +42,7 @@ public class AuthorService implements UserDetailsService
return authorRepository.findById(id); return authorRepository.findById(id);
} }
public ReturnableAuthor insert(Author author) { public AuthorWithToken insert(Author author) {
return mapper.mapAuthor(authorRepository.save(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) { public void delete(Author author) {
authorRepository.delete(author); authorRepository.delete(author);
} }
@ -63,6 +79,6 @@ public class AuthorService implements UserDetailsService
if (author == null) { if (author == null) {
throw new UsernameNotFoundException(username); 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())));
} }
} }

View File

@ -9,8 +9,10 @@ import java.util.UUID;
@Getter @Getter
@Setter @Setter
@Accessors(chain = true) @Accessors(chain = true)
public class ReturnableAuthor { public class AuthorWithToken {
private UUID id; private UUID id;
private String name; private String name;
private byte[] profilePicture; private byte[] profilePicture;
private String role;
private String token;
} }

View File

@ -8,10 +8,11 @@ public class Mapper {
public Mapper() {} public Mapper() {}
public ReturnableAuthor mapAuthor(Author author) { public AuthorWithToken mapAuthor(Author author) {
return new ReturnableAuthor() return new AuthorWithToken()
.setId(author.getId()) .setId(author.getId())
.setName(author.getName()) .setName(author.getName())
.setRole(author.getRole())
.setProfilePicture(author.getProfilePicture()); .setProfilePicture(author.getProfilePicture());
} }
} }

View File

@ -4,10 +4,11 @@ drop table if exists author;
create table author create table author
( (
id uuid default gen_random_uuid() primary key, id uuid default gen_random_uuid() primary key,
name varchar(255) not null, name varchar(255) unique not null,
password text not null, password text not null,
profile_picture bytea profile_picture bytea,
role varchar(50) default 'USER' not null
); );
create table post create table post
@ -18,7 +19,7 @@ create table post
title varchar(50) not null, title varchar(50) not null,
body text not null, body text not null,
category varchar(50) not null, category varchar(50) not null,
publication_date date not null publication_date date not null
); );
create table publication create table publication