diff --git a/pom.xml b/pom.xml index 59c144b..0b5b42c 100644 --- a/pom.xml +++ b/pom.xml @@ -31,6 +31,7 @@ checkstyle.xml 0.2.0 1.5.5.Final + 0.12.6 @@ -123,6 +124,24 @@ 2.6.0 + + io.jsonwebtoken + jjwt-api + ${jjwt.version} + + + + io.jsonwebtoken + jjwt-impl + ${jjwt.version} + + + + io.jsonwebtoken + jjwt-jackson + ${jjwt.version} + + diff --git a/src/main/java/project/bookstore/config/SecurityConfig.java b/src/main/java/project/bookstore/config/SecurityConfig.java index e7796c8..222f6c6 100644 --- a/src/main/java/project/bookstore/config/SecurityConfig.java +++ b/src/main/java/project/bookstore/config/SecurityConfig.java @@ -1,21 +1,29 @@ package project.bookstore.config; +import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher; + import lombok.RequiredArgsConstructor; 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; +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; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import project.bookstore.security.JwtAuthenticationFilter; @Configuration @EnableMethodSecurity @RequiredArgsConstructor public class SecurityConfig { - private final UserDetailsService userDetailsService; + private final UserDetailsService service; + private final JwtAuthenticationFilter jwtAuthenticationFilter; @Bean public PasswordEncoder passwordEncoder() { @@ -23,22 +31,33 @@ public PasswordEncoder passwordEncoder() { } @Bean - public SecurityFilterChain getSecurityFilterChain(HttpSecurity http) throws Exception { + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { return http .cors(AbstractHttpConfigurer::disable) .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests( auth -> auth .requestMatchers( - "/auth/**", - "/swagger-ui/**", - "/v3/api-docs/**" + antMatcher("/auth/**"), + antMatcher("/swagger-ui/**"), + antMatcher("/v3/api-docs/**") ) .permitAll() .anyRequest() .authenticated() ) - .userDetailsService(userDetailsService) + .sessionManagement( + s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .addFilterBefore(jwtAuthenticationFilter, + UsernamePasswordAuthenticationFilter.class) + .userDetailsService(service) .build(); } + + @Bean + public AuthenticationManager authenticationManager( + AuthenticationConfiguration authenticationConfiguration + ) throws Exception { + return authenticationConfiguration.getAuthenticationManager(); + } } diff --git a/src/main/java/project/bookstore/controller/AuthenticationController.java b/src/main/java/project/bookstore/controller/AuthenticationController.java index 72a2826..6a802bc 100644 --- a/src/main/java/project/bookstore/controller/AuthenticationController.java +++ b/src/main/java/project/bookstore/controller/AuthenticationController.java @@ -8,9 +8,12 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import project.bookstore.dto.user.UserLoginRequestDto; +import project.bookstore.dto.user.UserLoginResponseDto; import project.bookstore.dto.user.UserRegistrationRequestDto; import project.bookstore.dto.user.UserResponseDto; import project.bookstore.exception.RegistrationException; +import project.bookstore.security.AuthenticationService; import project.bookstore.service.UserService; @Tag(name = "Authentication controller", description = "endpoints for authentication") @@ -19,6 +22,7 @@ @RequestMapping("/auth") public class AuthenticationController { private final UserService userService; + private final AuthenticationService authenticationService; @PostMapping("/register") @Operation( @@ -31,4 +35,13 @@ public UserResponseDto register(@RequestBody @Valid UserRegistrationRequestDto r return userService.register(requestDto); } + + @PostMapping("/login") + @Operation( + summary = "login user", + description = "login user by fields: email, password" + ) + public UserLoginResponseDto login(@RequestBody @Valid UserLoginRequestDto requestDto) { + return authenticationService.authenticate(requestDto); + } } diff --git a/src/main/java/project/bookstore/controller/BookController.java b/src/main/java/project/bookstore/controller/BookController.java index 68033b3..55f4b77 100644 --- a/src/main/java/project/bookstore/controller/BookController.java +++ b/src/main/java/project/bookstore/controller/BookController.java @@ -30,6 +30,7 @@ public class BookController { private final BookMapper bookMapper; @GetMapping + @PreAuthorize("hasRole('ROLE_USER')") @Operation( summary = "Get all books", description = "Get all books from db") diff --git a/src/main/java/project/bookstore/dto/user/UserLoginRequestDto.java b/src/main/java/project/bookstore/dto/user/UserLoginRequestDto.java new file mode 100644 index 0000000..d07bfda --- /dev/null +++ b/src/main/java/project/bookstore/dto/user/UserLoginRequestDto.java @@ -0,0 +1,17 @@ +package project.bookstore.dto.user; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public record UserLoginRequestDto( + @NotBlank + @Size(min = 6, max = 20) + @Email + String email, + @NotBlank + @Size(min = 6, max = 20) + String password +) { + +} diff --git a/src/main/java/project/bookstore/dto/user/UserLoginResponseDto.java b/src/main/java/project/bookstore/dto/user/UserLoginResponseDto.java new file mode 100644 index 0000000..636a4a6 --- /dev/null +++ b/src/main/java/project/bookstore/dto/user/UserLoginResponseDto.java @@ -0,0 +1,4 @@ +package project.bookstore.dto.user; + +public record UserLoginResponseDto(String token) { +} diff --git a/src/main/java/project/bookstore/model/Book.java b/src/main/java/project/bookstore/model/Book.java index eada0bd..c1f81c2 100644 --- a/src/main/java/project/bookstore/model/Book.java +++ b/src/main/java/project/bookstore/model/Book.java @@ -22,23 +22,16 @@ public class Book { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(nullable = false) private String title; - @Column(nullable = false) private String author; - @Column(unique = true, nullable = false) private String isbn; - @Column(nullable = false) private BigDecimal price; - private String description; - private String coverImage; - @Column(nullable = false, columnDefinition = "TINYINT(1)") private boolean isDeleted = false; } diff --git a/src/main/java/project/bookstore/security/AuthenticationService.java b/src/main/java/project/bookstore/security/AuthenticationService.java new file mode 100644 index 0000000..36ddf54 --- /dev/null +++ b/src/main/java/project/bookstore/security/AuthenticationService.java @@ -0,0 +1,24 @@ +package project.bookstore.security; + +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Service; +import project.bookstore.dto.user.UserLoginRequestDto; +import project.bookstore.dto.user.UserLoginResponseDto; + +@RequiredArgsConstructor +@Service +public class AuthenticationService { + private final JwtUtil jwtUtil; + private final AuthenticationManager authenticationManager; + + public UserLoginResponseDto authenticate(UserLoginRequestDto requestDto) { + final Authentication authentication = authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(requestDto.email(), requestDto.password()) + ); + String token = jwtUtil.generateToken(authentication.getName()); + return new UserLoginResponseDto(token); + } +} diff --git a/src/main/java/project/bookstore/security/JwtAuthenticationFilter.java b/src/main/java/project/bookstore/security/JwtAuthenticationFilter.java new file mode 100644 index 0000000..1a1bdc3 --- /dev/null +++ b/src/main/java/project/bookstore/security/JwtAuthenticationFilter.java @@ -0,0 +1,47 @@ +package project.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.http.HttpHeaders; +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.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +@RequiredArgsConstructor +@Component +public class JwtAuthenticationFilter extends OncePerRequestFilter { + private static final String TOKEN_HEADER = "Bearer "; + private final JwtUtil jwtUtil; + private final UserDetailsService service; + + @Override + protected void doFilterInternal( + HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain + ) throws ServletException, IOException { + String token = getToken(request); + if (token != null && jwtUtil.isValidToken(token)) { + UserDetails userDetails = service.loadUserByUsername(jwtUtil.getUserName(token)); + Authentication authentication = new UsernamePasswordAuthenticationToken( + userDetails, null, userDetails.getAuthorities()); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + filterChain.doFilter(request, response); + } + + private String getToken(HttpServletRequest request) { + String token = request.getHeader(HttpHeaders.AUTHORIZATION); + return (StringUtils.hasText(token) && token.startsWith(TOKEN_HEADER)) + ? token.substring(TOKEN_HEADER.length()) : null; + } +} diff --git a/src/main/java/project/bookstore/security/JwtUtil.java b/src/main/java/project/bookstore/security/JwtUtil.java new file mode 100644 index 0000000..b571dd8 --- /dev/null +++ b/src/main/java/project/bookstore/security/JwtUtil.java @@ -0,0 +1,52 @@ +package project.bookstore.security; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import java.nio.charset.StandardCharsets; +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 { + @Value("${jwt.expiration}") + private long expiration; + private final SecretKey secret; + + public JwtUtil(@Value(value = "${jwt.secret}") String secretString) { + secret = Keys.hmacShaKeyFor(secretString.getBytes(StandardCharsets.UTF_8)); + } + + public String generateToken(String username) { + return Jwts.builder() + .subject(username) + .expiration(new Date(System.currentTimeMillis() + expiration)) + .signWith(secret) + .compact(); + } + + public boolean isValidToken(String token) { + try { + return !getClaimFromToken(token, Claims::getExpiration).before(new Date()); + } catch (JwtException | IllegalArgumentException e) { + throw new JwtException("Expired or invalid JWT token", e); + } + } + + public String getUserName(String token) { + return getClaimFromToken(token, Claims::getSubject); + } + + private T getClaimFromToken(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/resources/application.properties b/src/main/resources/application.properties index 03ab421..ab32bb5 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=1000000 +jwt.secret=qwertyuiopasdfghjklzxcvbnm34567890 diff --git a/src/main/resources/db/changelog/changes/02-create-users-table.yaml b/src/main/resources/db/changelog/changes/02-create-users-table.yaml index 00b0b96..3b45732 100644 --- a/src/main/resources/db/changelog/changes/02-create-users-table.yaml +++ b/src/main/resources/db/changelog/changes/02-create-users-table.yaml @@ -36,4 +36,10 @@ databaseChangeLog: nullable: false - column: name: shipping_address - type: varchar(255) \ No newline at end of file + type: varchar(255) + - column: + name: is_deleted + type: tinyint + defaultValueBoolean: false + constraints: + nullable: false diff --git a/src/main/resources/db/changelog/changes/06-add-isDeleted-to-users.yaml b/src/main/resources/db/changelog/changes/06-add-isDeleted-to-users.yaml deleted file mode 100644 index dcab0c6..0000000 --- a/src/main/resources/db/changelog/changes/06-add-isDeleted-to-users.yaml +++ /dev/null @@ -1,14 +0,0 @@ -databaseChangeLog: - - changeSet: - id: add-isDeleted-to-users - author: vlad - changes: - - addColumn: - tableName: users - columns: - - column: - name: is_deleted - type: tinyint - defaultValueBoolean: false - constraints: - nullable: false diff --git a/src/main/resources/db/changelog/db.changelog-master.yaml b/src/main/resources/db/changelog/db.changelog-master.yaml index 19df5dd..ec80400 100644 --- a/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/src/main/resources/db/changelog/db.changelog-master.yaml @@ -9,5 +9,3 @@ databaseChangeLog: file: db/changelog/changes/04-create-users-roles-table.yaml - include: file: db/changelog/changes/05-insert-users-to-db.yaml - - include: - file: db/changelog/changes/06-add-isDeleted-to-users.yaml diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index 13790c5..67005ef 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -6,3 +6,6 @@ spring.jpa.database-platform=org.hibernate.dialect.H2Dialect spring.jpa.hibernate.ddl-auto=create-drop spring.jpa.show-sql=true + +jwt.expiration=1000000 +jwt.secret=qwertyuiopasdfghjklzxcvbnm34567890