From f5ce00fd876433e969c59d405715c722b4202ae5 Mon Sep 17 00:00:00 2001 From: nhkhai Date: Fri, 22 Mar 2024 17:46:58 +0800 Subject: [PATCH 1/6] Revert "Revert "Partially added JWT support."" This reverts commit 682f9305efbc96e2e53512c304be58335ebdd917. --- pom.xml | 14 +++ .../entities/ErrorResponse.java | 15 +++ .../sg/com/smartinventory/entities/User.java | 69 ++++++++++++++ .../com/smartinventory/entities/UserRole.java | 43 +++++++++ .../exceptions/GlobalExceptionHandler.java | 70 ++++++++++++++ .../repositories/UserRepository.java | 9 ++ .../security/JwtTokenFilter.java | 61 ++++++++++++ .../smartinventory/security/JwtTokenUtil.java | 92 +++++++++++++++++++ .../security/SecurityConfiguration.java | 65 +++++++++++++ .../CustomUserDetailsService.java | 35 +++++++ .../entities/ErrorResponseTest.java | 15 +++ 11 files changed, 488 insertions(+) create mode 100644 src/main/java/sg/com/smartinventory/entities/ErrorResponse.java create mode 100644 src/main/java/sg/com/smartinventory/entities/User.java create mode 100644 src/main/java/sg/com/smartinventory/entities/UserRole.java create mode 100644 src/main/java/sg/com/smartinventory/exceptions/GlobalExceptionHandler.java create mode 100644 src/main/java/sg/com/smartinventory/repositories/UserRepository.java create mode 100644 src/main/java/sg/com/smartinventory/security/JwtTokenFilter.java create mode 100644 src/main/java/sg/com/smartinventory/security/JwtTokenUtil.java create mode 100644 src/main/java/sg/com/smartinventory/security/SecurityConfiguration.java create mode 100644 src/main/java/sg/com/smartinventory/serviceImpls/CustomUserDetailsService.java create mode 100644 src/test/java/sg/com/smartinventory/entities/ErrorResponseTest.java diff --git a/pom.xml b/pom.xml index 0fd76f0..eb4b8f0 100644 --- a/pom.xml +++ b/pom.xml @@ -37,6 +37,10 @@ org.springframework.boot spring-boot-starter-web + + org.springframework.boot + spring-boot-starter-security + org.springframework.boot spring-boot-starter-test @@ -76,6 +80,11 @@ 3.25.3 test + + io.jsonwebtoken + jjwt + 0.9.1 + @@ -89,6 +98,11 @@ org.projectlombok lombok + diff --git a/src/main/java/sg/com/smartinventory/entities/ErrorResponse.java b/src/main/java/sg/com/smartinventory/entities/ErrorResponse.java new file mode 100644 index 0000000..42ae4d9 --- /dev/null +++ b/src/main/java/sg/com/smartinventory/entities/ErrorResponse.java @@ -0,0 +1,15 @@ +package sg.com.smartinventory.entities; + +import java.time.LocalDateTime; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +public class ErrorResponse { + private String message; + private LocalDateTime timestamp; +} \ No newline at end of file diff --git a/src/main/java/sg/com/smartinventory/entities/User.java b/src/main/java/sg/com/smartinventory/entities/User.java new file mode 100644 index 0000000..af96d06 --- /dev/null +++ b/src/main/java/sg/com/smartinventory/entities/User.java @@ -0,0 +1,69 @@ +package sg.com.smartinventory.entities; + +import java.util.List; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +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 jakarta.validation.constraints.Digits; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Entity +@Table(name = "my_user") +public class User { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + + @NotBlank(message = "Username cannot be blank") + @Column(name = "username") + private String username; + + @NotBlank(message = "Password cannot be blank") + @Column(name = "password") + private String password; + + @NotBlank(message = "First name cannot be blank") + @Column(name = "first_name") + private String firstName; + + @Column(name = "last_name") + private String lastName; + + @Digits(fraction = 0, integer = 8, message = "ContactNo must be 8 digits") + @Column(name = "contact_no") + private String contactNo; + + @Email(message = "Email format must be valid") + @Column(name = "email") + private String email; + + @ManyToMany(cascade = CascadeType.ALL) + @JoinTable(name = "user_user_role", joinColumns = { @JoinColumn(name = "user_id") }, inverseJoinColumns = { + @JoinColumn(name = "user_role_id") }) + private List roles; + + public User(String email) { + this.email = email; + } +} \ No newline at end of file diff --git a/src/main/java/sg/com/smartinventory/entities/UserRole.java b/src/main/java/sg/com/smartinventory/entities/UserRole.java new file mode 100644 index 0000000..660d6ac --- /dev/null +++ b/src/main/java/sg/com/smartinventory/entities/UserRole.java @@ -0,0 +1,43 @@ +package sg.com.smartinventory.entities; + +import java.util.List; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.Table; + +import com.fasterxml.jackson.annotation.JsonBackReference; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Entity +@Table(name = "user_role") +public class UserRole { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + + @Column(name = "role_name") + private String roleName; + + @Column(name = "description") + private String description; + + @JsonBackReference + @ManyToMany(mappedBy = "roles") + private List users; +} \ No newline at end of file diff --git a/src/main/java/sg/com/smartinventory/exceptions/GlobalExceptionHandler.java b/src/main/java/sg/com/smartinventory/exceptions/GlobalExceptionHandler.java new file mode 100644 index 0000000..ca6e759 --- /dev/null +++ b/src/main/java/sg/com/smartinventory/exceptions/GlobalExceptionHandler.java @@ -0,0 +1,70 @@ +package sg.com.smartinventory.exceptions; + +import java.time.LocalDateTime; +import java.util.List; + +import sg.com.smartinventory.entities.ErrorResponse; + +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.ObjectError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +@ControllerAdvice +public class GlobalExceptionHandler { + // This is handler for CustomerNotFoundException + @ExceptionHandler({ CustomerNotFoundException.class, ProductNotFoundException.class, + ReviewNotFoundException.class }) + public ResponseEntity handleResourceNotFoundException(CustomerNotFoundException ex) { + ErrorResponse errorResponse = new ErrorResponse(ex.getMessage(), LocalDateTime.now()); + + return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND); + } + + // @ExceptionHandler(ProductNotFoundException.class) + // public ResponseEntity + // handleProductNotFoundException(CustomerNotFoundException ex){ + // ErrorResponse errorResponse = new ErrorResponse(ex.getMessage(), + // LocalDateTime.now()); + // + // return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND); + // } + + @ExceptionHandler(EmptyResultDataAccessException.class) + public ResponseEntity handleEmptyResultDataAccessException(EmptyResultDataAccessException ex) { + ErrorResponse errorResponse = new ErrorResponse("Entry does not exist. ", LocalDateTime.now()); + + return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleValidationExceptions(MethodArgumentNotValidException ex) { + // Get a list of all validation errors from the exception object. + List validationErrors = ex.getBindingResult().getAllErrors(); + + // Create a StringBuilder to store all error messages. + StringBuilder sb = new StringBuilder(); + + for (ObjectError error : validationErrors) { + sb.append(error.getDefaultMessage() + "."); + } + + ErrorResponse errorResponse = new ErrorResponse(sb.toString(), LocalDateTime.now()); + + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleException(Exception ex) { + // We can log the exception here. + // logger.error(ex.getMessage(), ex); + + // Return generic error message. + ErrorResponse errorResponse = new ErrorResponse("Something went wrong", LocalDateTime.now()); + + return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR); + } +} \ No newline at end of file diff --git a/src/main/java/sg/com/smartinventory/repositories/UserRepository.java b/src/main/java/sg/com/smartinventory/repositories/UserRepository.java new file mode 100644 index 0000000..6f20afc --- /dev/null +++ b/src/main/java/sg/com/smartinventory/repositories/UserRepository.java @@ -0,0 +1,9 @@ +package sg.com.smartinventory.repositories; + +import org.springframework.data.jpa.repository.JpaRepository; + +import sg.com.smartinventory.entities.User; + +public interface UserRepository extends JpaRepository { + User findByUsername(String username); +} \ No newline at end of file diff --git a/src/main/java/sg/com/smartinventory/security/JwtTokenFilter.java b/src/main/java/sg/com/smartinventory/security/JwtTokenFilter.java new file mode 100644 index 0000000..941b23c --- /dev/null +++ b/src/main/java/sg/com/smartinventory/security/JwtTokenFilter.java @@ -0,0 +1,61 @@ +package sg.com.smartinventory.security; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.ArrayList; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import io.jsonwebtoken.Claims; +import sg.com.smartinventory.entities.ErrorResponse; + +@Component +public class JwtTokenFilter extends OncePerRequestFilter { + + private JwtTokenUtil jwtTokenUtil; + + public JwtTokenFilter(JwtTokenUtil jwtTokenUtil) { + this.jwtTokenUtil = jwtTokenUtil; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + try { + String accessToken = jwtTokenUtil.resolveToken(request); + if (accessToken == null) { + filterChain.doFilter(request, response); + return; + } + + Claims claims = jwtTokenUtil.resolveClaims(request); + + if (claims != null & jwtTokenUtil.validateClaims(claims)) { + String email = claims.getSubject(); + + Authentication authentication = new UsernamePasswordAuthenticationToken(email, "", new ArrayList<>()); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + + } catch (Exception ex) { + response.setStatus(HttpStatus.FORBIDDEN.value()); + response.getWriter().write(new ErrorResponse("Authentication failure", LocalDateTime.now()).toString()); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + + } + filterChain.doFilter(request, response); + + } +} \ No newline at end of file diff --git a/src/main/java/sg/com/smartinventory/security/JwtTokenUtil.java b/src/main/java/sg/com/smartinventory/security/JwtTokenUtil.java new file mode 100644 index 0000000..3092157 --- /dev/null +++ b/src/main/java/sg/com/smartinventory/security/JwtTokenUtil.java @@ -0,0 +1,92 @@ +package sg.com.smartinventory.security; + +import java.util.Date; +import java.util.concurrent.TimeUnit; + +import jakarta.servlet.http.HttpServletRequest; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.AuthenticationException; +import org.springframework.stereotype.Component; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.JwtParser; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import sg.com.smartinventory.entities.User; + +@Component +public class JwtTokenUtil { + + private final String JWT_SECRET_KEY = "mysecretkey"; + + @Value("${jwt.session.period:3600000}") + private long jwtSessionPeriod; + + private final JwtParser jwtParser; + + private final String TOKEN_HEADER = "Authorization"; + private final String TOKEN_PREFIX = "Bearer "; + + public JwtTokenUtil() { + this.jwtParser = Jwts.parser().setSigningKey(JWT_SECRET_KEY); + } + + public String createToken(User user) { + Claims claims = Jwts.claims().setSubject(user.getEmail()); + claims.put("userName", user.getUsername()); + claims.put("firstName", user.getFirstName()); + claims.put("lastName", user.getLastName()); + + Date tokenCreateTime = new Date(); + Date tokenValidity = new Date(tokenCreateTime.getTime() + TimeUnit.MINUTES.toMillis(jwtSessionPeriod)); + + return Jwts.builder() + .setClaims(claims) + .setExpiration(tokenValidity) + .signWith(SignatureAlgorithm.HS256, JWT_SECRET_KEY) + .compact(); + } + + private Claims parseJwtClaims(String token) { + return jwtParser.parseClaimsJws(token).getBody(); + } + + public String resolveToken(HttpServletRequest request) { + + String bearerToken = request.getHeader(TOKEN_HEADER); + if (bearerToken != null && bearerToken.startsWith(TOKEN_PREFIX)) { + return bearerToken.substring(TOKEN_PREFIX.length()); + } + return null; + } + + public Claims resolveClaims(HttpServletRequest req) { + try { + String token = resolveToken(req); + if (token != null) { + return parseJwtClaims(token); + } + return null; + } catch (ExpiredJwtException ex) { + req.setAttribute("expired", ex.getMessage()); + throw ex; + } catch (Exception ex) { + req.setAttribute("invalid", ex.getMessage()); + throw ex; + } + } + + public boolean validateClaims(Claims claims) throws AuthenticationException { + try { + return claims.getExpiration().after(new Date()); + } catch (Exception e) { + throw e; + } + } + + public String getEmail(Claims claims) { + return claims.getSubject(); + } +} \ No newline at end of file diff --git a/src/main/java/sg/com/smartinventory/security/SecurityConfiguration.java b/src/main/java/sg/com/smartinventory/security/SecurityConfiguration.java new file mode 100644 index 0000000..87a03d5 --- /dev/null +++ b/src/main/java/sg/com/smartinventory/security/SecurityConfiguration.java @@ -0,0 +1,65 @@ +package sg.com.smartinventory.security; + +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.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +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 sg.com.smartinventory.serviceImpls.CustomUserDetailsService; + +import jakarta.servlet.http.HttpServletResponse; + +@Configuration +@EnableWebSecurity +public class SecurityConfiguration { + + private final JwtTokenFilter jwtTokenFilter; + private final CustomUserDetailsService customUserDetailsService; + + public SecurityConfiguration(JwtTokenFilter jwtTokenFilter, CustomUserDetailsService customUserDetailsService) { + this.jwtTokenFilter = jwtTokenFilter; + this.customUserDetailsService = customUserDetailsService; + } + + @Bean + PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + AuthenticationManager authenticationManager(HttpSecurity http, PasswordEncoder passwordEncoder) + throws Exception { + AuthenticationManagerBuilder authenticationManagerBuilder = http + .getSharedObject(AuthenticationManagerBuilder.class); + authenticationManagerBuilder.userDetailsService(customUserDetailsService).passwordEncoder(passwordEncoder); + return authenticationManagerBuilder.build(); + } + + @Bean + SecurityFilterChain securityFitlerChain(HttpSecurity http) throws Exception { + http.csrf(csrf -> csrf.disable()); + + http = http.sessionManagement(management -> management + .sessionCreationPolicy(SessionCreationPolicy.STATELESS)); + + http = http.exceptionHandling(handling -> handling.authenticationEntryPoint((request, response, ex) -> { + response.sendError( + HttpServletResponse.SC_UNAUTHORIZED, + ex.getMessage()); + })); + + http.authorizeRequests( + (authorize) -> authorize.antMatchers("/auth/**").permitAll().anyRequest().authenticated()); + + http.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } +} \ No newline at end of file diff --git a/src/main/java/sg/com/smartinventory/serviceImpls/CustomUserDetailsService.java b/src/main/java/sg/com/smartinventory/serviceImpls/CustomUserDetailsService.java new file mode 100644 index 0000000..b7411e6 --- /dev/null +++ b/src/main/java/sg/com/smartinventory/serviceImpls/CustomUserDetailsService.java @@ -0,0 +1,35 @@ +package sg.com.smartinventory.serviceImpls; + +import java.util.ArrayList; +import java.util.List; + +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; + +import sg.com.smartinventory.entities.User; +import sg.com.smartinventory.repositories.UserRepository; + +@Service +public class CustomUserDetailsService implements UserDetailsService { + + private final UserRepository userRepository; + + public CustomUserDetailsService(UserRepository userRepository) { + this.userRepository = userRepository; + } + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + User user = userRepository.findByUsername(username); + List roles = new ArrayList<>(); + roles.add("USER"); + UserDetails userDetails = org.springframework.security.core.userdetails.User.builder() + .username(user.getUsername()) + .password(user.getPassword()) + .roles(roles.toArray(new String[0])) + .build(); + return userDetails; + } +} \ No newline at end of file diff --git a/src/test/java/sg/com/smartinventory/entities/ErrorResponseTest.java b/src/test/java/sg/com/smartinventory/entities/ErrorResponseTest.java new file mode 100644 index 0000000..ae73c98 --- /dev/null +++ b/src/test/java/sg/com/smartinventory/entities/ErrorResponseTest.java @@ -0,0 +1,15 @@ +package sg.com.smartinventory.entities; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import org.mockito.InjectMocks; +import org.mockito.Mock; + +public class ErrorResponseTest { + @DisplayName("Error Response Test") + @Test + public void errorResponseTest() throws Exception { + + } +} \ No newline at end of file From 93bae24c48d2b93785dedfa34b2b76c01463a6e1 Mon Sep 17 00:00:00 2001 From: "nhkhai@gmail.com" Date: Sat, 23 Mar 2024 01:35:58 +0800 Subject: [PATCH 2/6] Update pom.xml --- pom.xml | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/pom.xml b/pom.xml index 5caf656..5791b56 100644 --- a/pom.xml +++ b/pom.xml @@ -1,7 +1,6 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 org.springframework.boot @@ -92,19 +91,17 @@ org.springframework.boot spring-boot-maven-plugin - + - + --> org.apache.maven.plugins @@ -113,4 +110,4 @@ - + \ No newline at end of file From d909bc9cbe2faedfbb991e09a87f093dfc87b2ea Mon Sep 17 00:00:00 2001 From: "nhkhai@gmail.com" Date: Sat, 23 Mar 2024 01:36:15 +0800 Subject: [PATCH 3/6] Update SecurityConfiguration.java Replaced the deprecated function. --- .../com/smartinventory/security/SecurityConfiguration.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/sg/com/smartinventory/security/SecurityConfiguration.java b/src/main/java/sg/com/smartinventory/security/SecurityConfiguration.java index 87a03d5..93a3a89 100644 --- a/src/main/java/sg/com/smartinventory/security/SecurityConfiguration.java +++ b/src/main/java/sg/com/smartinventory/security/SecurityConfiguration.java @@ -55,8 +55,10 @@ SecurityFilterChain securityFitlerChain(HttpSecurity http) throws Exception { ex.getMessage()); })); - http.authorizeRequests( - (authorize) -> authorize.antMatchers("/auth/**").permitAll().anyRequest().authenticated()); + // http.authorizeRequests((authorize) -> + // authorize.antMatchers("/auth/**").permitAll().anyRequest().authenticated()); + http.authorizeHttpRequests( + (authorize) -> authorize.requestMatchers("/auth/**").permitAll().anyRequest().authenticated()); http.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class); From a01bf6ec583ca39afce16f655b776621d8737673 Mon Sep 17 00:00:00 2001 From: "nhkhai@gmail.com" Date: Sat, 23 Mar 2024 02:36:24 +0800 Subject: [PATCH 4/6] Updated configuration and formatting. --- pom.xml | 5 +++++ .../sg/com/smartinventory/security/JwtTokenFilter.java | 3 ++- .../java/sg/com/smartinventory/security/JwtTokenUtil.java | 6 ++++-- .../smartinventory/security/SecurityConfiguration.java | 8 +++----- src/main/resources/application.properties | 4 ++++ 5 files changed, 18 insertions(+), 8 deletions(-) diff --git a/pom.xml b/pom.xml index 5791b56..aa46cd1 100644 --- a/pom.xml +++ b/pom.xml @@ -84,6 +84,11 @@ jjwt 0.9.1 + diff --git a/src/main/java/sg/com/smartinventory/security/JwtTokenFilter.java b/src/main/java/sg/com/smartinventory/security/JwtTokenFilter.java index 941b23c..f75217d 100644 --- a/src/main/java/sg/com/smartinventory/security/JwtTokenFilter.java +++ b/src/main/java/sg/com/smartinventory/security/JwtTokenFilter.java @@ -18,6 +18,7 @@ import org.springframework.web.filter.OncePerRequestFilter; import io.jsonwebtoken.Claims; + import sg.com.smartinventory.entities.ErrorResponse; @Component @@ -55,7 +56,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response.setContentType(MediaType.APPLICATION_JSON_VALUE); } - filterChain.doFilter(request, response); + filterChain.doFilter(request, response); } } \ No newline at end of file diff --git a/src/main/java/sg/com/smartinventory/security/JwtTokenUtil.java b/src/main/java/sg/com/smartinventory/security/JwtTokenUtil.java index 3092157..cc6a621 100644 --- a/src/main/java/sg/com/smartinventory/security/JwtTokenUtil.java +++ b/src/main/java/sg/com/smartinventory/security/JwtTokenUtil.java @@ -14,11 +14,11 @@ import io.jsonwebtoken.JwtParser; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; + import sg.com.smartinventory.entities.User; @Component public class JwtTokenUtil { - private final String JWT_SECRET_KEY = "mysecretkey"; @Value("${jwt.session.period:3600000}") @@ -35,6 +35,7 @@ public JwtTokenUtil() { public String createToken(User user) { Claims claims = Jwts.claims().setSubject(user.getEmail()); + claims.put("userName", user.getUsername()); claims.put("firstName", user.getFirstName()); claims.put("lastName", user.getLastName()); @@ -54,11 +55,12 @@ private Claims parseJwtClaims(String token) { } public String resolveToken(HttpServletRequest request) { - String bearerToken = request.getHeader(TOKEN_HEADER); + if (bearerToken != null && bearerToken.startsWith(TOKEN_PREFIX)) { return bearerToken.substring(TOKEN_PREFIX.length()); } + return null; } diff --git a/src/main/java/sg/com/smartinventory/security/SecurityConfiguration.java b/src/main/java/sg/com/smartinventory/security/SecurityConfiguration.java index 93a3a89..bc2b8db 100644 --- a/src/main/java/sg/com/smartinventory/security/SecurityConfiguration.java +++ b/src/main/java/sg/com/smartinventory/security/SecurityConfiguration.java @@ -39,6 +39,7 @@ AuthenticationManager authenticationManager(HttpSecurity http, PasswordEncoder p AuthenticationManagerBuilder authenticationManagerBuilder = http .getSharedObject(AuthenticationManagerBuilder.class); authenticationManagerBuilder.userDetailsService(customUserDetailsService).passwordEncoder(passwordEncoder); + return authenticationManagerBuilder.build(); } @@ -46,13 +47,10 @@ AuthenticationManager authenticationManager(HttpSecurity http, PasswordEncoder p SecurityFilterChain securityFitlerChain(HttpSecurity http) throws Exception { http.csrf(csrf -> csrf.disable()); - http = http.sessionManagement(management -> management - .sessionCreationPolicy(SessionCreationPolicy.STATELESS)); + http = http.sessionManagement(management -> management.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); http = http.exceptionHandling(handling -> handling.authenticationEntryPoint((request, response, ex) -> { - response.sendError( - HttpServletResponse.SC_UNAUTHORIZED, - ex.getMessage()); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, ex.getMessage()); })); // http.authorizeRequests((authorize) -> diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index b14bab1..6551e53 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -58,6 +58,10 @@ springdoc.swagger-ui.path=/swagger-ui.html # To sort the API paths according to their HTTP methods with the springdoc.swagger-ui.operationsSorter property. # springdoc.swagger-ui.operationsSorter=method +# Jwt details. +# https://medium.com/code-with-farhan/spring-security-jwt-authentication-authorization-a2c6860be3cf +# jwt.session.period=60000 + # Profile management. # Spring Boot will always read the application.properties file. # Other's profile files, such as application-development.properties only will complement and replace the properties defined before. From f0493eb46b75e9dd5b3d74cbd801f3ff4db79530 Mon Sep 17 00:00:00 2001 From: "nhkhai@gmail.com" Date: Sat, 23 Mar 2024 02:46:13 +0800 Subject: [PATCH 5/6] Update JwtTokenUtil.java Updated syntax. --- .../java/sg/com/smartinventory/security/JwtTokenUtil.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/sg/com/smartinventory/security/JwtTokenUtil.java b/src/main/java/sg/com/smartinventory/security/JwtTokenUtil.java index cc6a621..eb4fca8 100644 --- a/src/main/java/sg/com/smartinventory/security/JwtTokenUtil.java +++ b/src/main/java/sg/com/smartinventory/security/JwtTokenUtil.java @@ -67,15 +67,19 @@ public String resolveToken(HttpServletRequest request) { public Claims resolveClaims(HttpServletRequest req) { try { String token = resolveToken(req); + if (token != null) { return parseJwtClaims(token); } + return null; } catch (ExpiredJwtException ex) { req.setAttribute("expired", ex.getMessage()); + throw ex; } catch (Exception ex) { req.setAttribute("invalid", ex.getMessage()); + throw ex; } } From fc7b69861190f32d2513ba6e8192f71ae2e8e463 Mon Sep 17 00:00:00 2001 From: "nhkhai@gmail.com" Date: Sat, 23 Mar 2024 14:23:40 +0800 Subject: [PATCH 6/6] Added security management and user management related sample files. Appended with .dev to not affect build and test execution. KIV. --- pom.xml | 8 +-- .../controllers/AuthController.java.dev | 41 +++++++++++ .../GlobalExceptionHandler.java.dev | 62 +++++++++++++++++ .../controllers/UserController.java.dev | 60 ++++++++++++++++ .../controllers/UserRoleController.java.dev | 60 ++++++++++++++++ ...orResponse.java => ErrorResponse.java.dev} | 0 .../entities/{User.java => User.java.dev} | 0 .../{UserRole.java => UserRole.java.dev} | 0 ...r.java => GlobalExceptionHandler.java.dev} | 2 +- .../exceptions/UserNotFoundException.java.dev | 7 ++ .../UserRoleNotFoundException.java.dev | 7 ++ ...epository.java => UserRepository.java.dev} | 0 .../repositories/UserRoleRepository.java.dev | 9 +++ ...kenFilter.java => JwtTokenFilter.java.dev} | 0 ...wtTokenUtil.java => JwtTokenUtil.java.dev} | 0 ...on.java => SecurityConfiguration.java.dev} | 0 ...java => CustomUserDetailsService.java.dev} | 4 ++ .../serviceImpls/UserDataLoader.java.dev | 69 +++++++++++++++++++ .../serviceImpls/UserRoleServiceImpl.java.dev | 69 +++++++++++++++++++ .../serviceImpls/UserServiceImpl.java.dev | 69 +++++++++++++++++++ .../services/UserRoleService.java.dev | 19 +++++ .../services/UserService.java.dev | 19 +++++ 22 files changed, 500 insertions(+), 5 deletions(-) create mode 100644 src/main/java/sg/com/smartinventory/controllers/AuthController.java.dev create mode 100644 src/main/java/sg/com/smartinventory/controllers/GlobalExceptionHandler.java.dev create mode 100644 src/main/java/sg/com/smartinventory/controllers/UserController.java.dev create mode 100644 src/main/java/sg/com/smartinventory/controllers/UserRoleController.java.dev rename src/main/java/sg/com/smartinventory/entities/{ErrorResponse.java => ErrorResponse.java.dev} (100%) rename src/main/java/sg/com/smartinventory/entities/{User.java => User.java.dev} (100%) rename src/main/java/sg/com/smartinventory/entities/{UserRole.java => UserRole.java.dev} (100%) rename src/main/java/sg/com/smartinventory/exceptions/{GlobalExceptionHandler.java => GlobalExceptionHandler.java.dev} (98%) create mode 100644 src/main/java/sg/com/smartinventory/exceptions/UserNotFoundException.java.dev create mode 100644 src/main/java/sg/com/smartinventory/exceptions/UserRoleNotFoundException.java.dev rename src/main/java/sg/com/smartinventory/repositories/{UserRepository.java => UserRepository.java.dev} (100%) create mode 100644 src/main/java/sg/com/smartinventory/repositories/UserRoleRepository.java.dev rename src/main/java/sg/com/smartinventory/security/{JwtTokenFilter.java => JwtTokenFilter.java.dev} (100%) rename src/main/java/sg/com/smartinventory/security/{JwtTokenUtil.java => JwtTokenUtil.java.dev} (100%) rename src/main/java/sg/com/smartinventory/security/{SecurityConfiguration.java => SecurityConfiguration.java.dev} (100%) rename src/main/java/sg/com/smartinventory/serviceImpls/{CustomUserDetailsService.java => CustomUserDetailsService.java.dev} (99%) create mode 100644 src/main/java/sg/com/smartinventory/serviceImpls/UserDataLoader.java.dev create mode 100644 src/main/java/sg/com/smartinventory/serviceImpls/UserRoleServiceImpl.java.dev create mode 100644 src/main/java/sg/com/smartinventory/serviceImpls/UserServiceImpl.java.dev create mode 100644 src/main/java/sg/com/smartinventory/services/UserRoleService.java.dev create mode 100644 src/main/java/sg/com/smartinventory/services/UserService.java.dev diff --git a/pom.xml b/pom.xml index b2a17fa..5e62025 100644 --- a/pom.xml +++ b/pom.xml @@ -36,10 +36,10 @@ org.springframework.boot spring-boot-starter-web - + org.springframework.boot spring-boot-starter-test @@ -79,11 +79,11 @@ 3.25.3 test - +