From 6f5fe27b711b465b1c8d6dac774d9c698460038d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=91=D0=B0=D0=BA=D0=B0?= =?UTF-8?q?=D0=BB=D1=8C=D1=81=D0=BA=D0=B0=D1=8F?= Date: Sat, 11 May 2024 19:14:54 +0400 Subject: [PATCH] =?UTF-8?q?=D0=BF=D0=BE=D0=BC=D0=B5=D0=BD=D1=8F=D0=BB?= =?UTF-8?q?=D0=B0=20=D0=BA=D0=BB=D0=B0=D1=81=D1=81=D1=8B=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=D1=8E=D0=B7=D0=B5=D1=80=D0=B0,=20=D0=B4=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=D0=B2=D0=B8=D0=BB=D0=B0=20=D0=B4=D1=82=D0=BE=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20=D1=80=D0=B5=D0=B3=D0=B8=D1=81=D1=82=D1=80=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D0=B8,=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=D0=B0=20=D1=81=D0=B5=D1=80=D0=B2=D0=B8=D1=81=20(=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D0=B8?= =?UTF-8?q?=20=D1=8E=D0=B7=D0=B5=D1=80=D0=BD=D0=B5=D0=B9=D0=BC=D0=B0=20(?= =?UTF-8?q?=D0=BB=D0=BE=D0=B3=D0=B8=D0=BD=D0=B0=20=D0=B1=D0=BB=D0=B8=D0=BD?= =?UTF-8?q?,=20=D1=8E=D0=B7=D0=B5=D1=80=D0=BD=D0=B5=D0=B9=D0=BC=D0=B0=20?= =?UTF-8?q?=D0=B0=D0=B0=D0=B0=D0=B0=D0=B0=D0=B0=D0=B0=D0=B0=D0=B0=D0=B0))?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/data.mv.db | Bin 69632 -> 61440 bytes .../core/security/SecurityConfiguration.java | 63 +++++++++++++++++ .../backend/core/security/UserPrincipal.java | 64 +++++++++++++++++ .../users/api/UserSignupController.java | 65 ++++++++++++++++++ .../backend/users/api/UserSignupDTO.java | 40 +++++++++++ .../backend/users/model/UserEntity.java | 18 ++--- .../users/repository/UserRepository.java | 2 + .../backend/users/service/UserService.java | 52 ++++++++++++-- 8 files changed, 290 insertions(+), 14 deletions(-) create mode 100644 backend/src/main/java/com/example/backend/core/security/SecurityConfiguration.java create mode 100644 backend/src/main/java/com/example/backend/core/security/UserPrincipal.java create mode 100644 backend/src/main/java/com/example/backend/users/api/UserSignupController.java create mode 100644 backend/src/main/java/com/example/backend/users/api/UserSignupDTO.java diff --git a/backend/data.mv.db b/backend/data.mv.db index 5a4802588e81a689815fc478245263ffa1be4f72..8a8df7012af5e84f99edad242a46d4e457e942b8 100644 GIT binary patch delta 3016 zcmeHJTWl0n7@o7UJ4-LPjaH;pU>3LB9Ln5pAQTeiq2e_mN(|)8nHgwWw%f7=8e%At zczNP-lqDo4f*N|^R@TI5NT@IJqDTz-fDnl$M4o)o7!1a9W@dM1O9}D8H#a%^&CEG- z=0E@Uo$o&*A0|g`vIld(J+ypjt1I|@UJ-5c25rZcp=#=KO!$+yJwq*0Y%M` zgq_~tj-GxphbTEh-rHTYEuq+tY{@kxJh@^$K2zY9++`9hU}kY646}MJ!Ko@H8a4&b zQ#Xz+`P?$h)@yy};@G3hsmKM~OtX zyegjtoa#%8sg}F;K0FKG+&{FGVVH%|Un~Ihvr|lRcus(+8wcCkQe<(RkYQ4X)$K_< zcycM8I62UgrcZUti4fr@Mn5gLjt#Q-$|%oDFpJlA%uAAGLj2aLJ*<8;RZgBh1u~Yy zl0-2~^5v&}j%maf&j`&{Bic5U@9hjmuX$~8dCl2|MCnHWwzi%IK^M&z6r8?()N7;C zN&wpCXhHT>&k=>bo}JzXN(GYf+VgE#nrbhd0N{~DFrIH1BwAHOSLjD*r~vb5*cM&U z$1iRDBoi>E8ltJpru>!uNC4qUnr?>aVv>dZA(Fh%dy*G|48wK*Fgb7u;E{_Biz?cv zW4J0gP4pEc6JSuN?ccIh~r5Xy%wE%IafrR%--1xSrrI?nCebmQah& zd9C;bH$xfC4CRM*cy73r$ofmi0cf?I2QuvYqCZ~yh!d&Ezsq8g=v1uCP^tcO-__#!bD$DW zyc_Dz59KHNRDSDojU>i-{8=UxP#41}#LUuN4EXtaD4>gr^7uRP0^XNh$>n%{_WJ-I z(&pnUN>lms_ev@Dson&U)t%-J^|R2L$jhl^M}kSX11)iDk~*k0E!kT$q{x`u2)zjX z5TO{M6rmiU5}_KQ7NH)Y5uq8O6(J0e)uBV2@M^^XbJVx2JR~Kc2%SE!Qx(vl2aDdu zS{*=G9kU)sLa#S$n~rIjnkouAVV~_Np-tm=#ufmCN5^dP|I3(N8w)*M^6ST%aK5&mS- z6w)-(jD$?VB+ViXl19sy2P~ii6DWZ8ilnKe>7>Uc%?AegkIGysz52Hq4AH3>BARB1 z_wlL)YlDME)uqU`te!gljZ2v*zHw)CDZ`qm`6Y_{(!%Gu+wdBTTNhNRS{(YcI7ktS z?DD>)E40hHKv1=E#q82BiCum?KeQlCmPJ~dr}?Fdr#7@Nqozn~g%}QTh=pv3#3|@K zaSDQ@Uftce?zw01z7vm@4;;Uc`oC7iPNJ0~Xe{~vWv!|dpIEJm9nUpIk{sPNXsu#- GzVRPNLUOJE delta 1306 zcmcJPTSyd99LCS=?9SfYR!m6E*hw^%CY^OJ&JJ0M5ZwrSsmQR-&d#psHhZr%35KkE zQ8Eu?_!LABU2&Z#dhER-#H`G26hd!37L|5pXID2VD5!b)=A1cm{+Hi(j{KICFY8CD z&5#koB9wUNg_`=+kO_vxq|hzK`6lRdLFn^l;Fx{1_|Mi&(&i2a$Am?R_b^cy5)-_@ z9D`}z!}?kL66L)jjShLDhngcl+g(2jXIqJ5wSjHAhunvu;cRtnuu4UFDDumue6g; z`unDg>WmQp>&MN5dW9dGx6P?iQx{Vv|f2ck<>M#Qhfb$ zQ7x!ilQ0pvA8fNT*>vn%JIc!qxU>ccfMK-cof&1O>nt2Js&ih>k8G#YsPpNxjtwH` z42?zEF%T!w;<ry6m(v0Nu z=9)aQ<6=BVsKi8x_@DmX YkaufA)dd%w2Uq8J1=Kre4@uLO9~N|c3IG5A diff --git a/backend/src/main/java/com/example/backend/core/security/SecurityConfiguration.java b/backend/src/main/java/com/example/backend/core/security/SecurityConfiguration.java new file mode 100644 index 0000000..c6ea06f --- /dev/null +++ b/backend/src/main/java/com/example/backend/core/security/SecurityConfiguration.java @@ -0,0 +1,63 @@ +package com.example.backend.core.security; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.Customizer; +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.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; + +import com.example.backend.core.configurations.Constants; +import com.example.backend.users.api.UserSignupController; +import com.example.backend.users.model.UserRole; + +@Configuration +@EnableWebSecurity +public class SecurityConfiguration { + @Bean + SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception { + httpSecurity.headers(headers -> headers.frameOptions(FrameOptionsConfig::sameOrigin)); + httpSecurity.csrf(AbstractHttpConfigurer::disable); + httpSecurity.cors(Customizer.withDefaults()); + + httpSecurity.authorizeHttpRequests(requests -> requests + .requestMatchers("/css/**", "/webjars/**", "/*.svg") + .permitAll()); + + httpSecurity.authorizeHttpRequests(requests -> requests + .requestMatchers(Constants.ADMIN_PREFIX + "/**").hasRole(UserRole.ADMIN.name()) + .requestMatchers("/h2-console/**").hasRole(UserRole.ADMIN.name()) + .requestMatchers(UserSignupController.URL).anonymous() + .requestMatchers(Constants.LOGIN_URL).anonymous() + .anyRequest().authenticated()); + + httpSecurity.formLogin(formLogin -> formLogin + .loginPage(Constants.LOGIN_URL)); + + httpSecurity.rememberMe(rememberMe -> rememberMe.key("uniqueAndSecret")); + + httpSecurity.logout(logout -> logout + .deleteCookies("JSESSIONID")); + + 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(); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/example/backend/core/security/UserPrincipal.java b/backend/src/main/java/com/example/backend/core/security/UserPrincipal.java new file mode 100644 index 0000000..d51cf25 --- /dev/null +++ b/backend/src/main/java/com/example/backend/core/security/UserPrincipal.java @@ -0,0 +1,64 @@ +package com.example.backend.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.example.backend.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/backend/src/main/java/com/example/backend/users/api/UserSignupController.java b/backend/src/main/java/com/example/backend/users/api/UserSignupController.java new file mode 100644 index 0000000..72a0d56 --- /dev/null +++ b/backend/src/main/java/com/example/backend/users/api/UserSignupController.java @@ -0,0 +1,65 @@ +package com.example.backend.users.api; + +import java.util.Objects; + +import org.modelmapper.ModelMapper; +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 com.example.backend.core.configurations.Constants; +import com.example.backend.users.model.UserEntity; +import com.example.backend.users.service.UserService; + +import jakarta.validation.Valid; + +@Controller +@RequestMapping(UserSignupController.URL) +public class UserSignupController { + public static final String URL = "/signup"; + + private static final String SIGNUP_VIEW = "signup"; + private static final String USER_ATTRIBUTE = "user"; + + private final UserService userService; + private final ModelMapper modelMapper; + + public UserSignupController( + UserService userService, + ModelMapper modelMapper) { + this.userService = userService; + this.modelMapper = modelMapper; + } + + private UserEntity toEntity(UserSignupDTO dto) { + return modelMapper.map(dto, UserEntity.class); + } + + @GetMapping + public String getSignup(Model model) { + model.addAttribute(USER_ATTRIBUTE, new UserSignupDTO()); + return SIGNUP_VIEW; + } + + @PostMapping + public String signup( + @ModelAttribute(name = USER_ATTRIBUTE) @Valid UserSignupDTO user, + BindingResult bindingResult, + Model model) { + if (bindingResult.hasErrors()) { + return SIGNUP_VIEW; + } + if (!Objects.equals(user.getPassword(), user.getPasswordConfirm())) { + bindingResult.rejectValue("password", "signup:passwords", "Пароли не совпадают."); + model.addAttribute(USER_ATTRIBUTE, user); + return SIGNUP_VIEW; + } + userService.create(toEntity(user)); + return Constants.REDIRECT_VIEW + Constants.LOGIN_URL + "?signup"; + } + +} diff --git a/backend/src/main/java/com/example/backend/users/api/UserSignupDTO.java b/backend/src/main/java/com/example/backend/users/api/UserSignupDTO.java new file mode 100644 index 0000000..3cdb770 --- /dev/null +++ b/backend/src/main/java/com/example/backend/users/api/UserSignupDTO.java @@ -0,0 +1,40 @@ +package com.example.backend.users.api; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public class UserSignupDTO { + @NotBlank + @Size(min = 3, max = 20) + private String login; + @NotBlank + @Size(min = 3, max = 20) + private String password; + @NotBlank + @Size(min = 3, max = 20) + private String passwordConfirm; + + public String getLogin() { + return login; + } + + public void setLogin(String login) { + this.login = login; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getPasswordConfirm() { + return passwordConfirm; + } + + public void setPasswordConfirm(String passwordConfirm) { + this.passwordConfirm = passwordConfirm; + } +} diff --git a/backend/src/main/java/com/example/backend/users/model/UserEntity.java b/backend/src/main/java/com/example/backend/users/model/UserEntity.java index 54c7a0e..f43a973 100644 --- a/backend/src/main/java/com/example/backend/users/model/UserEntity.java +++ b/backend/src/main/java/com/example/backend/users/model/UserEntity.java @@ -13,7 +13,7 @@ import jakarta.persistence.Table; public class UserEntity extends BaseEntity { @Column(nullable = false, unique = true, length = 15) - private String username; + private String login; @Column(nullable = false, length = 5) private String password; @@ -24,18 +24,18 @@ public class UserEntity extends BaseEntity { } - public UserEntity(Integer id, String username, String password) { - this.username = username; + public UserEntity(Integer id, String login, String password) { + this.login = login; this.password = password; this.role = UserRole.USER; } - public String getUsername() { - return username; + public String getLogin() { + return login; } - public void setUsername(String username) { - this.username = username; + public void setLogin(String login) { + this.login = login; } public String getPassword() { @@ -56,7 +56,7 @@ public class UserEntity extends BaseEntity { @Override public int hashCode() { - return Objects.hash(id, username, password, role); + return Objects.hash(id, login, password, role); } @Override @@ -67,7 +67,7 @@ public class UserEntity extends BaseEntity { return false; final UserEntity other = (UserEntity) obj; return Objects.equals(other.getId(), id) && - Objects.equals(other.getUsername(), username) && + Objects.equals(other.getLogin(), login) && Objects.equals(other.getRole(), role) && Objects.equals(other.getPassword(), password); } diff --git a/backend/src/main/java/com/example/backend/users/repository/UserRepository.java b/backend/src/main/java/com/example/backend/users/repository/UserRepository.java index f873c56..0ecae0e 100644 --- a/backend/src/main/java/com/example/backend/users/repository/UserRepository.java +++ b/backend/src/main/java/com/example/backend/users/repository/UserRepository.java @@ -7,4 +7,6 @@ import com.example.backend.users.model.UserEntity; public interface UserRepository extends CrudRepository { Optional findByUsernameIgnoreCase(String username); + + Optional findByLoginIgnoreCase(String login); } diff --git a/backend/src/main/java/com/example/backend/users/service/UserService.java b/backend/src/main/java/com/example/backend/users/service/UserService.java index ed5d226..b43472c 100644 --- a/backend/src/main/java/com/example/backend/users/service/UserService.java +++ b/backend/src/main/java/com/example/backend/users/service/UserService.java @@ -1,22 +1,41 @@ package com.example.backend.users.service; import java.util.List; +import java.util.Optional; +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 org.springframework.util.StringUtils; import java.util.stream.StreamSupport; +import com.example.backend.core.configurations.Constants; import com.example.backend.core.errors.NotFoundException; +import com.example.backend.core.security.UserPrincipal; import com.example.backend.users.model.UserEntity; +import com.example.backend.users.model.UserRole; import com.example.backend.users.repository.UserRepository; @Service -public class UserService { +public class UserService implements UserDetailsService { private final UserRepository repository; + private final PasswordEncoder passwordEncoder; - public UserService(UserRepository repository) { + public UserService(UserRepository repository, PasswordEncoder passwordEncoder) { this.repository = repository; + this.passwordEncoder = passwordEncoder; + } + + private void checkLogin(Integer id, String login) { + final Optional existsUser = repository.findByLoginIgnoreCase(login); + if (existsUser.isPresent() && !existsUser.get().getId().equals(id)) { + throw new IllegalArgumentException( + String.format("User with login %s is already exists", login)); + } } @Transactional(readOnly = true) @@ -29,18 +48,35 @@ public class UserService { return repository.findById(id).orElseThrow(() -> new NotFoundException(id)); } + @Transactional(readOnly = true) + private UserEntity getByLogin(String username) { + return repository.findByLoginIgnoreCase(username) + .orElseThrow(() -> new IllegalArgumentException("Такого логина нет... хде вы его взяли?")); + } + @Transactional public UserEntity create(UserEntity entity) { + if (entity == null) { + throw new IllegalArgumentException("Entity is null"); + } + checkLogin(null, 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)); + repository.save(entity); return repository.save(entity); } @Transactional public UserEntity update(Integer id, UserEntity entity) { final UserEntity existsentity = get(id); - existsentity.setUsername(entity.getUsername()); + checkLogin(id, entity.getLogin()); + existsentity.setLogin(entity.getLogin()); existsentity.setPassword(entity.getPassword()); - existsentity.setIsAdmin(entity.getIsAdmin()); - return repository.save(existsentity); + existsentity.setRole(entity.getRole()); + repository.save(existsentity); + return existsentity; } @Transactional @@ -50,4 +86,10 @@ public class UserService { return existsentity; } + @Override + @Transactional(readOnly = true) + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + final UserEntity existUser = getByLogin(username); + return new UserPrincipal(existUser); + } }