From dd288fef5d9ef2d5408cdd38832c2d89491bcc49 Mon Sep 17 00:00:00 2001 From: Nazar Klimovych Date: Fri, 10 May 2024 19:58:41 +0300 Subject: [PATCH 01/12] Added SecurityConfig class with PasswordEncoder and SecurityFilterChain beans. Implemented UserDetails interface in the User class. Added the implementation for UserDetailsService interface. Added Role entity. Added RoleRepository. Added roles field to the User entity. Added Liquibase changesets to insert roles into DB and assign roles for users. --- pom.xml | 5 ++ .../bookstore/config/SecurityConfig.java | 46 ++++++++++++++++++ .../controller/AuthenticationController.java | 2 +- .../bookstore/controller/BookController.java | 8 +++- .../exception/GlobalExceptionHandler.java | 8 +++- .../academy/bookstore/mapper/UserMapper.java | 1 + .../mate/academy/bookstore/model/Role.java | 32 +++++++++++++ .../academy/bookstore/model/RoleName.java | 6 +++ .../mate/academy/bookstore/model/User.java | 48 ++++++++++++++++++- .../repository/role/RoleRepository.java | 13 +++++ .../repository/user/UserRepository.java | 3 ++ .../service/impl/UserDetailsServiceImpl.java | 20 ++++++++ .../service/impl/UserServiceImpl.java | 8 ++++ .../changes/03-create-roles-table.yaml | 20 ++++++++ .../changes/04-create-users-roles-table.yaml | 22 +++++++++ .../05-insert-roles-into-database.yaml | 17 +++++++ .../06-insert-admin-user-into-database.yaml | 23 +++++++++ .../07-assign-admin-user-to-role-admin.yaml | 14 ++++++ .../db/changelog/db.changelog-master.yaml | 10 ++++ 19 files changed, 302 insertions(+), 4 deletions(-) create mode 100644 src/main/java/mate/academy/bookstore/config/SecurityConfig.java create mode 100644 src/main/java/mate/academy/bookstore/model/Role.java create mode 100644 src/main/java/mate/academy/bookstore/model/RoleName.java create mode 100644 src/main/java/mate/academy/bookstore/repository/role/RoleRepository.java create mode 100644 src/main/java/mate/academy/bookstore/service/impl/UserDetailsServiceImpl.java create mode 100644 src/main/resources/db/changelog/changes/03-create-roles-table.yaml create mode 100644 src/main/resources/db/changelog/changes/04-create-users-roles-table.yaml create mode 100644 src/main/resources/db/changelog/changes/05-insert-roles-into-database.yaml create mode 100644 src/main/resources/db/changelog/changes/06-insert-admin-user-into-database.yaml create mode 100644 src/main/resources/db/changelog/changes/07-assign-admin-user-to-role-admin.yaml diff --git a/pom.xml b/pom.xml index a129064..b5709df 100644 --- a/pom.xml +++ b/pom.xml @@ -89,6 +89,11 @@ hibernate-validator 8.0.1.Final + + org.springframework.boot + spring-boot-starter-security + 3.2.5 + diff --git a/src/main/java/mate/academy/bookstore/config/SecurityConfig.java b/src/main/java/mate/academy/bookstore/config/SecurityConfig.java new file mode 100644 index 0000000..1d88bd5 --- /dev/null +++ b/src/main/java/mate/academy/bookstore/config/SecurityConfig.java @@ -0,0 +1,46 @@ +package mate.academy.bookstore.config; + +import static org.springframework.security.config.Customizer.withDefaults; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; + +@EnableMethodSecurity +@Configuration +@RequiredArgsConstructor +public class SecurityConfig { + private final UserDetailsService userDetailsService; + + @Bean + public PasswordEncoder getPasswordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + return http + .cors(AbstractHttpConfigurer::disable) + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests( + auth -> auth + .requestMatchers("/auth/**", "/error", "/swagger-ui/**") + .permitAll() + .anyRequest() + .authenticated() + ) + .httpBasic(withDefaults()) + .sessionManagement( + session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .userDetailsService(userDetailsService) + .build(); + } +} diff --git a/src/main/java/mate/academy/bookstore/controller/AuthenticationController.java b/src/main/java/mate/academy/bookstore/controller/AuthenticationController.java index 341b960..8028336 100644 --- a/src/main/java/mate/academy/bookstore/controller/AuthenticationController.java +++ b/src/main/java/mate/academy/bookstore/controller/AuthenticationController.java @@ -18,7 +18,7 @@ @RequiredArgsConstructor @RestController @Tag(name = "Auth management", description = "Endpoints for authentication management") -@RequestMapping(value = "/api/auth") +@RequestMapping(value = "/auth") public class AuthenticationController { private final UserService userService; diff --git a/src/main/java/mate/academy/bookstore/controller/BookController.java b/src/main/java/mate/academy/bookstore/controller/BookController.java index aee43fe..953a2b8 100644 --- a/src/main/java/mate/academy/bookstore/controller/BookController.java +++ b/src/main/java/mate/academy/bookstore/controller/BookController.java @@ -11,6 +11,7 @@ import mate.academy.bookstore.service.BookService; import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -24,10 +25,11 @@ @RequiredArgsConstructor @RestController @Tag(name = "Book management", description = "Endpoints for book management") -@RequestMapping(value = "/api/books") +@RequestMapping(value = "/books") public class BookController { private final BookService bookService; + @PreAuthorize("hasRole('USER')") @GetMapping @ResponseStatus(HttpStatus.OK) @Operation(summary = "Get all books", description = "Get a list of all available books") @@ -35,6 +37,7 @@ public List getAll(Pageable pageable) { return bookService.findAll(pageable); } + @PreAuthorize("hasRole('USER')") @GetMapping("/{id}") @ResponseStatus(HttpStatus.OK) @Operation(summary = "Get a book by ID", description = "Get a book by ID, if there is one") @@ -42,6 +45,7 @@ public BookDto getBookById(@PathVariable Long id) { return bookService.findById(id); } + @PreAuthorize("hasRole('ADMIN')") @PostMapping @ResponseStatus(HttpStatus.CREATED) @Operation(summary = "Create a new book", description = "Create a new book with generated ID") @@ -49,6 +53,7 @@ public BookDto createBook(@RequestBody @Valid BookRequestDto requestDto) { return bookService.save(requestDto); } + @PreAuthorize("hasRole('ADMIN')") @PutMapping("/{id}") @ResponseStatus(HttpStatus.ACCEPTED) @Operation(summary = "Update a book by ID", description = "Update a book by its ID, if exists") @@ -57,6 +62,7 @@ public BookDto updateBook(@PathVariable Long id, return bookService.updateById(id, requestDto); } + @PreAuthorize("hasRole('ADMIN')") @DeleteMapping("/{id}") @ResponseStatus(HttpStatus.NO_CONTENT) @Operation(summary = "Delete a book by ID", description = "Delete a book by ID, if exists") diff --git a/src/main/java/mate/academy/bookstore/exception/GlobalExceptionHandler.java b/src/main/java/mate/academy/bookstore/exception/GlobalExceptionHandler.java index e9cf4e0..77ebcc3 100644 --- a/src/main/java/mate/academy/bookstore/exception/GlobalExceptionHandler.java +++ b/src/main/java/mate/academy/bookstore/exception/GlobalExceptionHandler.java @@ -8,6 +8,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatusCode; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.AccessDeniedException; import org.springframework.validation.FieldError; import org.springframework.validation.ObjectError; import org.springframework.web.bind.MethodArgumentNotValidException; @@ -43,10 +44,15 @@ protected ResponseEntity handleMethodNotFound(EntityNotFoundException ex } @ExceptionHandler(RegistrationException.class) - protected ResponseEntity handleMethodRegistrationException(RegistrationException ex) { + protected ResponseEntity handleMethodRegistration(RegistrationException ex) { return getResponseEntity(HttpStatus.CONFLICT, ex.getMessage()); } + @ExceptionHandler(AccessDeniedException.class) + protected ResponseEntity handleAccessDenied(AccessDeniedException ex) { + return getResponseEntity(HttpStatus.FORBIDDEN, ex.getMessage()); + } + private ResponseEntity getResponseEntity(HttpStatus status, Object message) { Map body = new LinkedHashMap<>(); body.put("timestamp", LocalDateTime.now()); diff --git a/src/main/java/mate/academy/bookstore/mapper/UserMapper.java b/src/main/java/mate/academy/bookstore/mapper/UserMapper.java index 9d56079..106cce1 100644 --- a/src/main/java/mate/academy/bookstore/mapper/UserMapper.java +++ b/src/main/java/mate/academy/bookstore/mapper/UserMapper.java @@ -12,5 +12,6 @@ public interface UserMapper { UserResponseDto toDto(User user); @Mapping(target = "id", ignore = true) + @Mapping(target = "roles", ignore = true) User toModel(UserRegistrationRequestDto requestDto); } diff --git a/src/main/java/mate/academy/bookstore/model/Role.java b/src/main/java/mate/academy/bookstore/model/Role.java new file mode 100644 index 0000000..038db0e --- /dev/null +++ b/src/main/java/mate/academy/bookstore/model/Role.java @@ -0,0 +1,32 @@ +package mate.academy.bookstore.model; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.Setter; +import org.springframework.security.core.GrantedAuthority; + +@Getter +@Setter +@Entity +@Table(name = "roles") +public class Role implements GrantedAuthority { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, unique = true) + @Enumerated(EnumType.STRING) + private RoleName role; + + @Override + public String getAuthority() { + return "ROLE_" + role.name(); + } +} diff --git a/src/main/java/mate/academy/bookstore/model/RoleName.java b/src/main/java/mate/academy/bookstore/model/RoleName.java new file mode 100644 index 0000000..aec09c2 --- /dev/null +++ b/src/main/java/mate/academy/bookstore/model/RoleName.java @@ -0,0 +1,6 @@ +package mate.academy.bookstore.model; + +public enum RoleName { + USER, + ADMIN +} diff --git a/src/main/java/mate/academy/bookstore/model/User.java b/src/main/java/mate/academy/bookstore/model/User.java index 657fef0..f4b9cb3 100644 --- a/src/main/java/mate/academy/bookstore/model/User.java +++ b/src/main/java/mate/academy/bookstore/model/User.java @@ -2,15 +2,23 @@ import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinTable; +import jakarta.persistence.ManyToMany; import jakarta.persistence.Table; +import java.util.Collection; +import java.util.Set; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import org.hibernate.annotations.SQLDelete; import org.hibernate.annotations.SQLRestriction; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; @Getter @Setter @@ -19,7 +27,7 @@ @SQLDelete(sql = "UPDATE users SET is_deleted=true WHERE id=?") @SQLRestriction(value = "is_deleted=false") @Table(name = "users") -public class User { +public class User implements UserDetails { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -38,6 +46,44 @@ public class User { private String shippingAddress; + @ManyToMany(fetch = FetchType.EAGER) + @JoinTable( + name = "users_roles", + joinColumns = @JoinColumn(name = "user_id"), + inverseJoinColumns = @JoinColumn(name = "role_id") + ) + private Set roles; + @Column(nullable = false) private boolean isDeleted; + + @Override + public Collection getAuthorities() { + return roles; + } + + @Override + public String getUsername() { + return email; + } + + @Override + public boolean isAccountNonExpired() { + return !isDeleted; + } + + @Override + public boolean isAccountNonLocked() { + return !isDeleted; + } + + @Override + public boolean isCredentialsNonExpired() { + return !isDeleted; + } + + @Override + public boolean isEnabled() { + return !isDeleted; + } } diff --git a/src/main/java/mate/academy/bookstore/repository/role/RoleRepository.java b/src/main/java/mate/academy/bookstore/repository/role/RoleRepository.java new file mode 100644 index 0000000..713514f --- /dev/null +++ b/src/main/java/mate/academy/bookstore/repository/role/RoleRepository.java @@ -0,0 +1,13 @@ +package mate.academy.bookstore.repository.role; + +import java.util.Set; +import mate.academy.bookstore.model.Role; +import mate.academy.bookstore.model.RoleName; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +public interface RoleRepository extends JpaRepository { + + @Query("SELECT r FROM Role r WHERE r.role IN :roles") + Set findAllByRoles(Set roles); +} diff --git a/src/main/java/mate/academy/bookstore/repository/user/UserRepository.java b/src/main/java/mate/academy/bookstore/repository/user/UserRepository.java index eb5e1ce..e638b10 100644 --- a/src/main/java/mate/academy/bookstore/repository/user/UserRepository.java +++ b/src/main/java/mate/academy/bookstore/repository/user/UserRepository.java @@ -1,8 +1,11 @@ package mate.academy.bookstore.repository.user; +import java.util.Optional; import mate.academy.bookstore.model.User; import org.springframework.data.jpa.repository.JpaRepository; public interface UserRepository extends JpaRepository { boolean existsByEmailIgnoreCase(String email); + + Optional findByEmail(String email); } diff --git a/src/main/java/mate/academy/bookstore/service/impl/UserDetailsServiceImpl.java b/src/main/java/mate/academy/bookstore/service/impl/UserDetailsServiceImpl.java new file mode 100644 index 0000000..6f216bf --- /dev/null +++ b/src/main/java/mate/academy/bookstore/service/impl/UserDetailsServiceImpl.java @@ -0,0 +1,20 @@ +package mate.academy.bookstore.service.impl; + +import lombok.RequiredArgsConstructor; +import mate.academy.bookstore.repository.user.UserRepository; +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; + +@RequiredArgsConstructor +@Service +public class UserDetailsServiceImpl implements UserDetailsService { + private final UserRepository userRepository; + + @Override + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + return userRepository.findByEmail(email).orElseThrow( + () -> new UsernameNotFoundException("Can't find user by email" + email)); + } +} diff --git a/src/main/java/mate/academy/bookstore/service/impl/UserServiceImpl.java b/src/main/java/mate/academy/bookstore/service/impl/UserServiceImpl.java index 8e0328c..d9b8e51 100644 --- a/src/main/java/mate/academy/bookstore/service/impl/UserServiceImpl.java +++ b/src/main/java/mate/academy/bookstore/service/impl/UserServiceImpl.java @@ -1,13 +1,17 @@ package mate.academy.bookstore.service.impl; +import java.util.Set; import lombok.RequiredArgsConstructor; import mate.academy.bookstore.dto.user.UserRegistrationRequestDto; import mate.academy.bookstore.dto.user.UserResponseDto; import mate.academy.bookstore.exception.RegistrationException; import mate.academy.bookstore.mapper.UserMapper; +import mate.academy.bookstore.model.RoleName; import mate.academy.bookstore.model.User; +import mate.academy.bookstore.repository.role.RoleRepository; import mate.academy.bookstore.repository.user.UserRepository; import mate.academy.bookstore.service.UserService; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component; @RequiredArgsConstructor @@ -15,6 +19,8 @@ public class UserServiceImpl implements UserService { private final UserRepository userRepository; private final UserMapper userMapper; + private final PasswordEncoder passwordEncoder; + private final RoleRepository roleRepository; @Override public UserResponseDto save(UserRegistrationRequestDto requestDto) @@ -26,6 +32,8 @@ public UserResponseDto save(UserRegistrationRequestDto requestDto) User user = userMapper.toModel(requestDto); user.setEmail(email); + user.setPassword(passwordEncoder.encode(user.getPassword())); + user.setRoles(roleRepository.findAllByRoles(Set.of(RoleName.USER))); User savedUser = userRepository.save(user); return userMapper.toDto(savedUser); } diff --git a/src/main/resources/db/changelog/changes/03-create-roles-table.yaml b/src/main/resources/db/changelog/changes/03-create-roles-table.yaml new file mode 100644 index 0000000..cc4f507 --- /dev/null +++ b/src/main/resources/db/changelog/changes/03-create-roles-table.yaml @@ -0,0 +1,20 @@ +databaseChangeLog: + - changeSet: + id: create-roles-table + author: nklimovych + changes: + - createTable: + tableName: roles + columns: + - column: + name: id + type: bigint + autoIncrement: true + constraints: + primaryKey: true + - column: + name: role + type: enum('USER', 'ADMIN') + constraints: + nullable: false + unique: true \ No newline at end of file diff --git a/src/main/resources/db/changelog/changes/04-create-users-roles-table.yaml b/src/main/resources/db/changelog/changes/04-create-users-roles-table.yaml new file mode 100644 index 0000000..e1c13c9 --- /dev/null +++ b/src/main/resources/db/changelog/changes/04-create-users-roles-table.yaml @@ -0,0 +1,22 @@ +databaseChangeLog: + - changeSet: + id: create-users-roles-table + author: nklimovych + changes: + - createTable: + tableName: users_roles + columns: + - column: + name: user_id + type: bigint + constraints: + nullable: false + references: users(id) + foreignKeyName: fk_users_role_id + - column: + name: role_id + type: bigint + constraints: + nullable: false + references: roles(id) + foreignKeyName: fk_roles_user_id diff --git a/src/main/resources/db/changelog/changes/05-insert-roles-into-database.yaml b/src/main/resources/db/changelog/changes/05-insert-roles-into-database.yaml new file mode 100644 index 0000000..2b256b5 --- /dev/null +++ b/src/main/resources/db/changelog/changes/05-insert-roles-into-database.yaml @@ -0,0 +1,17 @@ +databaseChangeLog: + - changeSet: + id: insert-roles-data + author: nklimovych + changes: + - insert: + tableName: roles + columns: + - column: + name: role + value: 'ADMIN' + - insert: + tableName: roles + columns: + - column: + name: role + value: 'USER' diff --git a/src/main/resources/db/changelog/changes/06-insert-admin-user-into-database.yaml b/src/main/resources/db/changelog/changes/06-insert-admin-user-into-database.yaml new file mode 100644 index 0000000..0cfab15 --- /dev/null +++ b/src/main/resources/db/changelog/changes/06-insert-admin-user-into-database.yaml @@ -0,0 +1,23 @@ +databaseChangeLog: + - changeSet: + id: insert-admin-user + author: nklimovych + changes: + - insert: + tableName: users + columns: + - column: + name: email + value: "admin@gmail.com" + - column: + name: password + value: "$2a$10$jylhh5qCNXLrDdpPLTWmvOxGJeDTWQiSS5rN8v5b0G12QF1FlgeCe" + - column: + name: first_name + value: "Admin" + - column: + name: last_name + value: "User" + - column: + name: shipping_address + value: "Truskavetska st. 117, Lviv" diff --git a/src/main/resources/db/changelog/changes/07-assign-admin-user-to-role-admin.yaml b/src/main/resources/db/changelog/changes/07-assign-admin-user-to-role-admin.yaml new file mode 100644 index 0000000..06c9410 --- /dev/null +++ b/src/main/resources/db/changelog/changes/07-assign-admin-user-to-role-admin.yaml @@ -0,0 +1,14 @@ +databaseChangeLog: + - changeSet: + id: insert-users_roles + author: nklimovych + changes: + - insert: + tableName: users_roles + columns: + - column: + name: user_id + valueComputed: (SELECT id FROM users WHERE email = 'admin@gmail.com') + - column: + name: role_id + valueComputed: (SELECT id FROM roles WHERE role = 'ADMIN') diff --git a/src/main/resources/db/changelog/db.changelog-master.yaml b/src/main/resources/db/changelog/db.changelog-master.yaml index 2b6f804..9794f55 100644 --- a/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/src/main/resources/db/changelog/db.changelog-master.yaml @@ -3,3 +3,13 @@ databaseChangeLog: file: db/changelog/changes/01-create-book-table.yaml - include: file: db/changelog/changes/02-create-user-table.yaml + - include: + file: db/changelog/changes/03-create-roles-table.yaml + - include: + file: db/changelog/changes/04-create-users-roles-table.yaml + - include: + file: db/changelog/changes/05-insert-roles-into-database.yaml + - include: + file: db/changelog/changes/06-insert-admin-user-into-database.yaml + - include: + file: db/changelog/changes/07-assign-admin-user-to-role-admin.yaml From b2c7f65f9f4aedc228cc0d1c98f42b1f647b546b Mon Sep 17 00:00:00 2001 From: Nazar Klimovych Date: Mon, 13 May 2024 02:17:18 +0300 Subject: [PATCH 02/12] Added JWT support --- pom.xml | 15 +++++ .../bookstore/config/SecurityConfig.java | 13 ++++ .../controller/AuthenticationController.java | 13 +++- .../bookstore/controller/BookController.java | 2 - .../dto/user/UserLoginRequestDto.java | 15 +++++ .../dto/user/UserLoginResponseDto.java | 4 ++ ....java => UserRegistrationResponseDto.java} | 2 +- .../academy/bookstore/mapper/UserMapper.java | 4 +- .../security/AuthenticationService.java | 25 ++++++++ .../security/JwtAuthenticationFilter.java | 49 +++++++++++++++ .../academy/bookstore/security/JwtUtil.java | 60 +++++++++++++++++++ .../bookstore/service/UserService.java | 5 +- .../service/impl/UserServiceImpl.java | 4 +- src/main/resources/application.properties | 3 + src/test/resources/application.properties | 3 + 15 files changed, 206 insertions(+), 11 deletions(-) create mode 100644 src/main/java/mate/academy/bookstore/dto/user/UserLoginRequestDto.java create mode 100644 src/main/java/mate/academy/bookstore/dto/user/UserLoginResponseDto.java rename src/main/java/mate/academy/bookstore/dto/user/{UserResponseDto.java => UserRegistrationResponseDto.java} (79%) create mode 100644 src/main/java/mate/academy/bookstore/security/AuthenticationService.java create mode 100644 src/main/java/mate/academy/bookstore/security/JwtAuthenticationFilter.java create mode 100644 src/main/java/mate/academy/bookstore/security/JwtUtil.java diff --git a/pom.xml b/pom.xml index b5709df..a4ef937 100644 --- a/pom.xml +++ b/pom.xml @@ -94,6 +94,21 @@ spring-boot-starter-security 3.2.5 + + io.jsonwebtoken + jjwt-api + 0.12.5 + + + io.jsonwebtoken + jjwt-impl + 0.12.5 + + + io.jsonwebtoken + jjwt-jackson + 0.12.5 + diff --git a/src/main/java/mate/academy/bookstore/config/SecurityConfig.java b/src/main/java/mate/academy/bookstore/config/SecurityConfig.java index 1d88bd5..dfbed91 100644 --- a/src/main/java/mate/academy/bookstore/config/SecurityConfig.java +++ b/src/main/java/mate/academy/bookstore/config/SecurityConfig.java @@ -3,8 +3,11 @@ import static org.springframework.security.config.Customizer.withDefaults; import lombok.RequiredArgsConstructor; +import mate.academy.bookstore.security.JwtAuthenticationFilter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; @@ -13,12 +16,14 @@ 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; @EnableMethodSecurity @Configuration @RequiredArgsConstructor public class SecurityConfig { private final UserDetailsService userDetailsService; + private final JwtAuthenticationFilter jwtAuthFilter; @Bean public PasswordEncoder getPasswordEncoder() { @@ -40,7 +45,15 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .httpBasic(withDefaults()) .sessionManagement( session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class) .userDetailsService(userDetailsService) .build(); } + + @Bean + public AuthenticationManager authenticationManager( + AuthenticationConfiguration authenticationConfiguration + ) throws Exception { + return authenticationConfiguration.getAuthenticationManager(); + } } diff --git a/src/main/java/mate/academy/bookstore/controller/AuthenticationController.java b/src/main/java/mate/academy/bookstore/controller/AuthenticationController.java index 8028336..ac012a8 100644 --- a/src/main/java/mate/academy/bookstore/controller/AuthenticationController.java +++ b/src/main/java/mate/academy/bookstore/controller/AuthenticationController.java @@ -4,9 +4,12 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import mate.academy.bookstore.dto.user.UserLoginRequestDto; +import mate.academy.bookstore.dto.user.UserLoginResponseDto; import mate.academy.bookstore.dto.user.UserRegistrationRequestDto; -import mate.academy.bookstore.dto.user.UserResponseDto; +import mate.academy.bookstore.dto.user.UserRegistrationResponseDto; import mate.academy.bookstore.exception.RegistrationException; +import mate.academy.bookstore.security.AuthenticationService; import mate.academy.bookstore.service.UserService; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.PostMapping; @@ -21,12 +24,18 @@ @RequestMapping(value = "/auth") public class AuthenticationController { private final UserService userService; + private final AuthenticationService authenticationService; @PostMapping("/registration") @ResponseStatus(HttpStatus.CREATED) @Operation(summary = "Register new user", description = "Register new users with unique email" - ) public UserResponseDto register( + ) public UserRegistrationResponseDto register( @RequestBody @Valid UserRegistrationRequestDto userDto) throws RegistrationException { return userService.save(userDto); } + + @PostMapping("/login") + public UserLoginResponseDto login(@RequestBody @Valid UserLoginRequestDto requestDto) { + return authenticationService.authenticate(requestDto); + } } diff --git a/src/main/java/mate/academy/bookstore/controller/BookController.java b/src/main/java/mate/academy/bookstore/controller/BookController.java index 953a2b8..85bd8b1 100644 --- a/src/main/java/mate/academy/bookstore/controller/BookController.java +++ b/src/main/java/mate/academy/bookstore/controller/BookController.java @@ -29,7 +29,6 @@ public class BookController { private final BookService bookService; - @PreAuthorize("hasRole('USER')") @GetMapping @ResponseStatus(HttpStatus.OK) @Operation(summary = "Get all books", description = "Get a list of all available books") @@ -37,7 +36,6 @@ public List getAll(Pageable pageable) { return bookService.findAll(pageable); } - @PreAuthorize("hasRole('USER')") @GetMapping("/{id}") @ResponseStatus(HttpStatus.OK) @Operation(summary = "Get a book by ID", description = "Get a book by ID, if there is one") diff --git a/src/main/java/mate/academy/bookstore/dto/user/UserLoginRequestDto.java b/src/main/java/mate/academy/bookstore/dto/user/UserLoginRequestDto.java new file mode 100644 index 0000000..eb644f5 --- /dev/null +++ b/src/main/java/mate/academy/bookstore/dto/user/UserLoginRequestDto.java @@ -0,0 +1,15 @@ +package mate.academy.bookstore.dto.user; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public record UserLoginRequestDto( + @NotBlank + @Email + String email, + @NotBlank + @Size(min = 8, message = "Password must be at least 8 characters long") + String password +) { +} diff --git a/src/main/java/mate/academy/bookstore/dto/user/UserLoginResponseDto.java b/src/main/java/mate/academy/bookstore/dto/user/UserLoginResponseDto.java new file mode 100644 index 0000000..a6ff0ff --- /dev/null +++ b/src/main/java/mate/academy/bookstore/dto/user/UserLoginResponseDto.java @@ -0,0 +1,4 @@ +package mate.academy.bookstore.dto.user; + +public record UserLoginResponseDto(String token) { +} diff --git a/src/main/java/mate/academy/bookstore/dto/user/UserResponseDto.java b/src/main/java/mate/academy/bookstore/dto/user/UserRegistrationResponseDto.java similarity index 79% rename from src/main/java/mate/academy/bookstore/dto/user/UserResponseDto.java rename to src/main/java/mate/academy/bookstore/dto/user/UserRegistrationResponseDto.java index db654c2..3782542 100644 --- a/src/main/java/mate/academy/bookstore/dto/user/UserResponseDto.java +++ b/src/main/java/mate/academy/bookstore/dto/user/UserRegistrationResponseDto.java @@ -1,6 +1,6 @@ package mate.academy.bookstore.dto.user; -public record UserResponseDto( +public record UserRegistrationResponseDto( Long id, String email, String firstName, diff --git a/src/main/java/mate/academy/bookstore/mapper/UserMapper.java b/src/main/java/mate/academy/bookstore/mapper/UserMapper.java index 106cce1..3f2e30e 100644 --- a/src/main/java/mate/academy/bookstore/mapper/UserMapper.java +++ b/src/main/java/mate/academy/bookstore/mapper/UserMapper.java @@ -2,14 +2,14 @@ import mate.academy.bookstore.config.MapperConfig; import mate.academy.bookstore.dto.user.UserRegistrationRequestDto; -import mate.academy.bookstore.dto.user.UserResponseDto; +import mate.academy.bookstore.dto.user.UserRegistrationResponseDto; import mate.academy.bookstore.model.User; import org.mapstruct.Mapper; import org.mapstruct.Mapping; @Mapper(config = MapperConfig.class) public interface UserMapper { - UserResponseDto toDto(User user); + UserRegistrationResponseDto toDto(User user); @Mapping(target = "id", ignore = true) @Mapping(target = "roles", ignore = true) diff --git a/src/main/java/mate/academy/bookstore/security/AuthenticationService.java b/src/main/java/mate/academy/bookstore/security/AuthenticationService.java new file mode 100644 index 0000000..fd843a3 --- /dev/null +++ b/src/main/java/mate/academy/bookstore/security/AuthenticationService.java @@ -0,0 +1,25 @@ +package mate.academy.bookstore.security; + +import lombok.RequiredArgsConstructor; +import mate.academy.bookstore.dto.user.UserLoginRequestDto; +import mate.academy.bookstore.dto.user.UserLoginResponseDto; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class AuthenticationService { + private final JwtUtil jwtUtil; + private final AuthenticationManager authenticationManager; + + public UserLoginResponseDto authenticate(UserLoginRequestDto request) { + final Authentication authentication = authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(request.email(), request.password()) + ); + + String token = jwtUtil.generateToken(authentication.getName()); + return new UserLoginResponseDto(token); + } +} diff --git a/src/main/java/mate/academy/bookstore/security/JwtAuthenticationFilter.java b/src/main/java/mate/academy/bookstore/security/JwtAuthenticationFilter.java new file mode 100644 index 0000000..e5eeb02 --- /dev/null +++ b/src/main/java/mate/academy/bookstore/security/JwtAuthenticationFilter.java @@ -0,0 +1,49 @@ +package mate.academy.bookstore.security; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +@RequiredArgsConstructor +@Component +public class JwtAuthenticationFilter extends OncePerRequestFilter { + private final JwtUtil jwtUtil; + private final UserDetailsService userDetailsService; + + @Override + protected void doFilterInternal( + HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain + ) throws ServletException, IOException { + String token = getToken(request); + + if (token != null && jwtUtil.isValidToken(token)) { + String username = jwtUtil.getUsername(token); + UserDetails userDetails = userDetailsService.loadUserByUsername(username); + Authentication authentication = new UsernamePasswordAuthenticationToken( + userDetails, null, userDetails.getAuthorities() + ); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + filterChain.doFilter(request, response); + } + + private String getToken(HttpServletRequest request) { + String bearerToken = request.getHeader("Authorization"); + if (bearerToken != null && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring(7); + } + return null; + } +} diff --git a/src/main/java/mate/academy/bookstore/security/JwtUtil.java b/src/main/java/mate/academy/bookstore/security/JwtUtil.java new file mode 100644 index 0000000..73aa6d8 --- /dev/null +++ b/src/main/java/mate/academy/bookstore/security/JwtUtil.java @@ -0,0 +1,60 @@ +package mate.academy.bookstore.security; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import java.nio.charset.StandardCharsets; +import java.security.Key; +import java.util.Date; +import java.util.function.Function; +import javax.crypto.SecretKey; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class JwtUtil { + private final Key secret; + + @Value("${jwt.expiration}") + private Long expiration; + + public JwtUtil(@Value("${jwt.secret}") String secretKey) { + secret = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8)); + } + + public String generateToken(String username) { + return Jwts.builder() + .subject(username) + .issuedAt(new Date(System.currentTimeMillis())) + .expiration(new Date(System.currentTimeMillis() + expiration)) + .signWith(secret) + .compact(); + } + + public boolean isValidToken(String token) { + try { + Jws claimsJws = Jwts.parser() + .verifyWith((SecretKey) secret) + .build() + .parseSignedClaims(token); + return !claimsJws.getPayload().getExpiration().before(new Date()); + } catch (JwtException | IllegalArgumentException e) { + throw new JwtException("Expired or invalid JWT token"); + } + } + + public String getUsername(String token) { + return getClaimsFromToken(token, Claims::getSubject); + } + + private T getClaimsFromToken(String token, Function claimsResolver) { + final Claims claims = Jwts.parser() + .verifyWith((SecretKey) secret) + .build() + .parseSignedClaims(token) + .getPayload(); + return claimsResolver.apply(claims); + } +} diff --git a/src/main/java/mate/academy/bookstore/service/UserService.java b/src/main/java/mate/academy/bookstore/service/UserService.java index af6aa1a..53f89a0 100644 --- a/src/main/java/mate/academy/bookstore/service/UserService.java +++ b/src/main/java/mate/academy/bookstore/service/UserService.java @@ -1,9 +1,10 @@ package mate.academy.bookstore.service; import mate.academy.bookstore.dto.user.UserRegistrationRequestDto; -import mate.academy.bookstore.dto.user.UserResponseDto; +import mate.academy.bookstore.dto.user.UserRegistrationResponseDto; import mate.academy.bookstore.exception.RegistrationException; public interface UserService { - UserResponseDto save(UserRegistrationRequestDto requestDto) throws RegistrationException; + UserRegistrationResponseDto save(UserRegistrationRequestDto requestDto) + throws RegistrationException; } diff --git a/src/main/java/mate/academy/bookstore/service/impl/UserServiceImpl.java b/src/main/java/mate/academy/bookstore/service/impl/UserServiceImpl.java index d9b8e51..cfce884 100644 --- a/src/main/java/mate/academy/bookstore/service/impl/UserServiceImpl.java +++ b/src/main/java/mate/academy/bookstore/service/impl/UserServiceImpl.java @@ -3,7 +3,7 @@ import java.util.Set; import lombok.RequiredArgsConstructor; import mate.academy.bookstore.dto.user.UserRegistrationRequestDto; -import mate.academy.bookstore.dto.user.UserResponseDto; +import mate.academy.bookstore.dto.user.UserRegistrationResponseDto; import mate.academy.bookstore.exception.RegistrationException; import mate.academy.bookstore.mapper.UserMapper; import mate.academy.bookstore.model.RoleName; @@ -23,7 +23,7 @@ public class UserServiceImpl implements UserService { private final RoleRepository roleRepository; @Override - public UserResponseDto save(UserRegistrationRequestDto requestDto) + public UserRegistrationResponseDto save(UserRegistrationRequestDto requestDto) throws RegistrationException { String email = requestDto.getEmail(); if (userRepository.existsByEmailIgnoreCase(email)) { diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 17debb9..95dd7a0 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -9,3 +9,6 @@ spring.jpa.show-sql=true spring.jpa.open-in-view=false server.servlet.context-path=/api + +jwt.expiration=300000 +jwt.secret=${JWT_SECRET} diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index bc2fdde..6b43605 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -3,3 +3,6 @@ spring.datasource.driverClassName=org.h2.Driver spring.datasource.username=sa spring.datasource.password=password spring.jpa.database-platform=org.hibernate.dialect.H2Dialect + +jwt.expiration=300000 +jwt.secret=JustAnotherSuperSecretString1234! From 9ef8fb5f31ce87b5ee00a35a3a18d7dc55938f29 Mon Sep 17 00:00:00 2001 From: Nazar Klimovych Date: Mon, 13 May 2024 02:33:43 +0300 Subject: [PATCH 03/12] Added handling of JwtException and AuthenticationException in GlobalExceptionHandler --- .../bookstore/exception/GlobalExceptionHandler.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/java/mate/academy/bookstore/exception/GlobalExceptionHandler.java b/src/main/java/mate/academy/bookstore/exception/GlobalExceptionHandler.java index 77ebcc3..d115095 100644 --- a/src/main/java/mate/academy/bookstore/exception/GlobalExceptionHandler.java +++ b/src/main/java/mate/academy/bookstore/exception/GlobalExceptionHandler.java @@ -1,5 +1,6 @@ package mate.academy.bookstore.exception; +import io.jsonwebtoken.JwtException; import java.time.LocalDateTime; import java.util.LinkedHashMap; import java.util.List; @@ -9,6 +10,7 @@ import org.springframework.http.HttpStatusCode; import org.springframework.http.ResponseEntity; import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.core.AuthenticationException; import org.springframework.validation.FieldError; import org.springframework.validation.ObjectError; import org.springframework.web.bind.MethodArgumentNotValidException; @@ -53,6 +55,11 @@ protected ResponseEntity handleAccessDenied(AccessDeniedException ex) { return getResponseEntity(HttpStatus.FORBIDDEN, ex.getMessage()); } + @ExceptionHandler({JwtException.class, AuthenticationException.class}) + protected ResponseEntity handleAuthentication(Exception ex) { + return getResponseEntity(HttpStatus.UNAUTHORIZED, ex.getMessage()); + } + private ResponseEntity getResponseEntity(HttpStatus status, Object message) { Map body = new LinkedHashMap<>(); body.put("timestamp", LocalDateTime.now()); From 8bb0b4b4c80f9f49f278018f2e14115fd8037569 Mon Sep 17 00:00:00 2001 From: Nazar Klimovych Date: Mon, 13 May 2024 10:37:04 +0300 Subject: [PATCH 04/12] Removed redundant Key to SecretKey cast in JwtUtil in lines 38 & 53 --- src/main/java/mate/academy/bookstore/security/JwtUtil.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main/java/mate/academy/bookstore/security/JwtUtil.java b/src/main/java/mate/academy/bookstore/security/JwtUtil.java index 73aa6d8..8b16cd9 100644 --- a/src/main/java/mate/academy/bookstore/security/JwtUtil.java +++ b/src/main/java/mate/academy/bookstore/security/JwtUtil.java @@ -6,7 +6,6 @@ import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; import java.nio.charset.StandardCharsets; -import java.security.Key; import java.util.Date; import java.util.function.Function; import javax.crypto.SecretKey; @@ -15,7 +14,7 @@ @Component public class JwtUtil { - private final Key secret; + private final SecretKey secret; @Value("${jwt.expiration}") private Long expiration; @@ -36,7 +35,7 @@ public String generateToken(String username) { public boolean isValidToken(String token) { try { Jws claimsJws = Jwts.parser() - .verifyWith((SecretKey) secret) + .verifyWith(secret) .build() .parseSignedClaims(token); return !claimsJws.getPayload().getExpiration().before(new Date()); @@ -51,7 +50,7 @@ public String getUsername(String token) { private T getClaimsFromToken(String token, Function claimsResolver) { final Claims claims = Jwts.parser() - .verifyWith((SecretKey) secret) + .verifyWith(secret) .build() .parseSignedClaims(token) .getPayload(); From b574464dc5f8ed3798e1eaaa67cc02afc3f9cc3a Mon Sep 17 00:00:00 2001 From: Nazar Klimovych Date: Tue, 14 May 2024 11:19:17 +0300 Subject: [PATCH 05/12] Added swagger request matchers --- .../mate/academy/bookstore/config/SecurityConfig.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/mate/academy/bookstore/config/SecurityConfig.java b/src/main/java/mate/academy/bookstore/config/SecurityConfig.java index dfbed91..0053ec0 100644 --- a/src/main/java/mate/academy/bookstore/config/SecurityConfig.java +++ b/src/main/java/mate/academy/bookstore/config/SecurityConfig.java @@ -37,7 +37,14 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests( auth -> auth - .requestMatchers("/auth/**", "/error", "/swagger-ui/**") + .requestMatchers( + "/auth/**", + "/error", + "/swagger-ui/**", + "/v3/api-docs/**", + "/swagger-resources/**", + "/swagger-ui.html", + "/webjars/**") .permitAll() .anyRequest() .authenticated() From 6cd74195865754d0c2ee5f91b67d439c8b6991d1 Mon Sep 17 00:00:00 2001 From: Nazar Klimovych Date: Fri, 10 May 2024 19:58:41 +0300 Subject: [PATCH 06/12] Added SecurityConfig class with PasswordEncoder and SecurityFilterChain beans. Implemented UserDetails interface in the User class. Added the implementation for UserDetailsService interface. Added Role entity. Added RoleRepository. Added roles field to the User entity. Added Liquibase changesets to insert roles into DB and assign roles for users. --- .../java/mate/academy/bookstore/controller/BookController.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/mate/academy/bookstore/controller/BookController.java b/src/main/java/mate/academy/bookstore/controller/BookController.java index 85bd8b1..953a2b8 100644 --- a/src/main/java/mate/academy/bookstore/controller/BookController.java +++ b/src/main/java/mate/academy/bookstore/controller/BookController.java @@ -29,6 +29,7 @@ public class BookController { private final BookService bookService; + @PreAuthorize("hasRole('USER')") @GetMapping @ResponseStatus(HttpStatus.OK) @Operation(summary = "Get all books", description = "Get a list of all available books") @@ -36,6 +37,7 @@ public List getAll(Pageable pageable) { return bookService.findAll(pageable); } + @PreAuthorize("hasRole('USER')") @GetMapping("/{id}") @ResponseStatus(HttpStatus.OK) @Operation(summary = "Get a book by ID", description = "Get a book by ID, if there is one") From d2e8e3c503a4042812d1be23159c3d6d421a1e2e Mon Sep 17 00:00:00 2001 From: Nazar Klimovych Date: Mon, 13 May 2024 02:17:18 +0300 Subject: [PATCH 07/12] Added JWT support --- pom.xml | 15 +++++ .../bookstore/config/SecurityConfig.java | 13 ++++ .../controller/AuthenticationController.java | 13 +++- .../bookstore/controller/BookController.java | 2 - .../dto/user/UserLoginRequestDto.java | 15 +++++ .../dto/user/UserLoginResponseDto.java | 4 ++ ....java => UserRegistrationResponseDto.java} | 2 +- .../academy/bookstore/mapper/UserMapper.java | 4 +- .../security/AuthenticationService.java | 25 ++++++++ .../security/JwtAuthenticationFilter.java | 49 +++++++++++++++ .../academy/bookstore/security/JwtUtil.java | 60 +++++++++++++++++++ .../bookstore/service/UserService.java | 5 +- .../service/impl/UserServiceImpl.java | 4 +- src/main/resources/application.properties | 3 + src/test/resources/application.properties | 3 + 15 files changed, 206 insertions(+), 11 deletions(-) create mode 100644 src/main/java/mate/academy/bookstore/dto/user/UserLoginRequestDto.java create mode 100644 src/main/java/mate/academy/bookstore/dto/user/UserLoginResponseDto.java rename src/main/java/mate/academy/bookstore/dto/user/{UserResponseDto.java => UserRegistrationResponseDto.java} (79%) create mode 100644 src/main/java/mate/academy/bookstore/security/AuthenticationService.java create mode 100644 src/main/java/mate/academy/bookstore/security/JwtAuthenticationFilter.java create mode 100644 src/main/java/mate/academy/bookstore/security/JwtUtil.java diff --git a/pom.xml b/pom.xml index b5709df..a4ef937 100644 --- a/pom.xml +++ b/pom.xml @@ -94,6 +94,21 @@ spring-boot-starter-security 3.2.5 + + io.jsonwebtoken + jjwt-api + 0.12.5 + + + io.jsonwebtoken + jjwt-impl + 0.12.5 + + + io.jsonwebtoken + jjwt-jackson + 0.12.5 + diff --git a/src/main/java/mate/academy/bookstore/config/SecurityConfig.java b/src/main/java/mate/academy/bookstore/config/SecurityConfig.java index 1d88bd5..dfbed91 100644 --- a/src/main/java/mate/academy/bookstore/config/SecurityConfig.java +++ b/src/main/java/mate/academy/bookstore/config/SecurityConfig.java @@ -3,8 +3,11 @@ import static org.springframework.security.config.Customizer.withDefaults; import lombok.RequiredArgsConstructor; +import mate.academy.bookstore.security.JwtAuthenticationFilter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; @@ -13,12 +16,14 @@ 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; @EnableMethodSecurity @Configuration @RequiredArgsConstructor public class SecurityConfig { private final UserDetailsService userDetailsService; + private final JwtAuthenticationFilter jwtAuthFilter; @Bean public PasswordEncoder getPasswordEncoder() { @@ -40,7 +45,15 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .httpBasic(withDefaults()) .sessionManagement( session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class) .userDetailsService(userDetailsService) .build(); } + + @Bean + public AuthenticationManager authenticationManager( + AuthenticationConfiguration authenticationConfiguration + ) throws Exception { + return authenticationConfiguration.getAuthenticationManager(); + } } diff --git a/src/main/java/mate/academy/bookstore/controller/AuthenticationController.java b/src/main/java/mate/academy/bookstore/controller/AuthenticationController.java index 8028336..ac012a8 100644 --- a/src/main/java/mate/academy/bookstore/controller/AuthenticationController.java +++ b/src/main/java/mate/academy/bookstore/controller/AuthenticationController.java @@ -4,9 +4,12 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import mate.academy.bookstore.dto.user.UserLoginRequestDto; +import mate.academy.bookstore.dto.user.UserLoginResponseDto; import mate.academy.bookstore.dto.user.UserRegistrationRequestDto; -import mate.academy.bookstore.dto.user.UserResponseDto; +import mate.academy.bookstore.dto.user.UserRegistrationResponseDto; import mate.academy.bookstore.exception.RegistrationException; +import mate.academy.bookstore.security.AuthenticationService; import mate.academy.bookstore.service.UserService; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.PostMapping; @@ -21,12 +24,18 @@ @RequestMapping(value = "/auth") public class AuthenticationController { private final UserService userService; + private final AuthenticationService authenticationService; @PostMapping("/registration") @ResponseStatus(HttpStatus.CREATED) @Operation(summary = "Register new user", description = "Register new users with unique email" - ) public UserResponseDto register( + ) public UserRegistrationResponseDto register( @RequestBody @Valid UserRegistrationRequestDto userDto) throws RegistrationException { return userService.save(userDto); } + + @PostMapping("/login") + public UserLoginResponseDto login(@RequestBody @Valid UserLoginRequestDto requestDto) { + return authenticationService.authenticate(requestDto); + } } diff --git a/src/main/java/mate/academy/bookstore/controller/BookController.java b/src/main/java/mate/academy/bookstore/controller/BookController.java index 953a2b8..85bd8b1 100644 --- a/src/main/java/mate/academy/bookstore/controller/BookController.java +++ b/src/main/java/mate/academy/bookstore/controller/BookController.java @@ -29,7 +29,6 @@ public class BookController { private final BookService bookService; - @PreAuthorize("hasRole('USER')") @GetMapping @ResponseStatus(HttpStatus.OK) @Operation(summary = "Get all books", description = "Get a list of all available books") @@ -37,7 +36,6 @@ public List getAll(Pageable pageable) { return bookService.findAll(pageable); } - @PreAuthorize("hasRole('USER')") @GetMapping("/{id}") @ResponseStatus(HttpStatus.OK) @Operation(summary = "Get a book by ID", description = "Get a book by ID, if there is one") diff --git a/src/main/java/mate/academy/bookstore/dto/user/UserLoginRequestDto.java b/src/main/java/mate/academy/bookstore/dto/user/UserLoginRequestDto.java new file mode 100644 index 0000000..eb644f5 --- /dev/null +++ b/src/main/java/mate/academy/bookstore/dto/user/UserLoginRequestDto.java @@ -0,0 +1,15 @@ +package mate.academy.bookstore.dto.user; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public record UserLoginRequestDto( + @NotBlank + @Email + String email, + @NotBlank + @Size(min = 8, message = "Password must be at least 8 characters long") + String password +) { +} diff --git a/src/main/java/mate/academy/bookstore/dto/user/UserLoginResponseDto.java b/src/main/java/mate/academy/bookstore/dto/user/UserLoginResponseDto.java new file mode 100644 index 0000000..a6ff0ff --- /dev/null +++ b/src/main/java/mate/academy/bookstore/dto/user/UserLoginResponseDto.java @@ -0,0 +1,4 @@ +package mate.academy.bookstore.dto.user; + +public record UserLoginResponseDto(String token) { +} diff --git a/src/main/java/mate/academy/bookstore/dto/user/UserResponseDto.java b/src/main/java/mate/academy/bookstore/dto/user/UserRegistrationResponseDto.java similarity index 79% rename from src/main/java/mate/academy/bookstore/dto/user/UserResponseDto.java rename to src/main/java/mate/academy/bookstore/dto/user/UserRegistrationResponseDto.java index db654c2..3782542 100644 --- a/src/main/java/mate/academy/bookstore/dto/user/UserResponseDto.java +++ b/src/main/java/mate/academy/bookstore/dto/user/UserRegistrationResponseDto.java @@ -1,6 +1,6 @@ package mate.academy.bookstore.dto.user; -public record UserResponseDto( +public record UserRegistrationResponseDto( Long id, String email, String firstName, diff --git a/src/main/java/mate/academy/bookstore/mapper/UserMapper.java b/src/main/java/mate/academy/bookstore/mapper/UserMapper.java index 106cce1..3f2e30e 100644 --- a/src/main/java/mate/academy/bookstore/mapper/UserMapper.java +++ b/src/main/java/mate/academy/bookstore/mapper/UserMapper.java @@ -2,14 +2,14 @@ import mate.academy.bookstore.config.MapperConfig; import mate.academy.bookstore.dto.user.UserRegistrationRequestDto; -import mate.academy.bookstore.dto.user.UserResponseDto; +import mate.academy.bookstore.dto.user.UserRegistrationResponseDto; import mate.academy.bookstore.model.User; import org.mapstruct.Mapper; import org.mapstruct.Mapping; @Mapper(config = MapperConfig.class) public interface UserMapper { - UserResponseDto toDto(User user); + UserRegistrationResponseDto toDto(User user); @Mapping(target = "id", ignore = true) @Mapping(target = "roles", ignore = true) diff --git a/src/main/java/mate/academy/bookstore/security/AuthenticationService.java b/src/main/java/mate/academy/bookstore/security/AuthenticationService.java new file mode 100644 index 0000000..fd843a3 --- /dev/null +++ b/src/main/java/mate/academy/bookstore/security/AuthenticationService.java @@ -0,0 +1,25 @@ +package mate.academy.bookstore.security; + +import lombok.RequiredArgsConstructor; +import mate.academy.bookstore.dto.user.UserLoginRequestDto; +import mate.academy.bookstore.dto.user.UserLoginResponseDto; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class AuthenticationService { + private final JwtUtil jwtUtil; + private final AuthenticationManager authenticationManager; + + public UserLoginResponseDto authenticate(UserLoginRequestDto request) { + final Authentication authentication = authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(request.email(), request.password()) + ); + + String token = jwtUtil.generateToken(authentication.getName()); + return new UserLoginResponseDto(token); + } +} diff --git a/src/main/java/mate/academy/bookstore/security/JwtAuthenticationFilter.java b/src/main/java/mate/academy/bookstore/security/JwtAuthenticationFilter.java new file mode 100644 index 0000000..e5eeb02 --- /dev/null +++ b/src/main/java/mate/academy/bookstore/security/JwtAuthenticationFilter.java @@ -0,0 +1,49 @@ +package mate.academy.bookstore.security; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +@RequiredArgsConstructor +@Component +public class JwtAuthenticationFilter extends OncePerRequestFilter { + private final JwtUtil jwtUtil; + private final UserDetailsService userDetailsService; + + @Override + protected void doFilterInternal( + HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain + ) throws ServletException, IOException { + String token = getToken(request); + + if (token != null && jwtUtil.isValidToken(token)) { + String username = jwtUtil.getUsername(token); + UserDetails userDetails = userDetailsService.loadUserByUsername(username); + Authentication authentication = new UsernamePasswordAuthenticationToken( + userDetails, null, userDetails.getAuthorities() + ); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + filterChain.doFilter(request, response); + } + + private String getToken(HttpServletRequest request) { + String bearerToken = request.getHeader("Authorization"); + if (bearerToken != null && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring(7); + } + return null; + } +} diff --git a/src/main/java/mate/academy/bookstore/security/JwtUtil.java b/src/main/java/mate/academy/bookstore/security/JwtUtil.java new file mode 100644 index 0000000..73aa6d8 --- /dev/null +++ b/src/main/java/mate/academy/bookstore/security/JwtUtil.java @@ -0,0 +1,60 @@ +package mate.academy.bookstore.security; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import java.nio.charset.StandardCharsets; +import java.security.Key; +import java.util.Date; +import java.util.function.Function; +import javax.crypto.SecretKey; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class JwtUtil { + private final Key secret; + + @Value("${jwt.expiration}") + private Long expiration; + + public JwtUtil(@Value("${jwt.secret}") String secretKey) { + secret = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8)); + } + + public String generateToken(String username) { + return Jwts.builder() + .subject(username) + .issuedAt(new Date(System.currentTimeMillis())) + .expiration(new Date(System.currentTimeMillis() + expiration)) + .signWith(secret) + .compact(); + } + + public boolean isValidToken(String token) { + try { + Jws claimsJws = Jwts.parser() + .verifyWith((SecretKey) secret) + .build() + .parseSignedClaims(token); + return !claimsJws.getPayload().getExpiration().before(new Date()); + } catch (JwtException | IllegalArgumentException e) { + throw new JwtException("Expired or invalid JWT token"); + } + } + + public String getUsername(String token) { + return getClaimsFromToken(token, Claims::getSubject); + } + + private T getClaimsFromToken(String token, Function claimsResolver) { + final Claims claims = Jwts.parser() + .verifyWith((SecretKey) secret) + .build() + .parseSignedClaims(token) + .getPayload(); + return claimsResolver.apply(claims); + } +} diff --git a/src/main/java/mate/academy/bookstore/service/UserService.java b/src/main/java/mate/academy/bookstore/service/UserService.java index af6aa1a..53f89a0 100644 --- a/src/main/java/mate/academy/bookstore/service/UserService.java +++ b/src/main/java/mate/academy/bookstore/service/UserService.java @@ -1,9 +1,10 @@ package mate.academy.bookstore.service; import mate.academy.bookstore.dto.user.UserRegistrationRequestDto; -import mate.academy.bookstore.dto.user.UserResponseDto; +import mate.academy.bookstore.dto.user.UserRegistrationResponseDto; import mate.academy.bookstore.exception.RegistrationException; public interface UserService { - UserResponseDto save(UserRegistrationRequestDto requestDto) throws RegistrationException; + UserRegistrationResponseDto save(UserRegistrationRequestDto requestDto) + throws RegistrationException; } diff --git a/src/main/java/mate/academy/bookstore/service/impl/UserServiceImpl.java b/src/main/java/mate/academy/bookstore/service/impl/UserServiceImpl.java index ca3f963..dc9f322 100644 --- a/src/main/java/mate/academy/bookstore/service/impl/UserServiceImpl.java +++ b/src/main/java/mate/academy/bookstore/service/impl/UserServiceImpl.java @@ -3,7 +3,7 @@ import java.util.Collections; import lombok.RequiredArgsConstructor; import mate.academy.bookstore.dto.user.UserRegistrationRequestDto; -import mate.academy.bookstore.dto.user.UserResponseDto; +import mate.academy.bookstore.dto.user.UserRegistrationResponseDto; import mate.academy.bookstore.exception.RegistrationException; import mate.academy.bookstore.mapper.UserMapper; import mate.academy.bookstore.model.RoleName; @@ -23,7 +23,7 @@ public class UserServiceImpl implements UserService { private final RoleRepository roleRepository; @Override - public UserResponseDto save(UserRegistrationRequestDto requestDto) + public UserRegistrationResponseDto save(UserRegistrationRequestDto requestDto) throws RegistrationException { String email = requestDto.getEmail(); if (userRepository.existsByEmailIgnoreCase(email)) { diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 17debb9..95dd7a0 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -9,3 +9,6 @@ spring.jpa.show-sql=true spring.jpa.open-in-view=false server.servlet.context-path=/api + +jwt.expiration=300000 +jwt.secret=${JWT_SECRET} diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index bc2fdde..6b43605 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -3,3 +3,6 @@ spring.datasource.driverClassName=org.h2.Driver spring.datasource.username=sa spring.datasource.password=password spring.jpa.database-platform=org.hibernate.dialect.H2Dialect + +jwt.expiration=300000 +jwt.secret=JustAnotherSuperSecretString1234! From 53077bbc12b6da983250f12b2d76581eea9e4296 Mon Sep 17 00:00:00 2001 From: Nazar Klimovych Date: Mon, 13 May 2024 02:33:43 +0300 Subject: [PATCH 08/12] Added handling of JwtException and AuthenticationException in GlobalExceptionHandler --- .../bookstore/exception/GlobalExceptionHandler.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/java/mate/academy/bookstore/exception/GlobalExceptionHandler.java b/src/main/java/mate/academy/bookstore/exception/GlobalExceptionHandler.java index 77ebcc3..d115095 100644 --- a/src/main/java/mate/academy/bookstore/exception/GlobalExceptionHandler.java +++ b/src/main/java/mate/academy/bookstore/exception/GlobalExceptionHandler.java @@ -1,5 +1,6 @@ package mate.academy.bookstore.exception; +import io.jsonwebtoken.JwtException; import java.time.LocalDateTime; import java.util.LinkedHashMap; import java.util.List; @@ -9,6 +10,7 @@ import org.springframework.http.HttpStatusCode; import org.springframework.http.ResponseEntity; import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.core.AuthenticationException; import org.springframework.validation.FieldError; import org.springframework.validation.ObjectError; import org.springframework.web.bind.MethodArgumentNotValidException; @@ -53,6 +55,11 @@ protected ResponseEntity handleAccessDenied(AccessDeniedException ex) { return getResponseEntity(HttpStatus.FORBIDDEN, ex.getMessage()); } + @ExceptionHandler({JwtException.class, AuthenticationException.class}) + protected ResponseEntity handleAuthentication(Exception ex) { + return getResponseEntity(HttpStatus.UNAUTHORIZED, ex.getMessage()); + } + private ResponseEntity getResponseEntity(HttpStatus status, Object message) { Map body = new LinkedHashMap<>(); body.put("timestamp", LocalDateTime.now()); From 926837c025a82443bfc1322e017ed9056446e470 Mon Sep 17 00:00:00 2001 From: Nazar Klimovych Date: Mon, 13 May 2024 10:37:04 +0300 Subject: [PATCH 09/12] Removed redundant Key to SecretKey cast in JwtUtil in lines 38 & 53 --- src/main/java/mate/academy/bookstore/security/JwtUtil.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main/java/mate/academy/bookstore/security/JwtUtil.java b/src/main/java/mate/academy/bookstore/security/JwtUtil.java index 73aa6d8..8b16cd9 100644 --- a/src/main/java/mate/academy/bookstore/security/JwtUtil.java +++ b/src/main/java/mate/academy/bookstore/security/JwtUtil.java @@ -6,7 +6,6 @@ import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; import java.nio.charset.StandardCharsets; -import java.security.Key; import java.util.Date; import java.util.function.Function; import javax.crypto.SecretKey; @@ -15,7 +14,7 @@ @Component public class JwtUtil { - private final Key secret; + private final SecretKey secret; @Value("${jwt.expiration}") private Long expiration; @@ -36,7 +35,7 @@ public String generateToken(String username) { public boolean isValidToken(String token) { try { Jws claimsJws = Jwts.parser() - .verifyWith((SecretKey) secret) + .verifyWith(secret) .build() .parseSignedClaims(token); return !claimsJws.getPayload().getExpiration().before(new Date()); @@ -51,7 +50,7 @@ public String getUsername(String token) { private T getClaimsFromToken(String token, Function claimsResolver) { final Claims claims = Jwts.parser() - .verifyWith((SecretKey) secret) + .verifyWith(secret) .build() .parseSignedClaims(token) .getPayload(); From 6969d928cdbc27aad7e268d46fee0d892c66e9ec Mon Sep 17 00:00:00 2001 From: Nazar Klimovych Date: Tue, 14 May 2024 11:19:17 +0300 Subject: [PATCH 10/12] Added swagger request matchers --- .../mate/academy/bookstore/config/SecurityConfig.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/mate/academy/bookstore/config/SecurityConfig.java b/src/main/java/mate/academy/bookstore/config/SecurityConfig.java index dfbed91..0053ec0 100644 --- a/src/main/java/mate/academy/bookstore/config/SecurityConfig.java +++ b/src/main/java/mate/academy/bookstore/config/SecurityConfig.java @@ -37,7 +37,14 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests( auth -> auth - .requestMatchers("/auth/**", "/error", "/swagger-ui/**") + .requestMatchers( + "/auth/**", + "/error", + "/swagger-ui/**", + "/v3/api-docs/**", + "/swagger-resources/**", + "/swagger-ui.html", + "/webjars/**") .permitAll() .anyRequest() .authenticated() From bcf91b9136c848f1a203f3a613978c32df70d696 Mon Sep 17 00:00:00 2001 From: Nazar Klimovych Date: Tue, 14 May 2024 15:38:48 +0300 Subject: [PATCH 11/12] Code refactored according to mentor suggestions. Added token prefix and auth header strings to constant. Removed redundant AuthenticationException handling in GlobalExceptionHandler. Added max size limits for email and password in UserLoginRequestDto --- .../bookstore/dto/user/UserLoginRequestDto.java | 3 ++- .../bookstore/exception/GlobalExceptionHandler.java | 7 ------- .../bookstore/security/JwtAuthenticationFilter.java | 11 +++++++---- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/main/java/mate/academy/bookstore/dto/user/UserLoginRequestDto.java b/src/main/java/mate/academy/bookstore/dto/user/UserLoginRequestDto.java index eb644f5..7dc61d2 100644 --- a/src/main/java/mate/academy/bookstore/dto/user/UserLoginRequestDto.java +++ b/src/main/java/mate/academy/bookstore/dto/user/UserLoginRequestDto.java @@ -6,10 +6,11 @@ public record UserLoginRequestDto( @NotBlank + @Size(min = 8, max = 20) @Email String email, @NotBlank - @Size(min = 8, message = "Password must be at least 8 characters long") + @Size(min = 8, max = 20, message = "Password must be at least 8 characters long") String password ) { } diff --git a/src/main/java/mate/academy/bookstore/exception/GlobalExceptionHandler.java b/src/main/java/mate/academy/bookstore/exception/GlobalExceptionHandler.java index d115095..77ebcc3 100644 --- a/src/main/java/mate/academy/bookstore/exception/GlobalExceptionHandler.java +++ b/src/main/java/mate/academy/bookstore/exception/GlobalExceptionHandler.java @@ -1,6 +1,5 @@ package mate.academy.bookstore.exception; -import io.jsonwebtoken.JwtException; import java.time.LocalDateTime; import java.util.LinkedHashMap; import java.util.List; @@ -10,7 +9,6 @@ import org.springframework.http.HttpStatusCode; import org.springframework.http.ResponseEntity; import org.springframework.security.access.AccessDeniedException; -import org.springframework.security.core.AuthenticationException; import org.springframework.validation.FieldError; import org.springframework.validation.ObjectError; import org.springframework.web.bind.MethodArgumentNotValidException; @@ -55,11 +53,6 @@ protected ResponseEntity handleAccessDenied(AccessDeniedException ex) { return getResponseEntity(HttpStatus.FORBIDDEN, ex.getMessage()); } - @ExceptionHandler({JwtException.class, AuthenticationException.class}) - protected ResponseEntity handleAuthentication(Exception ex) { - return getResponseEntity(HttpStatus.UNAUTHORIZED, ex.getMessage()); - } - private ResponseEntity getResponseEntity(HttpStatus status, Object message) { Map body = new LinkedHashMap<>(); body.put("timestamp", LocalDateTime.now()); diff --git a/src/main/java/mate/academy/bookstore/security/JwtAuthenticationFilter.java b/src/main/java/mate/academy/bookstore/security/JwtAuthenticationFilter.java index e5eeb02..8de77a2 100644 --- a/src/main/java/mate/academy/bookstore/security/JwtAuthenticationFilter.java +++ b/src/main/java/mate/academy/bookstore/security/JwtAuthenticationFilter.java @@ -17,8 +17,11 @@ @RequiredArgsConstructor @Component public class JwtAuthenticationFilter extends OncePerRequestFilter { - private final JwtUtil jwtUtil; + private static final int START_INDEX = 7; + private static final String BEARER_PREFIX = "Bearer "; + private static final String AUTHORIZATION = "Authorization"; private final UserDetailsService userDetailsService; + private final JwtUtil jwtUtil; @Override protected void doFilterInternal( @@ -40,9 +43,9 @@ protected void doFilterInternal( } private String getToken(HttpServletRequest request) { - String bearerToken = request.getHeader("Authorization"); - if (bearerToken != null && bearerToken.startsWith("Bearer ")) { - return bearerToken.substring(7); + String bearerToken = request.getHeader(AUTHORIZATION); + if (bearerToken != null && bearerToken.startsWith(BEARER_PREFIX)) { + return bearerToken.substring(START_INDEX); } return null; } From 4313a36ce9c0648fdc22b01f2301fd7c390bfb28 Mon Sep 17 00:00:00 2001 From: Nazar Klimovych Date: Tue, 14 May 2024 16:03:48 +0300 Subject: [PATCH 12/12] START_INDEX was deleted in JwtAuthenticationFilter. --- .../academy/bookstore/security/JwtAuthenticationFilter.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/mate/academy/bookstore/security/JwtAuthenticationFilter.java b/src/main/java/mate/academy/bookstore/security/JwtAuthenticationFilter.java index 8de77a2..df82100 100644 --- a/src/main/java/mate/academy/bookstore/security/JwtAuthenticationFilter.java +++ b/src/main/java/mate/academy/bookstore/security/JwtAuthenticationFilter.java @@ -17,7 +17,6 @@ @RequiredArgsConstructor @Component public class JwtAuthenticationFilter extends OncePerRequestFilter { - private static final int START_INDEX = 7; private static final String BEARER_PREFIX = "Bearer "; private static final String AUTHORIZATION = "Authorization"; private final UserDetailsService userDetailsService; @@ -45,7 +44,7 @@ protected void doFilterInternal( private String getToken(HttpServletRequest request) { String bearerToken = request.getHeader(AUTHORIZATION); if (bearerToken != null && bearerToken.startsWith(BEARER_PREFIX)) { - return bearerToken.substring(START_INDEX); + return bearerToken.substring(BEARER_PREFIX.length()); } return null; }