- Добавил аутентификацию с помощью Spring Security.

- Реализовал кастомные  CustomUserDetails и CustomUserDetailsService

https://www.youtube.com/watch?v=WDlifgLS8iQ&list=PLU2ftbIeotGoGSEUf54LQH-DgiQPF2XRO&index=3
https://spring.io/guides/gs/securing-web
https://medium.com/@CodeWithTech/spring-security-with-jdbcuserdetailsmanager-and-custom-userdetailsservice-a-complete-guide-248ddce0196c
This commit is contained in:
Leonid
2025-02-06 23:11:24 +03:00
parent 7583c00749
commit 42a1b7466b
17 changed files with 358 additions and 24 deletions

12
.idea/dataSources.xml generated Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="sweater_db@localhost" uuid="df35570d-4519-439c-b57d-a357161e7809">
<driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://localhost:3306/sweater_db</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>

View File

@@ -7,5 +7,5 @@ services:
- 'MYSQL_ROOT_PASSWORD=verysecret'
- 'MYSQL_USER=user'
ports:
- '3306'
- '3306:3306'

View File

@@ -56,6 +56,15 @@
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>6.3.1</version>
</dependency>
</dependencies>
<build>

View File

@@ -0,0 +1,13 @@
package net.sytes.kashey.sweater.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class MvcConfig implements WebMvcConfigurer {
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/login").setViewName("login");
}
}

View File

@@ -0,0 +1,34 @@
package net.sytes.kashey.sweater.config;
import net.sytes.kashey.sweater.service.CustomUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.LogoutConfigurer;
import org.springframework.security.web.SecurityFilterChain;
import javax.sql.DataSource;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((requests) -> requests
.requestMatchers("/", "/registration").permitAll()
.anyRequest().authenticated()
)
.formLogin((form) -> form
.loginPage("/login")
.defaultSuccessUrl("/main", true)
.permitAll()
)
.logout(LogoutConfigurer::permitAll);
return http.build();
}
}

View File

@@ -1,4 +1,4 @@
package net.sytes.kashey.sweater;
package net.sytes.kashey.sweater.controller;
import net.sytes.kashey.sweater.domain.Message;
import net.sytes.kashey.sweater.repository.MessageRepository;
@@ -10,29 +10,27 @@ import org.springframework.web.bind.annotation.RequestParam;
import java.util.Map;
@Controller
public class GreetingController {
public class MainController {
private final MessageRepository messageRepository;
public GreetingController(MessageRepository messageRepository) {
public MainController(MessageRepository messageRepository) {
this.messageRepository = messageRepository;
}
@GetMapping("/greeting")
public String greeting(@RequestParam(name = "name", required = false, defaultValue = "World") String name,
Map<String, Object> model) {
model.put("name", name);
@GetMapping("/")
public String greeting() {
return "greeting";
}
@GetMapping
@GetMapping("/main")
public String findAllMessages(Map<String, Object> model) {
model.put("messages", messageRepository.findAll());
return "main";
}
@PostMapping
@PostMapping("/main")
public String addMessage(@RequestParam String text, @RequestParam String tag, Map<String, Object> model) {
messageRepository.save(new Message(text, tag));

View File

@@ -0,0 +1,48 @@
package net.sytes.kashey.sweater.controller;
import net.sytes.kashey.sweater.domain.Role;
import net.sytes.kashey.sweater.domain.User;
import net.sytes.kashey.sweater.repository.UserRepository;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import java.util.Collections;
import java.util.Map;
@Controller
public class RegistrationController {
private final UserRepository userRepository;
public RegistrationController(UserRepository userRepository) {
this.userRepository = userRepository;
}
@GetMapping("/registration")
public String registration() {
return "registration";
}
@PostMapping("/registration")
public String addUser(@ModelAttribute User user, Map<String, Object> model) {
if (user.getUsername() == null || user.getUsername().isEmpty() || user.getUsername().isBlank()) {
model.put("message", "User name must not be empty");
return "registration";
}
if (userRepository.findByUsername(user.getUsername()).isPresent()) {
model.put("message", "User already exists");
return "registration";
}
user.setEnabled(true);
user.setRoles(Collections.singleton(Role.USER));
user.setPassword("{noop}" + user.getPassword()); //пока не введено шифрование паролей
userRepository.save(user);
return "redirect:/login";
}
}

View File

@@ -0,0 +1,5 @@
package net.sytes.kashey.sweater.domain;
public enum Role {
ADMIN, USER;
}

View File

@@ -0,0 +1,73 @@
package net.sytes.kashey.sweater.domain;
import jakarta.persistence.*;
import java.util.Set;
@Entity
@Table(name = "usr")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String username;
private String password;
private boolean enabled;
@ElementCollection(targetClass = Role.class, fetch = FetchType.EAGER)
@CollectionTable(name = "user_role", joinColumns = @JoinColumn(name = "user_id"))
@Enumerated(EnumType.STRING)
private Set<Role> roles;
public User() {
}
public User(String username) {
this.username = username;
}
public User(String username, String password) {
this.username = username;
this.password = password;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String name) {
this.username = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public Set<Role> getRoles() {
return roles;
}
public void setRoles(Set<Role> roles) {
this.roles = roles;
}
}

View File

@@ -0,0 +1,12 @@
package net.sytes.kashey.sweater.repository;
import net.sytes.kashey.sweater.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String name);
}

View File

@@ -0,0 +1,55 @@
package net.sytes.kashey.sweater.security;
import net.sytes.kashey.sweater.domain.User;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.stream.Collectors;
public class CustomUserDetails implements UserDetails {
private final User user;
public CustomUserDetails(User user) {
this.user = user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority(role.name()))
.collect(Collectors.toSet());
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return user.isEnabled();
}
}

View File

@@ -0,0 +1,28 @@
package net.sytes.kashey.sweater.service;
import net.sytes.kashey.sweater.domain.User;
import net.sytes.kashey.sweater.repository.UserRepository;
import net.sytes.kashey.sweater.security.CustomUserDetails;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
public CustomUserDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {
User user = userRepository.findByUsername(name)
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + name));
return new CustomUserDetails(user);
}
}

