From 767fa9c7581810b22a23ec9b793f149c7155f8e9 Mon Sep 17 00:00:00 2001 From: Zakharov_Rostislav Date: Thu, 23 May 2024 13:59:13 +0400 Subject: [PATCH 1/3] lab-5 Change core and users package to add login and sign up functions --- SpringApp/library/build.gradle | 5 + .../library/core/configuration/Constants.java | 5 + .../core/configuration/WebConfiguration.java | 15 --- .../library/core/error/AdviceController.java | 83 +++++++++++++ .../ip/library/core/error/ErrorCauseDto.java | 31 +++++ .../com/ip/library/core/error/ErrorDto.java | 20 ++++ .../com/ip/library/core/jwt/JwtException.java | 11 ++ .../com/ip/library/core/jwt/JwtFilter.java | 57 +++++++++ .../ip/library/core/jwt/JwtProperties.java | 18 +++ .../com/ip/library/core/jwt/JwtProvider.java | 110 ++++++++++++++++++ .../core/security/SecurityConfiguration.java | 89 ++++++++++++++ .../core/security/SwaggerConfiguration.java | 31 +++++ .../library/core/security/UserPrincipal.java | 64 ++++++++++ .../ip/library/users/api/LoginController.java | 47 ++++++++ .../ip/library/users/api/UserController.java | 4 +- .../com/ip/library/users/api/UserDto.java | 24 ---- .../ip/library/users/model/UserEntity.java | 7 +- .../com/ip/library/users/model/UserRole.java | 22 ++++ .../ip/library/users/service/UserService.java | 55 ++++++++- .../test/java/com/ip/library/UsersTests.java | 9 +- 20 files changed, 654 insertions(+), 53 deletions(-) delete mode 100644 SpringApp/library/src/main/java/com/ip/library/core/configuration/WebConfiguration.java create mode 100644 SpringApp/library/src/main/java/com/ip/library/core/error/AdviceController.java create mode 100644 SpringApp/library/src/main/java/com/ip/library/core/error/ErrorCauseDto.java create mode 100644 SpringApp/library/src/main/java/com/ip/library/core/error/ErrorDto.java create mode 100644 SpringApp/library/src/main/java/com/ip/library/core/jwt/JwtException.java create mode 100644 SpringApp/library/src/main/java/com/ip/library/core/jwt/JwtFilter.java create mode 100644 SpringApp/library/src/main/java/com/ip/library/core/jwt/JwtProperties.java create mode 100644 SpringApp/library/src/main/java/com/ip/library/core/jwt/JwtProvider.java create mode 100644 SpringApp/library/src/main/java/com/ip/library/core/security/SecurityConfiguration.java create mode 100644 SpringApp/library/src/main/java/com/ip/library/core/security/SwaggerConfiguration.java create mode 100644 SpringApp/library/src/main/java/com/ip/library/core/security/UserPrincipal.java create mode 100644 SpringApp/library/src/main/java/com/ip/library/users/api/LoginController.java create mode 100644 SpringApp/library/src/main/java/com/ip/library/users/model/UserRole.java diff --git a/SpringApp/library/build.gradle b/SpringApp/library/build.gradle index dd32415..df8ff22 100644 --- a/SpringApp/library/build.gradle +++ b/SpringApp/library/build.gradle @@ -34,6 +34,11 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'com.h2database:h2:2.2.224' + + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'com.auth0:java-jwt:4.4.0' + + annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' testImplementation 'org.springframework.boot:spring-boot-starter-test' } diff --git a/SpringApp/library/src/main/java/com/ip/library/core/configuration/Constants.java b/SpringApp/library/src/main/java/com/ip/library/core/configuration/Constants.java index f5eb422..9d59937 100644 --- a/SpringApp/library/src/main/java/com/ip/library/core/configuration/Constants.java +++ b/SpringApp/library/src/main/java/com/ip/library/core/configuration/Constants.java @@ -7,5 +7,10 @@ public class Constants { public static final String DEFAULT_PAGE_SIZE = "5"; + public static final String LOGIN_URL = "/login"; + public static final String SIGNUP_URL = "/signup"; + + public static final String DEFAULT_PASSWORD = "123456"; + private Constants() {} } diff --git a/SpringApp/library/src/main/java/com/ip/library/core/configuration/WebConfiguration.java b/SpringApp/library/src/main/java/com/ip/library/core/configuration/WebConfiguration.java deleted file mode 100644 index 1f8a25d..0000000 --- a/SpringApp/library/src/main/java/com/ip/library/core/configuration/WebConfiguration.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.ip.library.core.configuration; - -import org.springframework.context.annotation.Configuration; -import org.springframework.lang.NonNull; -import org.springframework.web.servlet.config.annotation.CorsRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -@Configuration -public class WebConfiguration implements WebMvcConfigurer { - @Override - public void addCorsMappings(@NonNull CorsRegistry registry) { - registry.addMapping("/**") - .allowedMethods("GET", "POST", "PUT", "DELETE"); - } -} diff --git a/SpringApp/library/src/main/java/com/ip/library/core/error/AdviceController.java b/SpringApp/library/src/main/java/com/ip/library/core/error/AdviceController.java new file mode 100644 index 0000000..27ead98 --- /dev/null +++ b/SpringApp/library/src/main/java/com/ip/library/core/error/AdviceController.java @@ -0,0 +1,83 @@ +package com.ip.library.core.error; + +import java.io.IOException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +@RestControllerAdvice +public class AdviceController implements AuthenticationEntryPoint { + private final Logger log = LoggerFactory.getLogger(AdviceController.class); + + public static ErrorCauseDto getRootCause(Throwable throwable) { + Throwable rootCause = throwable; + while (rootCause.getCause() != null && rootCause.getCause() != rootCause) { + rootCause = rootCause.getCause(); + } + final StackTraceElement firstError = rootCause.getStackTrace()[0]; + return new ErrorCauseDto( + rootCause.getClass().getName(), + firstError.getMethodName(), + firstError.getFileName(), + firstError.getLineNumber()); + } + + private ResponseEntity handleException(Throwable throwable, HttpStatusCode httpCode) { + log.error("{}", throwable.getMessage()); + throwable.printStackTrace(); + final ErrorDto errorDto = new ErrorDto(throwable.getMessage(), AdviceController.getRootCause(throwable)); + return new ResponseEntity<>(errorDto, httpCode); + } + + @Override + public void commence( + HttpServletRequest request, + HttpServletResponse response, + AuthenticationException authException) throws IOException, ServletException { + final ResponseEntity body = handleException(authException, HttpStatus.UNAUTHORIZED); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setStatus(body.getStatusCode().value()); + response.getWriter().write(new ObjectMapper().writeValueAsString(body.getBody())); + } + + @ExceptionHandler(AccessDeniedException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + public ResponseEntity handleAccessDeniedException(Throwable throwable) { + return handleException(throwable, HttpStatus.FORBIDDEN); + } + + @ExceptionHandler(NotFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public ResponseEntity handleNotFoundException(Throwable throwable) { + return handleException(throwable, HttpStatus.NOT_FOUND); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ResponseEntity handleDataIntegrityViolationException(Throwable throwable) { + return handleException(throwable, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ResponseEntity handleAnyException(Throwable throwable) { + return handleException(throwable, HttpStatus.INTERNAL_SERVER_ERROR); + } +} diff --git a/SpringApp/library/src/main/java/com/ip/library/core/error/ErrorCauseDto.java b/SpringApp/library/src/main/java/com/ip/library/core/error/ErrorCauseDto.java new file mode 100644 index 0000000..7feee4d --- /dev/null +++ b/SpringApp/library/src/main/java/com/ip/library/core/error/ErrorCauseDto.java @@ -0,0 +1,31 @@ +package com.ip.library.core.error; + +class ErrorCauseDto { + private String exception; + private String methodName; + private String fineName; + private int lineNumber; + + ErrorCauseDto(String exception, String methodName, String fineName, int lineNumber) { + this.exception = exception; + this.methodName = methodName; + this.fineName = fineName; + this.lineNumber = lineNumber; + } + + public String getException() { + return exception; + } + + public String getMethodName() { + return methodName; + } + + public String getFineName() { + return fineName; + } + + public int getLineNumber() { + return lineNumber; + } +} diff --git a/SpringApp/library/src/main/java/com/ip/library/core/error/ErrorDto.java b/SpringApp/library/src/main/java/com/ip/library/core/error/ErrorDto.java new file mode 100644 index 0000000..c3eed48 --- /dev/null +++ b/SpringApp/library/src/main/java/com/ip/library/core/error/ErrorDto.java @@ -0,0 +1,20 @@ +package com.ip.library.core.error; + +public class ErrorDto { + private String error; + private ErrorCauseDto rootCause; + + public ErrorDto(String error, ErrorCauseDto rootCause) { + this.error = error; + this.rootCause = rootCause; + } + + public String getError() { + return error; + } + + public ErrorCauseDto getRootCause() { + return rootCause; + } + +} diff --git a/SpringApp/library/src/main/java/com/ip/library/core/jwt/JwtException.java b/SpringApp/library/src/main/java/com/ip/library/core/jwt/JwtException.java new file mode 100644 index 0000000..3cff4f5 --- /dev/null +++ b/SpringApp/library/src/main/java/com/ip/library/core/jwt/JwtException.java @@ -0,0 +1,11 @@ +package com.ip.library.core.jwt; + +public class JwtException extends RuntimeException { + public JwtException(Throwable throwable) { + super(throwable); + } + + public JwtException(String message) { + super(message); + } +} diff --git a/SpringApp/library/src/main/java/com/ip/library/core/jwt/JwtFilter.java b/SpringApp/library/src/main/java/com/ip/library/core/jwt/JwtFilter.java new file mode 100644 index 0000000..821c467 --- /dev/null +++ b/SpringApp/library/src/main/java/com/ip/library/core/jwt/JwtFilter.java @@ -0,0 +1,57 @@ +package com.ip.library.core.jwt; + +import java.io.IOException; + +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import com.ip.library.users.service.UserService; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public class JwtFilter extends OncePerRequestFilter { + private static final String AUTHORIZATION = "Authorization"; + public static final String TOKEN_BEGIN_STR = "Bearer "; + + private final UserService userService; + + public JwtFilter(UserService userService) { + this.userService = userService; + } + + private String getTokenFromRequest(HttpServletRequest request) { + String bearer = request.getHeader(AUTHORIZATION); + if (StringUtils.hasText(bearer) && StringUtils.startsWithIgnoreCase(bearer, TOKEN_BEGIN_STR)) { + return bearer.substring(TOKEN_BEGIN_STR.length()); + } + return null; + } + + @Override + protected void doFilterInternal( + HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + final String token = getTokenFromRequest(request); + if (!StringUtils.hasText(token)) { + filterChain.doFilter(request, response); + return; + } + final UserDetails user = userService.getByToken(token); + final UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken( + user, + null, + user.getAuthorities()); + final SecurityContext context = SecurityContextHolder.createEmptyContext(); + context.setAuthentication(auth); + SecurityContextHolder.setContext(context); + filterChain.doFilter(request, response); + } +} diff --git a/SpringApp/library/src/main/java/com/ip/library/core/jwt/JwtProperties.java b/SpringApp/library/src/main/java/com/ip/library/core/jwt/JwtProperties.java new file mode 100644 index 0000000..3df306e --- /dev/null +++ b/SpringApp/library/src/main/java/com/ip/library/core/jwt/JwtProperties.java @@ -0,0 +1,18 @@ +package com.ip.library.core.jwt; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix = "jwt", ignoreInvalidFields = true) +public class JwtProperties { + private String key = ""; + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } +} diff --git a/SpringApp/library/src/main/java/com/ip/library/core/jwt/JwtProvider.java b/SpringApp/library/src/main/java/com/ip/library/core/jwt/JwtProvider.java new file mode 100644 index 0000000..1083a4d --- /dev/null +++ b/SpringApp/library/src/main/java/com/ip/library/core/jwt/JwtProvider.java @@ -0,0 +1,110 @@ +package com.ip.library.core.jwt; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.Date; +import java.util.Optional; +import java.util.UUID; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.exceptions.JWTVerificationException; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.auth0.jwt.interfaces.JWTVerifier; + +@Component +public class JwtProvider { + private static final Logger log = LoggerFactory.getLogger(JwtProvider.class); + + private static final byte[] HEX_ARRAY = "0123456789ABCDEF".getBytes(StandardCharsets.US_ASCII); + private static final String ISSUER = "auth0"; + + private final Algorithm algorithm; + private final JWTVerifier verifier; + + public JwtProvider(JwtProperties jwtProperties) { + final String key = jwtProperties.getKey(); + if (!StringUtils.hasText(key)) { + log.info("Generate new JWT key"); + try { + final MessageDigest salt = MessageDigest.getInstance("SHA-256"); + salt.update(UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8)); + final String hex = bytesToHex(salt.digest()); + log.info("Use generated JWT key for prod \n{}", hex); + algorithm = Algorithm.HMAC256(hex); + } catch (NoSuchAlgorithmException e) { + throw new JwtException(e); + } + } else { + log.info("Use JWT key from config \n{}", key); + algorithm = Algorithm.HMAC256(key); + } + verifier = JWT.require(algorithm) + .withIssuer(ISSUER) + .build(); + } + + private static String bytesToHex(byte[] bytes) { + byte[] hexChars = new byte[bytes.length * 2]; + for (int j = 0; j < bytes.length; j++) { + int v = bytes[j] & 0xFF; + hexChars[j * 2] = HEX_ARRAY[v >>> 4]; + hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F]; + } + return new String(hexChars, StandardCharsets.UTF_8); + } + + public String generateToken(String login) { + final Date issueDate = Date.from(LocalDate.now() + .atStartOfDay(ZoneId.systemDefault()) + .toInstant()); + final Date expireDate = Date.from(LocalDate.now() + .plusDays(15) + .atStartOfDay(ZoneId.systemDefault()) + .toInstant()); + return JWT.create() + .withIssuer(ISSUER) + .withIssuedAt(issueDate) + .withExpiresAt(expireDate) + .withSubject(login) + .sign(algorithm); + } + + private DecodedJWT validateToken(String token) { + try { + return verifier.verify(token); + } catch (JWTVerificationException e) { + throw new JwtException(String.format("Token verification error: %s", e.getMessage())); + } + } + + public boolean isTokenValid(String token) { + if (!StringUtils.hasText(token)) { + return false; + } + try { + validateToken(token); + return true; + } catch (JwtException e) { + log.error(e.getMessage()); + return false; + } + } + + public Optional getLoginFromToken(String token) { + try { + return Optional.ofNullable(validateToken(token).getSubject()); + } catch (JwtException e) { + log.error(e.getMessage()); + return Optional.empty(); + } + } +} diff --git a/SpringApp/library/src/main/java/com/ip/library/core/security/SecurityConfiguration.java b/SpringApp/library/src/main/java/com/ip/library/core/security/SecurityConfiguration.java new file mode 100644 index 0000000..7d442c0 --- /dev/null +++ b/SpringApp/library/src/main/java/com/ip/library/core/security/SecurityConfiguration.java @@ -0,0 +1,89 @@ +package com.ip.library.core.security; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.Customizer; +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.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer.FrameOptionsConfig; +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 org.springframework.util.CollectionUtils; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +import com.ip.library.core.configuration.Constants; +import com.ip.library.core.error.AdviceController; +import com.ip.library.core.jwt.JwtFilter; +import com.ip.library.users.model.UserRole; +import com.ip.library.users.service.UserService; + +@Configuration +@EnableWebSecurity +@EnableMethodSecurity(securedEnabled = true) +public class SecurityConfiguration { + @Bean + SecurityFilterChain filterChain( + HttpSecurity httpSecurity, + UserService userService, + AdviceController adviceController) throws Exception { + httpSecurity.headers(headers -> headers.frameOptions(FrameOptionsConfig::sameOrigin)); + httpSecurity.csrf(AbstractHttpConfigurer::disable); + httpSecurity.cors(Customizer.withDefaults()); + httpSecurity.sessionManagement(sessionManagement -> sessionManagement + .sessionCreationPolicy(SessionCreationPolicy.STATELESS)); + + httpSecurity.authorizeHttpRequests(requests -> requests + .requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/swagger-ui.html") + .permitAll()); + + httpSecurity.authorizeHttpRequests(requests -> requests + .requestMatchers("/h2-console/**").hasRole(UserRole.ADMIN.name()) + .requestMatchers(HttpMethod.POST, Constants.SIGNUP_URL).anonymous() + .requestMatchers(HttpMethod.POST, Constants.LOGIN_URL).anonymous() + .anyRequest().authenticated()); + + httpSecurity.exceptionHandling(exceptionHandling -> exceptionHandling + .authenticationEntryPoint(adviceController)); + + httpSecurity.addFilterBefore(new JwtFilter(userService), UsernamePasswordAuthenticationFilter.class); + + return httpSecurity.build(); + } + + @Bean + DaoAuthenticationProvider authenticationProvider(UserDetailsService userDetailsService) { + final DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); + authProvider.setUserDetailsService(userDetailsService); + authProvider.setPasswordEncoder(passwordEncoder()); + return authProvider; + } + + @Bean + PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + CorsFilter corsFilter() { + final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + final CorsConfiguration config = new CorsConfiguration(); + if (!CollectionUtils.isEmpty(config.getAllowedOrigins()) + || !CollectionUtils.isEmpty(config.getAllowedOriginPatterns())) { + source.registerCorsConfiguration(Constants.API_URL + "/**", config); + source.registerCorsConfiguration("/v3/api-docs", config); + source.registerCorsConfiguration("/swagger-ui/**", config); + } + return new CorsFilter(source); + } + +} diff --git a/SpringApp/library/src/main/java/com/ip/library/core/security/SwaggerConfiguration.java b/SpringApp/library/src/main/java/com/ip/library/core/security/SwaggerConfiguration.java new file mode 100644 index 0000000..88a9b5f --- /dev/null +++ b/SpringApp/library/src/main/java/com/ip/library/core/security/SwaggerConfiguration.java @@ -0,0 +1,31 @@ +package com.ip.library.core.security; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; + +@Configuration +@OpenAPIDefinition +public class SwaggerConfiguration { + private static final String SCHEME_NAME = "JWT"; + private static final String SCHEME = "bearer"; + + private SecurityScheme createBearerScheme() { + return new SecurityScheme() + .type(SecurityScheme.Type.HTTP) + .scheme(SCHEME); + } + + @Bean + OpenAPI customOpenApi() { + return new OpenAPI() + .components(new Components() + .addSecuritySchemes(SCHEME_NAME, createBearerScheme())) + .addSecurityItem(new SecurityRequirement().addList(SCHEME_NAME)); + } +} diff --git a/SpringApp/library/src/main/java/com/ip/library/core/security/UserPrincipal.java b/SpringApp/library/src/main/java/com/ip/library/core/security/UserPrincipal.java new file mode 100644 index 0000000..24c505a --- /dev/null +++ b/SpringApp/library/src/main/java/com/ip/library/core/security/UserPrincipal.java @@ -0,0 +1,64 @@ +package com.ip.library.core.security; + +import java.util.Collection; +import java.util.Set; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import com.ip.library.users.model.UserEntity; + +public class UserPrincipal implements UserDetails { + private final long id; + private final String username; + private final String password; + private final Set roles; + private final boolean active; + + public UserPrincipal(UserEntity user) { + this.id = user.getId(); + this.username = user.getLogin(); + this.password = user.getPassword(); + this.roles = Set.of(user.getRole()); + this.active = true; + } + + public Long getId() { + return id; + } + + @Override + public String getUsername() { + return username; + } + + @Override + public String getPassword() { + return password; + } + + @Override + public Collection getAuthorities() { + return roles; + } + + @Override + public boolean isEnabled() { + return active; + } + + @Override + public boolean isAccountNonExpired() { + return isEnabled(); + } + + @Override + public boolean isAccountNonLocked() { + return isEnabled(); + } + + @Override + public boolean isCredentialsNonExpired() { + return isEnabled(); + } +} diff --git a/SpringApp/library/src/main/java/com/ip/library/users/api/LoginController.java b/SpringApp/library/src/main/java/com/ip/library/users/api/LoginController.java new file mode 100644 index 0000000..4a4c51a --- /dev/null +++ b/SpringApp/library/src/main/java/com/ip/library/users/api/LoginController.java @@ -0,0 +1,47 @@ +package com.ip.library.users.api; + +import java.util.Objects; + +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.ip.library.core.configuration.Constants; +import com.ip.library.users.model.UserEntity; +import com.ip.library.users.service.UserService; + +@RestController +public class LoginController { + private final UserService userService; + + public LoginController(UserService userService) { + this.userService = userService; + } + + @PostMapping(Constants.LOGIN_URL) + public String login( + @RequestParam(name = "login") String login, + @RequestParam(name = "password") String password) { + return userService.login(login, password); + } + + @PostMapping(Constants.SIGNUP_URL) + public boolean signup( + @RequestParam(name = "login") String login, + @RequestParam(name = "password") String password, + @RequestParam(name = "password2") String password2) { + if (!StringUtils.hasText(login)) { + throw new IllegalArgumentException("Invalid login"); + } + if (!StringUtils.hasText(password)) { + throw new IllegalArgumentException("Invalid password"); + } + if (!Objects.equals(password, password2)) { + throw new IllegalArgumentException("Passwords are not equals"); + } + final UserEntity user = new UserEntity(login, password); + userService.create(user); + return true; + } +} diff --git a/SpringApp/library/src/main/java/com/ip/library/users/api/UserController.java b/SpringApp/library/src/main/java/com/ip/library/users/api/UserController.java index c28a36a..ab75e47 100644 --- a/SpringApp/library/src/main/java/com/ip/library/users/api/UserController.java +++ b/SpringApp/library/src/main/java/com/ip/library/users/api/UserController.java @@ -50,8 +50,8 @@ public class UserController { @GetMapping public PageDto getAll( - @RequestParam(name = "page", defaultValue = "0") int page, - @RequestParam(name = "size", defaultValue = Constants.DEFAULT_PAGE_SIZE) int size) { + @RequestParam(name = "page", defaultValue = "0") int page, + @RequestParam(name = "size", defaultValue = Constants.DEFAULT_PAGE_SIZE) int size) { return PageDtoMapper.toDto(userService.getAll(page, size), this::toUserDto); } diff --git a/SpringApp/library/src/main/java/com/ip/library/users/api/UserDto.java b/SpringApp/library/src/main/java/com/ip/library/users/api/UserDto.java index 52efe25..67297aa 100644 --- a/SpringApp/library/src/main/java/com/ip/library/users/api/UserDto.java +++ b/SpringApp/library/src/main/java/com/ip/library/users/api/UserDto.java @@ -11,14 +11,6 @@ public class UserDto { @NotBlank @Size(min = 5, max = 20) private String login; - @JsonProperty(access = JsonProperty.Access.READ_ONLY) - @NotBlank - @Size(min = 5, max = 20) - private String password; - @JsonProperty(access = JsonProperty.Access.READ_ONLY) - @NotBlank - @Size(min = 4, max = 20) - private String role; public Long getId() { return id; @@ -35,20 +27,4 @@ public class UserDto { public void setLogin(String name) { this.login = name; } - - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } - - public String getRole() { - return role; - } - - public void setRole(String role) { - this.role = role; - } } diff --git a/SpringApp/library/src/main/java/com/ip/library/users/model/UserEntity.java b/SpringApp/library/src/main/java/com/ip/library/users/model/UserEntity.java index 756654a..ad9a684 100644 --- a/SpringApp/library/src/main/java/com/ip/library/users/model/UserEntity.java +++ b/SpringApp/library/src/main/java/com/ip/library/users/model/UserEntity.java @@ -22,8 +22,7 @@ public class UserEntity extends BaseEntity { private String login; @Column(nullable = false, unique = false, length = 20) private String password; - @Column(nullable = false, unique = false, length = 20) - private String role = "user"; + private UserRole role = UserRole.USER; @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) @OrderBy("id ASC") private Set favorites = new HashSet<>(); @@ -57,11 +56,11 @@ public class UserEntity extends BaseEntity { this.password = password; } - public String getRole() { + public UserRole getRole() { return role; } - public void setRole(String role) { + public void setRole(UserRole role) { this.role = role; } diff --git a/SpringApp/library/src/main/java/com/ip/library/users/model/UserRole.java b/SpringApp/library/src/main/java/com/ip/library/users/model/UserRole.java new file mode 100644 index 0000000..0eb24b8 --- /dev/null +++ b/SpringApp/library/src/main/java/com/ip/library/users/model/UserRole.java @@ -0,0 +1,22 @@ +package com.ip.library.users.model; + +import org.springframework.security.core.GrantedAuthority; + +public enum UserRole implements GrantedAuthority { + ADMIN, + USER; + + private static final String PREFIX = "ROLE_"; + + @Override + public String getAuthority() { + return PREFIX + this.name(); + } + + public static final class Secured { + private Secured() {} + + public static final String ADMIN = PREFIX + "ADMIN"; + public static final String USER = PREFIX + "USER"; + } +} diff --git a/SpringApp/library/src/main/java/com/ip/library/users/service/UserService.java b/SpringApp/library/src/main/java/com/ip/library/users/service/UserService.java index 1809381..5959641 100644 --- a/SpringApp/library/src/main/java/com/ip/library/users/service/UserService.java +++ b/SpringApp/library/src/main/java/com/ip/library/users/service/UserService.java @@ -5,23 +5,39 @@ import java.util.stream.StreamSupport; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.ip.library.books.model.BookEntity; import com.ip.library.books.service.BookService; import com.ip.library.core.error.NotFoundException; +import com.ip.library.core.jwt.JwtException; +import com.ip.library.core.jwt.JwtProvider; +import com.ip.library.core.security.UserPrincipal; import com.ip.library.users.model.UserEntity; +import com.ip.library.users.model.UserRole; import com.ip.library.users.repository.UserRepository; @Service -public class UserService { +public class UserService implements UserDetailsService{ private final UserRepository repository; private final BookService bookService; + private final PasswordEncoder passwordEncoder; + private final JwtProvider jwtProvider; - public UserService(UserRepository repository, BookService bookService) { + public UserService( + UserRepository repository, + BookService bookService, + PasswordEncoder passwordEncoder, + JwtProvider jwtProvider) { this.repository = repository; this.bookService = bookService; + this.jwtProvider = jwtProvider; + this.passwordEncoder = passwordEncoder; } private void checkLoginUniqueness(String name){ @@ -48,6 +64,12 @@ public class UserService { .orElseThrow(() -> new NotFoundException(UserEntity.class, id)); } + @Transactional(readOnly = true) + public UserEntity getByLogin(String login) { + return repository.findByLoginIgnoreCase(login) + .orElseThrow(() -> new IllegalArgumentException("Invalid login")); + } + @Transactional public UserEntity create(UserEntity entity) { checkLoginUniqueness(entity.getLogin()); @@ -72,14 +94,14 @@ public class UserService { @Transactional public UserEntity giveAdminRole(long id) { final UserEntity existsEntity = get(id); - existsEntity.setRole("admin"); + existsEntity.setRole(UserRole.ADMIN); return repository.save(existsEntity); } @Transactional public UserEntity giveUserRole(long id) { final UserEntity existsEntity = get(id); - existsEntity.setRole("user"); + existsEntity.setRole(UserRole.USER); return repository.save(existsEntity); } @@ -106,4 +128,29 @@ public class UserService { public Page getUserFavorities (long userId, int page, int size) { return repository.getUserFavorities(userId, PageRequest.of(page, size)); } + + @Transactional(readOnly = true) + public UserDetails getByToken(String token) { + if (!jwtProvider.isTokenValid(token)) + throw new JwtException("Bad token"); + final String userLogin = jwtProvider.getLoginFromToken(token) + .orElseThrow(() -> new JwtException("Token is not contain Login")); + return loadUserByUsername(userLogin); + } + + @Transactional(readOnly = true) + public String login(String login, String password) { + final UserEntity existsUser = getByLogin(login); + if (!passwordEncoder.matches(password, existsUser.getPassword())) + throw new IllegalArgumentException("Invalid login"); + return jwtProvider.generateToken(existsUser.getLogin()); + + } + + @Override + @Transactional(readOnly = true) + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + final UserEntity existsUser = getByLogin(username); + return new UserPrincipal(existsUser); + } } diff --git a/SpringApp/library/src/test/java/com/ip/library/UsersTests.java b/SpringApp/library/src/test/java/com/ip/library/UsersTests.java index 77d86a1..76dae9a 100644 --- a/SpringApp/library/src/test/java/com/ip/library/UsersTests.java +++ b/SpringApp/library/src/test/java/com/ip/library/UsersTests.java @@ -10,6 +10,7 @@ import org.springframework.dao.DataIntegrityViolationException; import com.ip.library.core.error.NotFoundException; import com.ip.library.users.model.UserEntity; +import com.ip.library.users.model.UserRole; import com.ip.library.users.service.UserService; @SpringBootTest @@ -36,7 +37,7 @@ class UsersTests { Assertions.assertEquals(3, userService.getAll().size()); Assertions.assertEquals("user3", user.getLogin()); Assertions.assertEquals("aqw2sed45", user.getPassword()); - Assertions.assertEquals("user", user.getRole()); + Assertions.assertEquals(UserRole.USER, user.getRole()); Assertions.assertEquals(0, user.getFavorites().size()); } @@ -79,11 +80,11 @@ class UsersTests { @Test void changeRoleTest() { - Assertions.assertEquals("user", user.getRole()); + Assertions.assertEquals(UserRole.USER, user.getRole()); user = userService.giveAdminRole(user.getId()); - Assertions.assertEquals("admin", user.getRole()); + Assertions.assertEquals(UserRole.ADMIN, user.getRole()); user = userService.giveUserRole(user.getId()); - Assertions.assertEquals("user", user.getRole()); + Assertions.assertEquals(UserRole.USER, user.getRole()); } @Test From 783f8886c2bf90641af2277aa9eb5f815499df3a Mon Sep 17 00:00:00 2001 From: Zakharov_Rostislav Date: Thu, 23 May 2024 14:23:16 +0400 Subject: [PATCH 2/3] lab-5 Change Controllers to secure admin-accesible actions from violations --- .../library/authors/api/AuthorController.java | 3 + .../ip/library/books/api/BookController.java | 8 +-- .../ip/library/types/api/TypeController.java | 3 + .../library/users/api/UserBookController.java | 59 +++++++++++++++++++ .../ip/library/users/api/UserController.java | 39 ++---------- 5 files changed, 74 insertions(+), 38 deletions(-) create mode 100644 SpringApp/library/src/main/java/com/ip/library/users/api/UserBookController.java diff --git a/SpringApp/library/src/main/java/com/ip/library/authors/api/AuthorController.java b/SpringApp/library/src/main/java/com/ip/library/authors/api/AuthorController.java index 31cf660..431ff51 100644 --- a/SpringApp/library/src/main/java/com/ip/library/authors/api/AuthorController.java +++ b/SpringApp/library/src/main/java/com/ip/library/authors/api/AuthorController.java @@ -3,6 +3,7 @@ package com.ip.library.authors.api; import java.util.List; import org.modelmapper.ModelMapper; +import org.springframework.security.access.annotation.Secured; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -13,6 +14,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.ip.library.core.configuration.Constants; +import com.ip.library.users.model.UserRole; import com.ip.library.authors.model.AuthorEntity; import com.ip.library.authors.service.AuthorService; @@ -20,6 +22,7 @@ import jakarta.validation.Valid; @RestController +@Secured(value = UserRole.Secured.ADMIN) @RequestMapping(Constants.API_URL + "/author") public class AuthorController { private final AuthorService authorService; diff --git a/SpringApp/library/src/main/java/com/ip/library/books/api/BookController.java b/SpringApp/library/src/main/java/com/ip/library/books/api/BookController.java index 537a191..4d4e914 100644 --- a/SpringApp/library/src/main/java/com/ip/library/books/api/BookController.java +++ b/SpringApp/library/src/main/java/com/ip/library/books/api/BookController.java @@ -3,6 +3,7 @@ package com.ip.library.books.api; import java.util.List; import org.modelmapper.ModelMapper; +import org.springframework.security.access.annotation.Secured; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -17,10 +18,12 @@ import com.ip.library.books.model.BookEntity; import com.ip.library.books.service.BookService; import com.ip.library.core.configuration.Constants; import com.ip.library.types.service.TypeService; +import com.ip.library.users.model.UserRole; import jakarta.validation.Valid; @RestController +@Secured(value = UserRole.Secured.ADMIN) @RequestMapping(Constants.API_URL + "/book") public class BookController { private final BookService bookService; @@ -74,11 +77,6 @@ public class BookController { return toBookDto(bookService.delete(id)); } - @GetMapping("/{bookId}/users/number") - public int getBookSubscribersNumber(@PathVariable(name = "bookId") Long bookId) { - return bookService.getBookSubscribersNumber(bookId); - } - @GetMapping("/{bookId}/author/{authorId}") public boolean addAuthor( @PathVariable(name = "bookId") Long bookId, diff --git a/SpringApp/library/src/main/java/com/ip/library/types/api/TypeController.java b/SpringApp/library/src/main/java/com/ip/library/types/api/TypeController.java index 3846ce9..f6e6125 100644 --- a/SpringApp/library/src/main/java/com/ip/library/types/api/TypeController.java +++ b/SpringApp/library/src/main/java/com/ip/library/types/api/TypeController.java @@ -3,6 +3,7 @@ package com.ip.library.types.api; import java.util.List; import org.modelmapper.ModelMapper; +import org.springframework.security.access.annotation.Secured; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -15,10 +16,12 @@ import org.springframework.web.bind.annotation.RestController; import com.ip.library.core.configuration.Constants; import com.ip.library.types.model.TypeEntity; import com.ip.library.types.service.TypeService; +import com.ip.library.users.model.UserRole; import jakarta.validation.Valid; @RestController +@Secured(value = UserRole.Secured.ADMIN) @RequestMapping(Constants.API_URL + "/type") public class TypeController { private final TypeService typeService; diff --git a/SpringApp/library/src/main/java/com/ip/library/users/api/UserBookController.java b/SpringApp/library/src/main/java/com/ip/library/users/api/UserBookController.java new file mode 100644 index 0000000..6226093 --- /dev/null +++ b/SpringApp/library/src/main/java/com/ip/library/users/api/UserBookController.java @@ -0,0 +1,59 @@ +package com.ip.library.users.api; + +import java.util.List; + +import org.modelmapper.ModelMapper; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.ip.library.books.api.BookDto; +import com.ip.library.books.model.BookEntity; +import com.ip.library.books.service.BookService; +import com.ip.library.core.configuration.Constants; +import com.ip.library.users.service.UserService; + +@RestController +@RequestMapping(Constants.API_URL + "/user/{userId}/book") +public class UserBookController { + private final UserService userService; + private final ModelMapper modelMapper; + private final BookService bookService; + + public UserBookController( + UserService userService, + ModelMapper modelMapper, + BookService bookService) { + this.userService = userService; + this.modelMapper = modelMapper; + this.bookService = bookService; + } + + private BookDto toBookDto (BookEntity entity) { + BookDto bookDto = modelMapper.map(entity, BookDto.class); + bookDto.setAuthorId(entity.getAuthorsBooks().stream().map(x -> x.getAuthor().getId()).toList()); + return bookDto; + } + + @GetMapping("/{bookId}") + public boolean addFavorite( + @PathVariable(name = "userId") Long userId, + @PathVariable(name = "bookId") Long bookId) { + return userService.addFavorite(userId, bookId); + } + + @GetMapping("/all-books") + public List getUserFavorites( + @PathVariable(name = "userId") Long userId, + @RequestParam(name = "page", defaultValue = "0") int page, + @RequestParam(name = "size", defaultValue = Constants.DEFAULT_PAGE_SIZE) int size) { + return userService.getUserFavorities(userId, page, size).stream().map(this::toBookDto).toList(); + } + + @GetMapping("/{bookId}/number") + public int getBookSubscribersNumber(@PathVariable(name = "bookId") Long bookId) { + return bookService.getBookSubscribersNumber(bookId); + } +} diff --git a/SpringApp/library/src/main/java/com/ip/library/users/api/UserController.java b/SpringApp/library/src/main/java/com/ip/library/users/api/UserController.java index ab75e47..e845453 100644 --- a/SpringApp/library/src/main/java/com/ip/library/users/api/UserController.java +++ b/SpringApp/library/src/main/java/com/ip/library/users/api/UserController.java @@ -1,8 +1,7 @@ package com.ip.library.users.api; -import java.util.List; - import org.modelmapper.ModelMapper; +import org.springframework.security.access.annotation.Secured; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -13,32 +12,28 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import com.ip.library.books.api.BookDto; -import com.ip.library.books.model.BookEntity; import com.ip.library.core.api.PageDto; import com.ip.library.core.api.PageDtoMapper; import com.ip.library.core.configuration.Constants; import com.ip.library.users.model.UserEntity; +import com.ip.library.users.model.UserRole; import com.ip.library.users.service.UserService; import jakarta.validation.Valid; @RestController +@Secured(value = UserRole.Secured.ADMIN) @RequestMapping(Constants.API_URL + "/user") public class UserController { private final UserService userService; private final ModelMapper modelMapper; - public UserController(UserService userService, ModelMapper modelMapper) { + public UserController( + UserService userService, + ModelMapper modelMapper) { this.userService = userService; this.modelMapper = modelMapper; } - - private BookDto toBookDto (BookEntity entity) { - BookDto bookDto = modelMapper.map(entity, BookDto.class); - bookDto.setAuthorId(entity.getAuthorsBooks().stream().map(x -> x.getAuthor().getId()).toList()); - return bookDto; - } private UserDto toUserDto(UserEntity entity) { return modelMapper.map(entity, UserDto.class); @@ -79,26 +74,4 @@ public class UserController { public UserDto changePassword(@PathVariable(name = "id") Long id, @RequestBody String newPassword) { return toUserDto(userService.changePassword(id, newPassword)); } - - @DeleteMapping("/{userId}/books/{bookId}") - public boolean removeFavorite( - @PathVariable(name = "userId") Long userId, - @PathVariable(name = "bookId") Long bookId) { - return true; - } - - @GetMapping("/{userId}/books/{bookId}") - public boolean addFavorite( - @PathVariable(name = "userId") Long userId, - @PathVariable(name = "bookId") Long bookId) { - return userService.addFavorite(userId, bookId); - } - - @GetMapping("/{userId}/books") - public List getUserFavorites( - @PathVariable(name = "userId") Long userId, - @RequestParam(name = "page", defaultValue = "0") int page, - @RequestParam(name = "size", defaultValue = Constants.DEFAULT_PAGE_SIZE) int size) { - return userService.getUserFavorities(userId, page, size).stream().map(this::toBookDto).toList(); - } } From 1d2b52444f9c3344c13985203567a4fcbfa2f23c Mon Sep 17 00:00:00 2001 From: Zakharov_Rostislav Date: Thu, 23 May 2024 15:34:37 +0400 Subject: [PATCH 3/3] lab-5 fix errors with password encoding --- .gitignore | 2 -- .../java/com/ip/library/users/model/UserEntity.java | 2 +- .../java/com/ip/library/users/service/UserService.java | 10 ++++++++++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index a057160..2c4a283 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1 @@ SpringApp/data.mv.db -SpringApp/library/data.mv.db -SpringApp/library/data.trace.db diff --git a/SpringApp/library/src/main/java/com/ip/library/users/model/UserEntity.java b/SpringApp/library/src/main/java/com/ip/library/users/model/UserEntity.java index ad9a684..9d634eb 100644 --- a/SpringApp/library/src/main/java/com/ip/library/users/model/UserEntity.java +++ b/SpringApp/library/src/main/java/com/ip/library/users/model/UserEntity.java @@ -20,7 +20,7 @@ import jakarta.persistence.Table; public class UserEntity extends BaseEntity { @Column(nullable = false, unique = true, length = 20) private String login; - @Column(nullable = false, unique = false, length = 20) + @Column(nullable = false, unique = false) private String password; private UserRole role = UserRole.USER; @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) diff --git a/SpringApp/library/src/main/java/com/ip/library/users/service/UserService.java b/SpringApp/library/src/main/java/com/ip/library/users/service/UserService.java index 5959641..2a2e333 100644 --- a/SpringApp/library/src/main/java/com/ip/library/users/service/UserService.java +++ b/SpringApp/library/src/main/java/com/ip/library/users/service/UserService.java @@ -1,6 +1,7 @@ package com.ip.library.users.service; import java.util.List; +import java.util.Optional; import java.util.stream.StreamSupport; import org.springframework.data.domain.Page; @@ -11,9 +12,11 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; import com.ip.library.books.model.BookEntity; import com.ip.library.books.service.BookService; +import com.ip.library.core.configuration.Constants; import com.ip.library.core.error.NotFoundException; import com.ip.library.core.jwt.JwtException; import com.ip.library.core.jwt.JwtProvider; @@ -72,7 +75,14 @@ public class UserService implements UserDetailsService{ @Transactional public UserEntity create(UserEntity entity) { + if (entity == null) + throw new IllegalArgumentException("Entity is null"); checkLoginUniqueness(entity.getLogin()); + final String password = Optional.ofNullable(entity.getPassword()).orElse(""); + entity.setPassword( + passwordEncoder.encode( + StringUtils.hasText(password.strip()) ? password : Constants.DEFAULT_PASSWORD)); + entity.setRole(Optional.ofNullable(entity.getRole()).orElse(UserRole.USER)); return repository.save(entity); }