From 14ff4b5b0659d672c261773ba0a318ed2d04ec38 Mon Sep 17 00:00:00 2001 From: ScarletRedMan Date: Sun, 17 Mar 2024 17:00:03 +0700 Subject: [PATCH] Enabled Basic HTTP Authentication in Spring Security --- .../impl/exception/NotEnoughPermissions.java | 8 ++ .../api/impl/repository/AuthException.java | 8 ++ .../picker/api/impl/util/RestTemplate.java | 9 ++ .../picker/cp/config/RoomPickerConfig.java | 2 +- .../picker/config/SecurityConfig.java | 35 ++++++- .../ru/dragonestia/picker/model/Account.java | 99 +++++++++++++++++++ .../dragonestia/picker/model/Permission.java | 18 ++++ .../picker/service/AccountService.java | 16 +++ .../service/impl/AccountServiceImpl.java | 58 +++++++++++ 9 files changed, 249 insertions(+), 4 deletions(-) create mode 100644 client-impl/src/main/java/ru/dragonestia/picker/api/impl/exception/NotEnoughPermissions.java create mode 100644 client-impl/src/main/java/ru/dragonestia/picker/api/impl/repository/AuthException.java create mode 100644 server/src/main/java/ru/dragonestia/picker/model/Account.java create mode 100644 server/src/main/java/ru/dragonestia/picker/model/Permission.java create mode 100644 server/src/main/java/ru/dragonestia/picker/service/AccountService.java create mode 100644 server/src/main/java/ru/dragonestia/picker/service/impl/AccountServiceImpl.java diff --git a/client-impl/src/main/java/ru/dragonestia/picker/api/impl/exception/NotEnoughPermissions.java b/client-impl/src/main/java/ru/dragonestia/picker/api/impl/exception/NotEnoughPermissions.java new file mode 100644 index 0000000..70f1639 --- /dev/null +++ b/client-impl/src/main/java/ru/dragonestia/picker/api/impl/exception/NotEnoughPermissions.java @@ -0,0 +1,8 @@ +package ru.dragonestia.picker.api.impl.exception; + +public class NotEnoughPermissions extends RuntimeException { + + public NotEnoughPermissions(String message) { + super(message); + } +} diff --git a/client-impl/src/main/java/ru/dragonestia/picker/api/impl/repository/AuthException.java b/client-impl/src/main/java/ru/dragonestia/picker/api/impl/repository/AuthException.java new file mode 100644 index 0000000..219a0c2 --- /dev/null +++ b/client-impl/src/main/java/ru/dragonestia/picker/api/impl/repository/AuthException.java @@ -0,0 +1,8 @@ +package ru.dragonestia.picker.api.impl.repository; + +public class AuthException extends RuntimeException { + + public AuthException(String message) { + super(message); + } +} diff --git a/client-impl/src/main/java/ru/dragonestia/picker/api/impl/util/RestTemplate.java b/client-impl/src/main/java/ru/dragonestia/picker/api/impl/util/RestTemplate.java index 9e440be..2982a37 100644 --- a/client-impl/src/main/java/ru/dragonestia/picker/api/impl/util/RestTemplate.java +++ b/client-impl/src/main/java/ru/dragonestia/picker/api/impl/util/RestTemplate.java @@ -5,6 +5,8 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import okhttp3.*; import org.jetbrains.annotations.ApiStatus.Internal; +import ru.dragonestia.picker.api.impl.exception.NotEnoughPermissions; +import ru.dragonestia.picker.api.impl.repository.AuthException; import ru.dragonestia.picker.api.exception.ExceptionFactory; import ru.dragonestia.picker.api.impl.RoomPickerClient; import ru.dragonestia.picker.api.impl.util.type.HttpMethod; @@ -110,6 +112,13 @@ public class RestTemplate { var statusCode = code / 100; if (statusCode == 4) { + if (code == 401) { + throw new AuthException("Invalid username and password"); + } + if (code == 403) { + throw new NotEnoughPermissions("Not enough permissions"); + } + var body = new String(Objects.requireNonNull(response.body()).bytes(), StandardCharsets.UTF_8); throw ExceptionFactory.of(json.readValue(body, ErrorResponse.class)); } diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/config/RoomPickerConfig.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/config/RoomPickerConfig.java index bb314ab..81d9913 100644 --- a/control-panel/src/main/java/ru/dragonestia/picker/cp/config/RoomPickerConfig.java +++ b/control-panel/src/main/java/ru/dragonestia/picker/cp/config/RoomPickerConfig.java @@ -20,6 +20,6 @@ public class RoomPickerConfig { @Bean RoomPickerClient roomPickerClient() { - return new RoomPickerClient(serverUrl, "test", "test"); + return new RoomPickerClient(serverUrl, "admin", "qwerty123"); } } diff --git a/server/src/main/java/ru/dragonestia/picker/config/SecurityConfig.java b/server/src/main/java/ru/dragonestia/picker/config/SecurityConfig.java index 3d5b1b8..14a3f3e 100644 --- a/server/src/main/java/ru/dragonestia/picker/config/SecurityConfig.java +++ b/server/src/main/java/ru/dragonestia/picker/config/SecurityConfig.java @@ -3,25 +3,54 @@ package ru.dragonestia.picker.config; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +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.http.SessionCreationPolicy; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; @Configuration @EnableWebSecurity -@EnableMethodSecurity @RequiredArgsConstructor public class SecurityConfig { @Bean - SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + PasswordEncoder passwordEncoder() { + return new PasswordEncoder() { // TODO: use hash algorithm + + @Override + public String encode(CharSequence rawPassword) { + return rawPassword.toString(); + } + + @Override + public boolean matches(CharSequence rawPassword, String encodedPassword) { + return rawPassword.toString().equals(encodedPassword); + } + }; + } + + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http, UserDetailsService userDetailsService) throws Exception { http.csrf(AbstractHttpConfigurer::disable); http.logout(AbstractHttpConfigurer::disable); http.formLogin(AbstractHttpConfigurer::disable); http.sessionManagement(m -> m.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); + http.authorizeHttpRequests(auth -> { + auth + .requestMatchers("/actuator").permitAll() + .requestMatchers("/actuator/**").permitAll() + .requestMatchers("/api-docs-ui").permitAll() + .requestMatchers("/swagger-ui").permitAll() + .requestMatchers("/swagger-ui/**").permitAll() + .requestMatchers("/info").permitAll() + .anyRequest().authenticated(); + }); + http.httpBasic(Customizer.withDefaults()); + http.userDetailsService(userDetailsService); return http.build(); } diff --git a/server/src/main/java/ru/dragonestia/picker/model/Account.java b/server/src/main/java/ru/dragonestia/picker/model/Account.java new file mode 100644 index 0000000..9d06d9a --- /dev/null +++ b/server/src/main/java/ru/dragonestia/picker/model/Account.java @@ -0,0 +1,99 @@ +package ru.dragonestia.picker.model; + +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +public class Account implements UserDetails { + + private final String username; + private final String lowerUsername; + private String password; + private Set permissions = new HashSet<>(); + private boolean locked = false; + private boolean enabled = true; + + public Account(@NotNull String username, @NotNull String password) { + this.username = username; + this.lowerUsername = username.toLowerCase(); + this.password = password; + } + + @Override + public Collection getAuthorities() { + return permissions; + } + + @Contract("_ -> this") + public @NotNull Account setAuthorities(@NotNull Set permissions) { + this.permissions = permissions; + return this; + } + + @Override + public String getPassword() { + return password; + } + + @Contract("_ -> this") + public @NotNull Account setPassword(String value) { + password = value; + return this; + } + + @Override + public @NotNull String getUsername() { + return username; + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return !locked; + } + + @Contract("_ -> this") + public @NotNull Account setLocked(boolean value) { + locked = value; + return this; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return enabled; + } + + @Contract("_ -> this") + public @NotNull Account setEnabled(boolean value) { + enabled = value; + return this; + } + + @Override + public int hashCode() { + return lowerUsername.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (obj == this) return true; + if (obj instanceof Account other) { + return lowerUsername.equals(other.lowerUsername); + } + return false; + } +} diff --git a/server/src/main/java/ru/dragonestia/picker/model/Permission.java b/server/src/main/java/ru/dragonestia/picker/model/Permission.java new file mode 100644 index 0000000..ff82953 --- /dev/null +++ b/server/src/main/java/ru/dragonestia/picker/model/Permission.java @@ -0,0 +1,18 @@ +package ru.dragonestia.picker.model; + +import org.springframework.security.core.GrantedAuthority; + +public enum Permission implements GrantedAuthority { + ADMIN("admin"); + + private final String id; + + Permission(String id) { + this.id = id; + } + + @Override + public String getAuthority() { + return id; + } +} diff --git a/server/src/main/java/ru/dragonestia/picker/service/AccountService.java b/server/src/main/java/ru/dragonestia/picker/service/AccountService.java new file mode 100644 index 0000000..4b9ca90 --- /dev/null +++ b/server/src/main/java/ru/dragonestia/picker/service/AccountService.java @@ -0,0 +1,16 @@ +package ru.dragonestia.picker.service; + +import org.jetbrains.annotations.NotNull; +import org.springframework.security.core.userdetails.UserDetailsService; +import ru.dragonestia.picker.model.Account; + +import java.util.Collection; + +public interface AccountService extends UserDetailsService { + + @NotNull Account createNewAccount(@NotNull String username, @NotNull String password); + + @NotNull Collection allAccounts(); + + void removeAccount(@NotNull Account account); +} diff --git a/server/src/main/java/ru/dragonestia/picker/service/impl/AccountServiceImpl.java b/server/src/main/java/ru/dragonestia/picker/service/impl/AccountServiceImpl.java new file mode 100644 index 0000000..3af48e6 --- /dev/null +++ b/server/src/main/java/ru/dragonestia/picker/service/impl/AccountServiceImpl.java @@ -0,0 +1,58 @@ +package ru.dragonestia.picker.service.impl; + +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import org.jetbrains.annotations.NotNull; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import ru.dragonestia.picker.model.Account; +import ru.dragonestia.picker.model.Permission; +import ru.dragonestia.picker.service.AccountService; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +@Service +@RequiredArgsConstructor +public class AccountServiceImpl implements AccountService { + + private final PasswordEncoder passwordEncoder; + + private final Map accounts = new ConcurrentHashMap<>(); + + @PostConstruct + void init() { + var account = createNewAccount("admin", "qwerty123"); + account.setAuthorities(Set.of(Permission.ADMIN)); + } + + public @NotNull Account createNewAccount(@NotNull String username, @NotNull String password) { + var account = new Account(username, passwordEncoder.encode(password)); + accounts.put(account.getUsername().toLowerCase(), account); + return account; + } + + @Override + public @NotNull Collection allAccounts() { + return accounts.values(); + } + + @Override + public void removeAccount(@NotNull Account account) { + accounts.remove(account.getUsername()); + account.setEnabled(false); + } + + @Override + public Account loadUserByUsername(String username) throws UsernameNotFoundException { + var lowerUsername = username.toLowerCase(); + if (accounts.containsKey(lowerUsername)) { + return accounts.get(lowerUsername); + } + + throw new UsernameNotFoundException("User '" + username + "' does not exists"); + } +}