- Добавил шифрование паролей в БД.

- Добавил валидацию полей ввода.
https://www.youtube.com/watch?v=AdLXmE4rjy4&list=PLU2ftbIeotGpAYRP9Iv2KLIwK36-o_qYk&index=14
This commit is contained in:
Leonid
2025-02-19 09:41:16 +03:00
parent 9a41c4caae
commit 4bf7fb22c6
12 changed files with 161 additions and 33 deletions

View File

@@ -77,6 +77,10 @@
<groupId>org.flywaydb</groupId> <groupId>org.flywaydb</groupId>
<artifactId>flyway-mysql</artifactId> <artifactId>flyway-mysql</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
</dependencies> </dependencies>
<build> <build>

View File

@@ -6,6 +6,8 @@ import org.springframework.security.config.annotation.method.configuration.Enabl
import org.springframework.security.config.annotation.web.builders.HttpSecurity; 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.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.LogoutConfigurer; import org.springframework.security.config.annotation.web.configurers.LogoutConfigurer;
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.SecurityFilterChain;
@Configuration @Configuration
@@ -13,6 +15,10 @@ import org.springframework.security.web.SecurityFilterChain;
@EnableMethodSecurity @EnableMethodSecurity
public class WebSecurityConfig { public class WebSecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(8);
}
@Bean @Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http http

View File

@@ -0,0 +1,19 @@
package net.sytes.kashey.sweater.controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import java.util.Map;
import java.util.stream.Collector;
import java.util.stream.Collectors;
public class ControllerUtils {
static Map<String, String> getErrors(BindingResult bindingResult) {
Collector<FieldError, ?, Map<String, String>> collector = Collectors.toMap(
fieldError -> fieldError.getField() + "Error",
FieldError::getDefaultMessage
);
return bindingResult.getFieldErrors().stream().collect(collector);
}
}

View File

