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