Enabled Basic HTTP Authentication in Spring Security

This commit is contained in:
Andrey Terentev 2024-03-17 17:00:03 +07:00 committed by Andrey Terentev
parent b8a5f8d717
commit 14ff4b5b06
9 changed files with 249 additions and 4 deletions

View File

@ -0,0 +1,8 @@
package ru.dragonestia.picker.api.impl.exception;
public class NotEnoughPermissions extends RuntimeException {
public NotEnoughPermissions(String message) {
super(message);
}
}

View File

@ -0,0 +1,8 @@
package ru.dragonestia.picker.api.impl.repository;
public class AuthException extends RuntimeException {
public AuthException(String message) {
super(message);
}
}

View File

@ -5,6 +5,8 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import okhttp3.*; import okhttp3.*;
import org.jetbrains.annotations.ApiStatus.Internal; 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.exception.ExceptionFactory;
import ru.dragonestia.picker.api.impl.RoomPickerClient; import ru.dragonestia.picker.api.impl.RoomPickerClient;
import ru.dragonestia.picker.api.impl.util.type.HttpMethod; import ru.dragonestia.picker.api.impl.util.type.HttpMethod;
@ -110,6 +112,13 @@ public class RestTemplate {
var statusCode = code / 100; var statusCode = code / 100;
if (statusCode == 4) { 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); var body = new String(Objects.requireNonNull(response.body()).bytes(), StandardCharsets.UTF_8);
throw ExceptionFactory.of(json.readValue(body, ErrorResponse.class)); throw ExceptionFactory.of(json.readValue(body, ErrorResponse.class));
} }

View File

@ -20,6 +20,6 @@ public class RoomPickerConfig {
@Bean @Bean
RoomPickerClient roomPickerClient() { RoomPickerClient roomPickerClient() {
return new RoomPickerClient(serverUrl, "test", "test"); return new RoomPickerClient(serverUrl, "admin", "qwerty123");
} }
} }

View File

@ -3,25 +3,54 @@ package ru.dragonestia.picker.config;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; 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.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.AbstractHttpConfigurer; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy; 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; import org.springframework.security.web.SecurityFilterChain;
@Configuration @Configuration
@EnableWebSecurity @EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor @RequiredArgsConstructor
public class SecurityConfig { public class SecurityConfig {
@Bean @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.csrf(AbstractHttpConfigurer::disable);
http.logout(AbstractHttpConfigurer::disable); http.logout(AbstractHttpConfigurer::disable);
http.formLogin(AbstractHttpConfigurer::disable); http.formLogin(AbstractHttpConfigurer::disable);
http.sessionManagement(m -> m.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); 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(); return http.build();
} }

View File

@ -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<Permission> 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<Permission> getAuthorities() {
return permissions;
}
@Contract("_ -> this")
public @NotNull Account setAuthorities(@NotNull Set<Permission> 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;
}
}

View File

@ -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;
}
}

View File

@ -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<Account> allAccounts();
void removeAccount(@NotNull Account account);
}

View File

@ -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<String, Account> 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<Account> 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");
}
}