View File

@@ -3,4 +3,5 @@ spring.datasource.url=jdbc:mysql://${MYSQL_HOST:localhost}:3306/sweater_db
spring.datasource.username=user
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.mustache.servlet.expose-request-attributes=true

View File

@@ -5,6 +5,7 @@
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<div>Hello {{name}}!!!</div>
<div>Hello there</div>
<a href="/main">Main page</a>
</body>
</html>

View File

@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Spring Security Example </title>
</head>
<body>
Login page
<form action="/login" method="post">
<div><label> User Name : <input type="text" name="username"/> </label></div>
<div><label> Password: <input type="password" name="password"/> </label></div>
<div><input type="submit" value="Sign In"/></div>
<input type="hidden" name="_csrf" value="{{_csrf.token}}" />
</form>
<a href="/registration">Add new user</a>
</body>
</html>

View File

@@ -1,19 +1,29 @@
<html>
<body>
<div>
<form method="post">
<input type="text" name="text" placeholder="Enter message"> </input>
<input type="text" name="tag" placeholder="tag"></input>
<button type="submit">Add message</button>
</form>
</div
<form action="/logout" method="post">
<input type="hidden" name="_csrf" value="{{_csrf.token}}" />
<input type="submit" value="Sign Out"/>
</form>
</div>
<div>
<form method="post" action="filter">
Tag to filer
<br>
<form method="post">
<input type="text" name="text" placeholder="Enter message"> </input>
<input type="text" name="tag" placeholder="tag"></input>
<input type="hidden" name="_csrf" value="{{_csrf.token}}" />
<button type="submit">Add message</button>
</form>
</div>
<div>
<form method="post" action="filter">
Tag to filer
<br>
<input type="text" name="tag">
<input type="hidden" name="_csrf" value="{{_csrf.token}}" />
<button>Find</button>
</form>
</form>
</div>
<div>Messages list</div>
@@ -24,6 +34,5 @@ Tag to filer
<i>{{tag}}</i>
</div>
{{/messages}}
</body>
</html>

View File

@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Spring Security Example </title>
</head>
<body>
Registration page
<br>
{{#message}}
{{message}}
{{/message}}
<form action="/registration" method="post">
<div><label> User Name : <input type="text" name="username"/> </label></div>
<div><label> Password: <input type="password" name="password"/> </label></div>
<div><input type="submit" value="Add user"/></div>
<input type="hidden" name="_csrf" value="{{_csrf.token}}" />
</form>
</body>
</html>