From 299a62b3648f997bba3803aa157f5dfef2b5cd97 Mon Sep 17 00:00:00 2001 From: parap Date: Tue, 16 May 2023 10:11:37 +0400 Subject: [PATCH] lab6 react --- build.gradle | 1 + data.mv.db | Bin 167936 -> 192512 bytes .../configuration/OpenAPI30Configuration.java | 28 +++++ .../PasswordEncoderConfiguration.java | 2 +- .../configuration/SecurityConfiguration.java | 55 +++++---- .../labs/configuration/WebConfiguration.java | 11 ++ .../labs/configuration/jwt/JwtException.java | 11 ++ .../labs/configuration/jwt/JwtFilter.java | 72 ++++++++++++ .../labs/configuration/jwt/JwtProperties.java | 27 +++++ .../labs/configuration/jwt/JwtProvider.java | 107 ++++++++++++++++++ .../labs/films/controller/UserController.java | 39 +++++++ .../films/controller/UserMvcController.java | 42 ------- .../controller/UserSignupMvcController.java | 51 --------- .../ru/ip/labs/labs/films/dto/UserDto.java | 28 ++--- .../ip/labs/labs/films/dto/UserInfoDTO.java | 27 +++++ .../films/service/UserExistsException.java | 7 ++ .../films/service/UserNotFoundException.java | 7 ++ .../labs/labs/films/service/UserService.java | 38 ++++++- src/main/resources/application.properties | 2 + 19 files changed, 419 insertions(+), 136 deletions(-) create mode 100644 src/main/java/ru/ip/labs/labs/configuration/OpenAPI30Configuration.java create mode 100644 src/main/java/ru/ip/labs/labs/configuration/jwt/JwtException.java create mode 100644 src/main/java/ru/ip/labs/labs/configuration/jwt/JwtFilter.java create mode 100644 src/main/java/ru/ip/labs/labs/configuration/jwt/JwtProperties.java create mode 100644 src/main/java/ru/ip/labs/labs/configuration/jwt/JwtProvider.java create mode 100644 src/main/java/ru/ip/labs/labs/films/controller/UserController.java delete mode 100644 src/main/java/ru/ip/labs/labs/films/controller/UserMvcController.java delete mode 100644 src/main/java/ru/ip/labs/labs/films/controller/UserSignupMvcController.java create mode 100644 src/main/java/ru/ip/labs/labs/films/dto/UserInfoDTO.java create mode 100644 src/main/java/ru/ip/labs/labs/films/service/UserExistsException.java create mode 100644 src/main/java/ru/ip/labs/labs/films/service/UserNotFoundException.java diff --git a/build.gradle b/build.gradle index 8fd4e4b..dbeed0f 100644 --- a/build.gradle +++ b/build.gradle @@ -29,6 +29,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5' + implementation 'com.auth0:java-jwt:4.4.0' implementation 'org.springdoc:springdoc-openapi-ui:1.6.5' diff --git a/data.mv.db b/data.mv.db index 2c4a8476735edb40bcae6df6873821f97cf6c95d..6f6e554cf28bcfcb05f1b9f3b08b9f16b231cf9a 100644 GIT binary patch delta 14405 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 D;aOC5 diff --git a/src/main/java/ru/ip/labs/labs/configuration/OpenAPI30Configuration.java b/src/main/java/ru/ip/labs/labs/configuration/OpenAPI30Configuration.java new file mode 100644 index 0000000..ae48092 --- /dev/null +++ b/src/main/java/ru/ip/labs/labs/configuration/OpenAPI30Configuration.java @@ -0,0 +1,28 @@ +package ru.ip.labs.labs.configuration; + +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; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import ru.ip.labs.labs.configuration.jwt.JwtFilter; + +@Configuration +public class OpenAPI30Configuration { + public static final String API_PREFIX = "/api/1.0"; + + @Bean + public OpenAPI customizeOpenAPI() { + final String securitySchemeName = JwtFilter.TOKEN_BEGIN_STR; + return new OpenAPI() + .addSecurityItem(new SecurityRequirement() + .addList(securitySchemeName)) + .components(new Components() + .addSecuritySchemes(securitySchemeName, new SecurityScheme() + .name(securitySchemeName) + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT"))); + } +} diff --git a/src/main/java/ru/ip/labs/labs/configuration/PasswordEncoderConfiguration.java b/src/main/java/ru/ip/labs/labs/configuration/PasswordEncoderConfiguration.java index 5aef121..fa789c4 100644 --- a/src/main/java/ru/ip/labs/labs/configuration/PasswordEncoderConfiguration.java +++ b/src/main/java/ru/ip/labs/labs/configuration/PasswordEncoderConfiguration.java @@ -8,7 +8,7 @@ import org.springframework.security.crypto.password.PasswordEncoder; @Configuration public class PasswordEncoderConfiguration { @Bean - public PasswordEncoder createPasswordEncoder() { + public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } } diff --git a/src/main/java/ru/ip/labs/labs/configuration/SecurityConfiguration.java b/src/main/java/ru/ip/labs/labs/configuration/SecurityConfiguration.java index ec3ec4a..6f37104 100644 --- a/src/main/java/ru/ip/labs/labs/configuration/SecurityConfiguration.java +++ b/src/main/java/ru/ip/labs/labs/configuration/SecurityConfiguration.java @@ -5,25 +5,28 @@ import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import ru.ip.labs.labs.films.controller.UserSignupMvcController; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import ru.ip.labs.labs.configuration.jwt.JwtFilter; +import ru.ip.labs.labs.films.controller.UserController; import ru.ip.labs.labs.films.models.UserRole; import ru.ip.labs.labs.films.service.UserService; @Configuration -@EnableWebSecurity -@EnableGlobalMethodSecurity(securedEnabled = true) public class SecurityConfiguration extends WebSecurityConfigurerAdapter { private final Logger log = LoggerFactory.getLogger(SecurityConfiguration.class); - private static final String LOGIN_URL = "/login"; + + public static final String SPA_URL_MASK = "/{path:[^\\.]*}"; + private final UserService userService; + private final JwtFilter jwtFilter; public SecurityConfiguration(UserService userService) { this.userService = userService; + this.jwtFilter = new JwtFilter(userService); createAdminOnStartup(); } @@ -37,31 +40,37 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { - http.headers().frameOptions().sameOrigin().and() - .cors().and() + log.info("Creating security configuration"); + http.cors() + .and() .csrf().disable() + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() .authorizeRequests() - .antMatchers(UserSignupMvcController.SIGNUP_URL).permitAll() - .antMatchers(HttpMethod.GET, LOGIN_URL).permitAll() - .anyRequest().authenticated() + .antMatchers("/", SPA_URL_MASK).permitAll() + .antMatchers(HttpMethod.POST, UserController.URL_SIGNUP).permitAll() + .antMatchers(HttpMethod.POST, UserController.URL_LOGIN).permitAll() + .anyRequest() + .authenticated() .and() - .formLogin() - .loginPage(LOGIN_URL).permitAll() - .and() - .logout().permitAll(); + .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class) + .anonymous(); } @Override - protected void configure(AuthenticationManagerBuilder auth) throws Exception { - auth.userDetailsService(userService); + protected void configure(AuthenticationManagerBuilder builder) throws Exception { + builder.userDetailsService(userService); } @Override public void configure(WebSecurity web) { - web.ignoring() - .antMatchers("/css/**") - .antMatchers("/js/**") - .antMatchers("/templates/**") - .antMatchers("/webjars/**"); + web + .ignoring() + .antMatchers(HttpMethod.OPTIONS, "/**") + .antMatchers("/**/*.{js,html,css,png}") + .antMatchers("/swagger-ui/index.html") + .antMatchers("/webjars/**") + .antMatchers("/swagger-resources/**") + .antMatchers("/v3/api-docs/**"); } -} \ No newline at end of file +} diff --git a/src/main/java/ru/ip/labs/labs/configuration/WebConfiguration.java b/src/main/java/ru/ip/labs/labs/configuration/WebConfiguration.java index 56c9ee5..9d34d4e 100644 --- a/src/main/java/ru/ip/labs/labs/configuration/WebConfiguration.java +++ b/src/main/java/ru/ip/labs/labs/configuration/WebConfiguration.java @@ -1,6 +1,11 @@ package ru.ip.labs.labs.configuration; +import org.springframework.boot.web.server.ErrorPage; +import org.springframework.boot.web.server.WebServerFactoryCustomizer; +import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatus; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; @@ -17,6 +22,12 @@ public class WebConfiguration implements WebMvcConfigurer { registry.addViewController("catalogs"); registry.addViewController("login"); } + + @Bean + public WebServerFactoryCustomizer containerCustomizer() { + return container -> container.addErrorPages(new ErrorPage(HttpStatus.NOT_FOUND, "/notFound")); + } + @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**").allowedMethods("*"); diff --git a/src/main/java/ru/ip/labs/labs/configuration/jwt/JwtException.java b/src/main/java/ru/ip/labs/labs/configuration/jwt/JwtException.java new file mode 100644 index 0000000..be842ff --- /dev/null +++ b/src/main/java/ru/ip/labs/labs/configuration/jwt/JwtException.java @@ -0,0 +1,11 @@ +package ru.ip.labs.labs.configuration.jwt; + +public class JwtException extends RuntimeException { + public JwtException(Throwable throwable) { + super(throwable); + } + + public JwtException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/ip/labs/labs/configuration/jwt/JwtFilter.java b/src/main/java/ru/ip/labs/labs/configuration/jwt/JwtFilter.java new file mode 100644 index 0000000..162a794 --- /dev/null +++ b/src/main/java/ru/ip/labs/labs/configuration/jwt/JwtFilter.java @@ -0,0 +1,72 @@ +package ru.ip.labs.labs.configuration.jwt; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.http.MediaType; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.GenericFilterBean; +import ru.ip.labs.labs.films.service.UserService; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class JwtFilter extends GenericFilterBean { + 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) && bearer.startsWith(TOKEN_BEGIN_STR)) { + return bearer.substring(TOKEN_BEGIN_STR.length()); + } + return null; + } + + private void raiseException(ServletResponse response, int status, String message) throws IOException { + if (response instanceof final HttpServletResponse httpResponse) { + httpResponse.setContentType(MediaType.APPLICATION_JSON_VALUE); + httpResponse.setStatus(status); + final byte[] body = new ObjectMapper().writeValueAsBytes(message); + response.getOutputStream().write(body); + } + } + + @Override + public void doFilter(ServletRequest request, + ServletResponse response, + FilterChain chain) throws IOException, ServletException { + if (request instanceof final HttpServletRequest httpRequest) { + final String token = getTokenFromRequest(httpRequest); + if (StringUtils.hasText(token)) { + try { + final UserDetails user = userService.loadUserByToken(token); + final UsernamePasswordAuthenticationToken auth = + new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities()); + SecurityContextHolder.getContext().setAuthentication(auth); + } catch (JwtException e) { + raiseException(response, HttpServletResponse.SC_UNAUTHORIZED, e.getMessage()); + return; + } catch (Exception e) { + e.printStackTrace(); + raiseException(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, + String.format("Internal error: %s", e.getMessage())); + return; + } + } + } + chain.doFilter(request, response); + } +} diff --git a/src/main/java/ru/ip/labs/labs/configuration/jwt/JwtProperties.java b/src/main/java/ru/ip/labs/labs/configuration/jwt/JwtProperties.java new file mode 100644 index 0000000..a55c0c9 --- /dev/null +++ b/src/main/java/ru/ip/labs/labs/configuration/jwt/JwtProperties.java @@ -0,0 +1,27 @@ +package ru.ip.labs.labs.configuration.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 devToken = ""; + private Boolean isDev = true; + + public String getDevToken() { + return devToken; + } + + public void setDevToken(String devToken) { + this.devToken = devToken; + } + + public Boolean isDev() { + return isDev; + } + + public void setDev(Boolean dev) { + isDev = dev; + } +} diff --git a/src/main/java/ru/ip/labs/labs/configuration/jwt/JwtProvider.java b/src/main/java/ru/ip/labs/labs/configuration/jwt/JwtProvider.java new file mode 100644 index 0000000..8b7694f --- /dev/null +++ b/src/main/java/ru/ip/labs/labs/configuration/jwt/JwtProvider.java @@ -0,0 +1,107 @@ +package ru.ip.labs.labs.configuration.jwt; + +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; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +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; + +@Component +public class JwtProvider { + private final static Logger LOG = LoggerFactory.getLogger(JwtProvider.class); + + private final static byte[] HEX_ARRAY = "0123456789ABCDEF".getBytes(StandardCharsets.US_ASCII); + private final static String ISSUER = "auth0"; + + private final Algorithm algorithm; + private final JWTVerifier verifier; + + public JwtProvider(JwtProperties jwtProperties) { + if (!jwtProperties.isDev()) { + LOG.info("Generate new JWT key for prod"); + try { + final MessageDigest salt = MessageDigest.getInstance("SHA-256"); + salt.update(UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8)); + LOG.info("Use generated JWT key for prod \n{}", bytesToHex(salt.digest())); + algorithm = Algorithm.HMAC256(bytesToHex(salt.digest())); + } catch (NoSuchAlgorithmException e) { + throw new JwtException(e); + } + } else { + LOG.info("Use default JWT key for dev \n{}", jwtProperties.getDevToken()); + algorithm = Algorithm.HMAC256(jwtProperties.getDevToken()); + } + 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/src/main/java/ru/ip/labs/labs/films/controller/UserController.java b/src/main/java/ru/ip/labs/labs/films/controller/UserController.java new file mode 100644 index 0000000..1ffbffc --- /dev/null +++ b/src/main/java/ru/ip/labs/labs/films/controller/UserController.java @@ -0,0 +1,39 @@ +package ru.ip.labs.labs.films.controller; + +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.bind.annotation.*; +import ru.ip.labs.labs.films.dto.UserDto; +import ru.ip.labs.labs.films.dto.UserInfoDTO; +import ru.ip.labs.labs.films.models.User; +import ru.ip.labs.labs.films.service.UserService; + +import javax.validation.Valid; + +@RestController +public class UserController { + public static final String URL_LOGIN = "/jwt/login"; + public static final String URL_SIGNUP = "/jwt/signup"; + + private final UserService userService; + + public UserController(UserService userService) { + this.userService = userService; + } + + @GetMapping("/user") + public String findUser(@RequestParam("token") String token) { + UserDetails userDetails = userService.loadUserByToken(token); + User user = userService.findByLogin(userDetails.getUsername()); + return user.getRole().toString(); + } + + @PostMapping(URL_SIGNUP) + public UserInfoDTO signup(@RequestBody @Valid UserDto userDto) { + return userService.signupAndGetToken(userDto); + } + + @PostMapping(URL_LOGIN) + public String login(@RequestBody @Valid UserDto userDto) { + return userService.loginAndGetToken(userDto); + } +} diff --git a/src/main/java/ru/ip/labs/labs/films/controller/UserMvcController.java b/src/main/java/ru/ip/labs/labs/films/controller/UserMvcController.java deleted file mode 100644 index e8dfc78..0000000 --- a/src/main/java/ru/ip/labs/labs/films/controller/UserMvcController.java +++ /dev/null @@ -1,42 +0,0 @@ -package ru.ip.labs.labs.films.controller; - -import org.springframework.data.domain.Page; -import org.springframework.security.access.annotation.Secured; -import org.springframework.stereotype.Controller; -import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import ru.ip.labs.labs.films.dto.UserDto; -import ru.ip.labs.labs.films.models.UserRole; -import ru.ip.labs.labs.films.service.UserService; - -import java.util.List; -import java.util.stream.IntStream; - -@Controller -@RequestMapping("/users") -public class UserMvcController { - private final UserService userService; - - public UserMvcController(UserService userService) { - this.userService = userService; - } - - @GetMapping - @Secured({UserRole.AsString.ADMIN}) - public String getUsers(@RequestParam(defaultValue = "1") int page, - @RequestParam(defaultValue = "5") int size, - Model model) { - final Page users = userService.findAllPages(page, size) - .map(UserDto::new); - model.addAttribute("users", users); - final int totalPages = users.getTotalPages(); - final List pageNumbers = IntStream.rangeClosed(1, totalPages) - .boxed() - .toList(); - model.addAttribute("pages", pageNumbers); - model.addAttribute("totalPages", totalPages); - return "users"; - } -} diff --git a/src/main/java/ru/ip/labs/labs/films/controller/UserSignupMvcController.java b/src/main/java/ru/ip/labs/labs/films/controller/UserSignupMvcController.java deleted file mode 100644 index bb842b5..0000000 --- a/src/main/java/ru/ip/labs/labs/films/controller/UserSignupMvcController.java +++ /dev/null @@ -1,51 +0,0 @@ -package ru.ip.labs.labs.films.controller; - -import org.springframework.stereotype.Controller; -import org.springframework.ui.Model; -import org.springframework.validation.BindingResult; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import ru.ip.labs.labs.films.models.User; -import ru.ip.labs.labs.films.models.UserSignupDto; -import ru.ip.labs.labs.films.service.UserService; -import ru.ip.labs.labs.films.util.validation.ValidationException; - -import javax.validation.Valid; - -@Controller -@RequestMapping(UserSignupMvcController.SIGNUP_URL) -public class UserSignupMvcController { - public static final String SIGNUP_URL = "/signup"; - - private final UserService userService; - - public UserSignupMvcController(UserService userService) { - this.userService = userService; - } - - @GetMapping - public String showSignupForm(Model model) { - model.addAttribute("userDto", new UserSignupDto()); - return "signup"; - } - - @PostMapping - public String signup(@ModelAttribute("userDto") @Valid UserSignupDto userSignupDto, - BindingResult bindingResult, - Model model) { - if (bindingResult.hasErrors()) { - model.addAttribute("errors", bindingResult.getAllErrors()); - return "signup"; - } - try { - final User user = userService.createUser( - userSignupDto.getLogin(), userSignupDto.getPassword(), userSignupDto.getPasswordConfirm()); - return "redirect:/login?created=" + user.getLogin(); - } catch (ValidationException e) { - model.addAttribute("errors", e.getMessage()); - return "signup"; - } - } -} diff --git a/src/main/java/ru/ip/labs/labs/films/dto/UserDto.java b/src/main/java/ru/ip/labs/labs/films/dto/UserDto.java index 7c20374..27ac8b1 100644 --- a/src/main/java/ru/ip/labs/labs/films/dto/UserDto.java +++ b/src/main/java/ru/ip/labs/labs/films/dto/UserDto.java @@ -1,28 +1,24 @@ package ru.ip.labs.labs.films.dto; -import ru.ip.labs.labs.films.models.User; -import ru.ip.labs.labs.films.models.UserRole; +import javax.validation.constraints.NotEmpty; public class UserDto { - private final long id; - private final String login; - private final UserRole role; + @NotEmpty + private String login; + @NotEmpty + private String password; + private String passwordConfirm; - public UserDto(User user) { - this.id = user.getId(); - this.login = user.getLogin(); - this.role = user.getRole(); - } - - public long getId() { - return id; - } public String getLogin() { return login; } - public UserRole getRole() { - return role; + public String getPassword() { + return password; } + public String getPasswordConfirm() { + return passwordConfirm; + } + } diff --git a/src/main/java/ru/ip/labs/labs/films/dto/UserInfoDTO.java b/src/main/java/ru/ip/labs/labs/films/dto/UserInfoDTO.java new file mode 100644 index 0000000..24b8ad3 --- /dev/null +++ b/src/main/java/ru/ip/labs/labs/films/dto/UserInfoDTO.java @@ -0,0 +1,27 @@ +package ru.ip.labs.labs.films.dto; + + +import ru.ip.labs.labs.films.models.UserRole; + +public class UserInfoDTO { + private final String token; + private String login; + private final UserRole role; + public UserInfoDTO(String token, String login, UserRole role) { + this.token = token; + this.login = login; + this.role = role; + } + + public String getToken() { + return token; + } + + public UserRole getRole() { + + return role; + } + public String getLogin() { + return login; + } +} diff --git a/src/main/java/ru/ip/labs/labs/films/service/UserExistsException.java b/src/main/java/ru/ip/labs/labs/films/service/UserExistsException.java new file mode 100644 index 0000000..503c671 --- /dev/null +++ b/src/main/java/ru/ip/labs/labs/films/service/UserExistsException.java @@ -0,0 +1,7 @@ +package ru.ip.labs.labs.films.service; + +public class UserExistsException extends RuntimeException { + public UserExistsException(String login) { + super(String.format("User '%s' already exists", login)); + } +} diff --git a/src/main/java/ru/ip/labs/labs/films/service/UserNotFoundException.java b/src/main/java/ru/ip/labs/labs/films/service/UserNotFoundException.java new file mode 100644 index 0000000..77f8603 --- /dev/null +++ b/src/main/java/ru/ip/labs/labs/films/service/UserNotFoundException.java @@ -0,0 +1,7 @@ +package ru.ip.labs.labs.films.service; + +public class UserNotFoundException extends RuntimeException { + public UserNotFoundException(String login) { + super(String.format("User not found '%s'", login)); + } +} diff --git a/src/main/java/ru/ip/labs/labs/films/service/UserService.java b/src/main/java/ru/ip/labs/labs/films/service/UserService.java index 247031b..3cce4d8 100644 --- a/src/main/java/ru/ip/labs/labs/films/service/UserService.java +++ b/src/main/java/ru/ip/labs/labs/films/service/UserService.java @@ -8,6 +8,10 @@ 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 ru.ip.labs.labs.configuration.jwt.JwtException; +import ru.ip.labs.labs.configuration.jwt.JwtProvider; +import ru.ip.labs.labs.films.dto.UserDto; +import ru.ip.labs.labs.films.dto.UserInfoDTO; import ru.ip.labs.labs.films.models.User; import ru.ip.labs.labs.films.models.UserRole; import ru.ip.labs.labs.films.repository.UserRepository; @@ -22,13 +26,16 @@ public class UserService implements UserDetailsService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; private final ValidatorUtil validatorUtil; + private final JwtProvider jwtProvider; public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder, - ValidatorUtil validatorUtil) { + ValidatorUtil validatorUtil, + JwtProvider jwtProvider) { this.userRepository = userRepository; this.passwordEncoder = passwordEncoder; this.validatorUtil = validatorUtil; + this.jwtProvider = jwtProvider; } public Page findAllPages(int page, int size) { @@ -45,7 +52,7 @@ public class UserService implements UserDetailsService { public User createUser(String login, String password, String passwordConfirm, UserRole role) { if (findByLogin(login) != null) { - throw new ValidationException(String.format("User '%s' already exists", login)); + throw new UserExistsException(login); } final User user = new User(login, passwordEncoder.encode(password), role); validatorUtil.validate(user); @@ -55,6 +62,26 @@ public class UserService implements UserDetailsService { return userRepository.save(user); } + public String loginAndGetToken(UserDto userDto) { + final User user = findByLogin(userDto.getLogin()); + if (user == null) { + throw new UserNotFoundException(userDto.getLogin()); + } + if (!passwordEncoder.matches(userDto.getPassword(), user.getPassword())) { + throw new UserNotFoundException(user.getLogin()); + } + return jwtProvider.generateToken(user.getLogin()); + } + + public UserDetails loadUserByToken(String token) throws UsernameNotFoundException { + 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); + } + @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { final User userEntity = findByLogin(username); @@ -64,4 +91,9 @@ public class UserService implements UserDetailsService { return new org.springframework.security.core.userdetails.User( userEntity.getLogin(), userEntity.getPassword(), Collections.singleton(userEntity.getRole())); } -} + + public UserInfoDTO signupAndGetToken(UserDto userDto) { + final User user = createUser(userDto.getLogin(), userDto.getPassword(), userDto.getPasswordConfirm(), UserRole.USER); + return new UserInfoDTO(jwtProvider.generateToken(user.getLogin()), user.getLogin(), UserRole.USER); + } +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index da7b0b1..055f74b 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -9,3 +9,5 @@ spring.jpa.hibernate.ddl-auto=update spring.h2.console.enabled=true spring.h2.console.settings.trace=false spring.h2.console.settings.web-allow-others=false +jwt.dev-token=my-secret-jwt +jwt.dev=true