@@ -1,5 +1,6 @@
package net.sytes.kashey.sweater.controller; package net.sytes.kashey.sweater.controller;
import jakarta.validation.Valid;
import net.sytes.kashey.sweater.domain.Message; import net.sytes.kashey.sweater.domain.Message;
import net.sytes.kashey.sweater.repository.MessageRepository; import net.sytes.kashey.sweater.repository.MessageRepository;
import net.sytes.kashey.sweater.security.CustomUserDetails; import net.sytes.kashey.sweater.security.CustomUserDetails;
@@ -7,6 +8,7 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
@@ -14,6 +16,7 @@ import org.springframework.web.multipart.MultipartFile;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.UUID; import java.util.UUID;
@@ -48,12 +51,19 @@ public class MainController {
@PostMapping("/main") @PostMapping("/main")
public String addMessage(@AuthenticationPrincipal CustomUserDetails customUserDetails, public String addMessage(@AuthenticationPrincipal CustomUserDetails customUserDetails,
@RequestParam String text, @Valid Message message,
@RequestParam String tag, BindingResult bindingResult,
@RequestParam("file") MultipartFile file, Model model,
Model model) throws IOException { @RequestParam("file") MultipartFile file
) throws IOException {
Message newMessage = new Message(text, tag, customUserDetails.getUser()); message.setAuthor(customUserDetails.getUser());
if (bindingResult.hasErrors()) {
Map<String, String> errorsMap = ControllerUtils.getErrors(bindingResult);
model.mergeAttributes(errorsMap);
model.addAttribute("message", message);
} else {
if (file != null && !Objects.requireNonNull(file.getOriginalFilename()).isEmpty()) { if (file != null && !Objects.requireNonNull(file.getOriginalFilename()).isEmpty()) {
File uploadDir = new File(uploadPath); // Директория на сервере, куда помещаем пришедший файл File uploadDir = new File(uploadPath); // Директория на сервере, куда помещаем пришедший файл
@@ -63,10 +73,12 @@ public class MainController {
String uuidFile = UUID.randomUUID().toString(); String uuidFile = UUID.randomUUID().toString();
String resultFilename = uuidFile + "." + file.getOriginalFilename(); // случайный уид + имя файла с клиента String resultFilename = uuidFile + "." + file.getOriginalFilename(); // случайный уид + имя файла с клиента
file.transferTo(new File(uploadPath + "/" + resultFilename)); // помещаем переименованный файл с клиента в директорию сервера file.transferTo(new File(uploadPath + "/" + resultFilename)); // помещаем переименованный файл с клиента в директорию сервера
newMessage.setFilename(resultFilename); message.setFilename(resultFilename);
} }
messageRepository.save(newMessage); messageRepository.save(message);
model.addAttribute("message", null);
}
model.addAttribute("messages", messageRepository.findAll()); model.addAttribute("messages", messageRepository.findAll());
model.addAttribute("tag", ""); model.addAttribute("tag", "");
return "main"; return "main";

View File

@@ -1,17 +1,16 @@
package net.sytes.kashey.sweater.controller; package net.sytes.kashey.sweater.controller;
import jakarta.validation.Valid;
import net.sytes.kashey.sweater.domain.User; import net.sytes.kashey.sweater.domain.User;
import net.sytes.kashey.sweater.repository.UserRepository;
import net.sytes.kashey.sweater.service.CustomUserDetailsService; import net.sytes.kashey.sweater.service.CustomUserDetailsService;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import java.util.Map;
@Controller @Controller
public class RegistrationController { public class RegistrationController {
private final CustomUserDetailsService customUserDetailsService; private final CustomUserDetailsService customUserDetailsService;
@@ -27,11 +26,20 @@ public class RegistrationController {
} }
@PostMapping("/registration") @PostMapping("/registration")
public String addUser(@ModelAttribute User user, Map<String, Object> model) { public String addUser(@ModelAttribute @Valid User user, BindingResult bindingResult, Model model) {
if (user.getPassword() != null && !user.getPassword().equals(user.getPassword2())) {
model.addAttribute("passwordError", "Passwords do not match!");
}
if (bindingResult.hasErrors()) {
model.mergeAttributes(ControllerUtils.getErrors(bindingResult));
return "registration";
}
String result = customUserDetailsService.addUser(user); String result = customUserDetailsService.addUser(user);
if (!result.isEmpty()) { if (!result.isEmpty()) {
model.put("message", result); model.addAttribute("usernameError", result);
return "registration"; return "registration";
} else { } else {
return "redirect:/login"; return "redirect:/login";

View File

@@ -1,13 +1,19 @@
package net.sytes.kashey.sweater.domain; package net.sytes.kashey.sweater.domain;
import jakarta.persistence.*; import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank;
import org.hibernate.validator.constraints.Length;
@Entity @Entity
public class Message { public class Message {
@Id @Id
@GeneratedValue(strategy = GenerationType.AUTO) @GeneratedValue(strategy = GenerationType.AUTO)
private Long id; private Long id;
@NotBlank(message = "Please fill in the field.")
@Length(max = 2048, message = "Message too long.")
private String text; private String text;
@Length(max = 255, message = "Tag too long.")
private String tag; private String tag;
private String filename; private String filename;

View File

@@ -1,6 +1,8 @@
package net.sytes.kashey.sweater.domain; package net.sytes.kashey.sweater.domain;
import jakarta.persistence.*; import jakarta.persistence.*;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import java.util.Set; import java.util.Set;
@@ -11,10 +13,18 @@ public class User {
@Id @Id
@GeneratedValue(strategy = GenerationType.AUTO) @GeneratedValue(strategy = GenerationType.AUTO)
private Long id; private Long id;
@NotBlank(message = "Username must not be empty.")
private String username; private String username;
@NotBlank(message = "Password must not be empty.")
private String password; private String password;
@Transient
@NotBlank(message = "Password confirmation must not be empty.")
private String password2;
private boolean enabled; private boolean enabled;
@Email(message = "E-mail is not valid.")
@NotBlank(message = "E-mail must not be empty.")
private String email; private String email;
private String activationCode; private String activationCode;
@@ -90,4 +100,12 @@ public class User {
public void setActivationCode(String activationCode) { public void setActivationCode(String activationCode) {
this.activationCode = activationCode; this.activationCode = activationCode;
} }
public String getPassword2() {
return password2;
}
public void setPassword2(String password2) {
this.password2 = password2;
}
} }

View File

@@ -8,6 +8,7 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils; import org.springframework.util.ObjectUtils;
@@ -19,19 +20,26 @@ public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository; private final UserRepository userRepository;
private final EmailService emailService; private final EmailService emailService;
private final PasswordEncoder passwordEncoder;
@Value("${hosting.path}") @Value("${hosting.path}")
private String hostingPath; private String hostingPath;
public CustomUserDetailsService(UserRepository userRepository, EmailService emailService) { public CustomUserDetailsService(UserRepository userRepository, EmailService emailService,
PasswordEncoder passwordEncoder) {
this.userRepository = userRepository; this.userRepository = userRepository;
this.emailService = emailService; this.emailService = emailService;
this.passwordEncoder = passwordEncoder;
} }
@Override @Override
public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException { public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {
if (name == null) {
throw new UsernameNotFoundException("Username is empty!");
}
User user = userRepository.findByUsername(name) User user = userRepository.findByUsername(name)
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + name)); .orElseThrow(() -> new UsernameNotFoundException("User not found: " + name));
return new CustomUserDetails(user); return new CustomUserDetails(user);
@@ -50,7 +58,7 @@ public class CustomUserDetailsService implements UserDetailsService {
user.setEnabled(true); user.setEnabled(true);
user.setActivationCode(UUID.randomUUID().toString()); user.setActivationCode(UUID.randomUUID().toString());
user.setRoles(Collections.singleton(Role.USER)); user.setRoles(Collections.singleton(Role.USER));
user.setPassword("{noop}" + user.getPassword()); //пока не введено шифрование паролей user.setPassword(passwordEncoder.encode(user.getPassword()));
userRepository.save(user); userRepository.save(user);
sendMessage(user); sendMessage(user);
@@ -114,7 +122,7 @@ public class CustomUserDetailsService implements UserDetailsService {
} }
if (!ObjectUtils.isEmpty(password)) { if (!ObjectUtils.isEmpty(password)) {
user.setPassword("{noop}" + password); user.setPassword(password);
} }
userRepository.save(user); userRepository.save(user);

View File

@@ -1,5 +1,5 @@
insert into usr (id, username, password, enabled) insert into usr (id, username, password, enabled)
values (1, 'admin', '{noop}admin', true); values (1, 'admin', '$2a$12$.XB1cxFOs6F8vgouixcTP.TaY2GXwFspYHID12JpLwotvoYI0JwnC', true);
insert into user_role (user_id, roles) insert into user_role (user_id, roles)
values (1, 'USER'), (1, 'ADMIN'); values (1, 'USER'), (1, 'ADMIN');

View File

@@ -2,8 +2,10 @@
<#import "parts/login.ftl" as l> <#import "parts/login.ftl" as l>
<@com.page> <@com.page>
<#if message??> <#if Session?? && Session.SPRING_SECURITY_LAST_EXCEPTION??>
<div class="mb-3"> ${message} </div> <div class="alert alert-danger" role="alert">
${Session.SPRING_SECURITY_LAST_EXCEPTION.message}
</div>
</#if> </#if>
<@l.login "/login" false/> <@l.login "/login" false/>
</@com.page> </@com.page>

View File

@@ -12,14 +12,26 @@
aria-controls="collapseExample"> aria-controls="collapseExample">
Click to add message Click to add message
</a> </a>
<div class="collapse" id="collapseExample"> <div class="collapse <#if message??>show</#if>" id="collapseExample">
<div class="form-group mt-3"> <div class="form-group mt-3">
<form method="post" enctype="multipart/form-data"> <form method="post" enctype="multipart/form-data">
<div class="form-group"> <div class="form-group">
<input type="text" class="form-control" name="text" placeholder="Text"/> <input type="text" class="form-control ${(textError??)?string('is-invalid', '')}"
value="<#if message??>${message.text}</#if>" name="text" placeholder="Text" />
<#if textError??>
<div class="invalid-feedback">
${textError}
</div>
</#if>
</div> </div>
<div class="form-group"> <div class="form-group">
<input type="text" class="form-control" name="tag" placeholder="Tag"> <input type="text" class="form-control ${(tagError??)?string('is-invalid', '')}"
value="<#if message??>${message.tag}</#if>" name="tag" placeholder="Tag">
<#if tagError??>
<div class="invalid-feedback">
${tagError}
</div>
</#if>
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="custom-file"> <div class="custom-file">

View File

@@ -3,24 +3,57 @@
<div class="row mb-3"> <div class="row mb-3">
<label class="col-sm-2 col-form-label"> User Name:</label> <label class="col-sm-2 col-form-label"> User Name:</label>
<div class="col-sm-6"> <div class="col-sm-6">
<input type="text" class="form-control" name="username" placeholder="User name"/> <input type="text" name="username" value="<#if user??>${user.username}</#if>"
class="form-control ${(usernameError??)?string('is-invalid', '')}"
placeholder="User name"/>
<#if usernameError??>
<div class="invalid-feedback">
${usernameError}
</div>
</#if>
</div> </div>
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<label for="inputPassword3" class="col-sm-2 col-form-label"> Password: </label> <label for="inputPassword3" class="col-sm-2 col-form-label"> Password: </label>
<div class="col-sm-6"> <div class="col-sm-6">
<input type="password" class="form-control" name="password" placeholder="Password"> <input type="password" name="password"
class="form-control ${(passwordError??)?string('is-invalid', '')}"
placeholder="Password"/>
<#if passwordError??>
<div class="invalid-feedback">
${passwordError}
</div>
</#if>
</div> </div>
</div> </div>
<#if isRegistrationForm> <#if isRegistrationForm>
<div class="form-group row">
<label class="col-sm-2 col-form-label">Password:</label>
<div class="col-sm-6">
<input type="password" name="password2"
class="form-control ${(password2Error??)?string('is-invalid', '')}"
placeholder="Confirm password"/>
<#if password2Error??>
<div class="invalid-feedback">
${password2Error}
</div>
</#if>
</div>
</div>
<div class="row mb-3"> <div class="row mb-3">
<label class="col-sm-2 col-form-label"> E-mail:</label> <label class="col-sm-2 col-form-label"> E-mail:</label>
<div class="col-sm-6"> <div class="col-sm-6">
<input type="email" class="form-control" name="email" placeholder="E-mail"/> <input type="email" name="email" value="<#if user??>${user.email}</#if>"
class="form-control ${(emailError??)?string('is-invalid', '')}"
placeholder="E-m@il"/>
<#if emailError??>
<div class="invalid-feedback">
${emailError}
</div>
</#if>
</div> </div>
</div> </div>
</#if> </#if>
<#if !isRegistrationForm> <#if !isRegistrationForm>
<a href="/registration" class="btn btn-primary">Add new user</a> <a href="/registration" class="btn btn-primary">Add new user</a>
</#if> </#if>