Kurs Java

Spring Security po nowemu!

Konfiguracja modelu bezpieczeństwa w aplikacjach webowych napisanych w Springu odbywa się z wykorzystaniem projektu Spring Security. W ramach tego projektu dostajemy szereg klas i interfejsów, które wymagają od nas dostarczenia odpowiedniej konfiguracji. Najczęściej tworzymy więc klasę oznaczoną przynajmniej dwiema adnotacjami — @EnableWebSecurity i @Configuration.

Dodatkowo klasa ta dziedziczy zwykle z klasy abstrakcyjnej WebSecurityConfigurerAdapter, w wyniku czego jesteśmy zobowiązani do zaimplementowania metody configure. W ten sposób możemy skonfigurować podstawowe elementy bezpieczeństwa, takie jak obsługa akcji poprawnego i błędnego logowania, czy też uprawnienia do konkretnych ścieżek w aplikacji.

A teraz uwaga...tak to wszystko wyglądało jeszcze do niedawna.

Spring Security 5.7.0-M2

Wersja Spring Security 5.7.0-M2 (pojawiła się w lutym) wprowadza dość istotną modyfikację, która wpłynie na konfigurację security w wielu projektach. Okazuje się, że od tej wersji klasa WebSecurityConfigurerAdapter została opisana adnotacją @Deprecated, co oznacza, że jest przestarzała i planowane jest jej usunięcie w niedalekiej przyszłości. Tym samym razem z nią wylatuje regularnie nadpisywana metoda configure.

Bean na ratunek

Na szczęście równolegle otrzymujemy nowe rozwiązanie, które jest zbliżone do poprzedniego, ale nie wymaga od nas dziedziczenia z predefiniowanej klasy. Od teraz konfiguracja security opiera się na tworzeniu beanów. Zobaczmy, jak to wygląda na przykładzie klasy konfiguracyjnej aplikacji webowej z naszego kursu. W poniższym kodzie klasy SecurityConfig widać najważniejsze elementy z konfiguracją "po nowemu":
@Configuration
@EnableWebSecurity
public class SecurityConfig  /** Do usunięcia: extends WebSecurityConfigurerAdapter */ {

    private CustomAuthSuccessHandler authSuccessHandler;
    private CustomAuthFailureHandler authFailureHandler;
    private CustomLogoutSuccessHandler logoutSuccessHandler;
    private CustomAuthEntryPoint authEntryPoint;

    public SecurityConfig(CustomAuthSuccessHandler authSuccessHandler,
                                CustomAuthFailureHandler authFailureHandler,
                                    CustomLogoutSuccessHandler logoutSuccessHandler,
                                        CustomAuthEntryPoint authEntryPoint...) {
        this.authSuccessHandler = authSuccessHandler;
        this.authFailureHandler = authFailureHandler;
        this.logoutSuccessHandler = logoutSuccessHandler;
        this.authEntryPoint = authEntryPoint;
        ...
    }

//  Do usunięcia:
//
//  @Override
//  protected void configure(HttpSecurity http) throws Exception {
//
//      http.cors().configurationSource(corsConfigurationSource())...
//  }

    @Bean
    protected SecurityFilterChain configure(HttpSecurity http) throws Exception {

        http.cors().configurationSource(corsConfigurationSource())...

        return http.build();
    }

//  Do usunięcia:
//
//  @Autowired
//  public void configAuthentication(AuthenticationManagerBuilder auth) throws Exception {
//      auth.jdbcAuthentication().dataSource(dataSource).passwordEncoder(passwordEncoder())
//				.usersByUsernameQuery("select email, password...")
//				.authoritiesByUsernameQuery(
//						"select u.email, r.role_name from users_roles...");
//  }

    @Bean
    public AuthenticationManager authManager(HttpSecurity http) throws Exception {
        return http.getSharedObject(AuthenticationManagerBuilder.class)
                .jdbcAuthentication()
                .dataSource(dataSource)
                .usersByUsernameQuery("select email, password...")
                .authoritiesByUsernameQuery("select u.email, r.role_name from users_roles...")
                .passwordEncoder(passwordEncoder()).and().build();
    }

    public PasswordEncoder passwordEncoder() {
        ...
    }

    private CorsConfigurationSource corsConfigurationSource() {
        ...
    }

    private Filter csrfHeaderFilter() {
        ...
    }

    private CsrfTokenRepository csrfTokenRepository() {
        ...
    }
}
Widać tutaj dokładnie, że zmianie ulegają tylko trzy elementy. Przede wszystkim rzuca się w oczy usunięcie rozszerzenia WebSecurityConfigurerAdapter. Następnie widać, że przenosimy zawartość nadpisywanej metody configure do nowej metody, z tą różnicą, że na końcu zwracamy budowany obiekt typu SecurityFilterChain. Zamiast wstrzykiwania AuthenticationManagerBuilder do metody configAuthentication wstrzykujemy HttpSecurity i to ten obiekt udostępnia AuthenticationManagerBuilder, na bazie którego budujemy obiekt interfejsu AuthenticationManager. Wszystkie konfiguracje (np. data source) tworzymy jak wcześniej.

Co z UserDetailsService?

W powyższym przykładzie dane podane w trakcie logowania weryfikujemy z tymi zapisanymi w bazie poprzez jdbc. Co jednak gdybyśmy chcieli użyć tutaj klasy UserDetailsService w celu pobrania danych przy pomocy Spring Data JPA?
@Service("userDetailsService")
public class CustomUserDetailsService implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(final String email) throws UsernameNotFoundException {

        try {
            final User user = userRepository.findByEmail(email);

            ...

            return MyUserDetails.build(user);
        } catch (final Exception e) {
            throw new RuntimeException(e);
        }
    }
}
Załóżmy, że do tego chcemy również użyć własnego providera uwierzytelnienia:
public class CustomAuthenticationProvider extends DaoAuthenticationProvider {

    private UserRepository userRepository;

    @Override
    public Authentication authenticate(Authentication auth) {
        final User user = userRepository.findByEmail(auth.getName());

        ...

        return new UsernamePasswordAuthenticationToken(user,
                                    result.getCredentials(), result.getAuthorities());
    }

    @Override
    public boolean supports(Class authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }
}
Tak zbudowana konfiguracja jest również dosyć często spotykana w projektach, między innymi w naszym security-app. W takich projektach zamiast poniższej konfiguracji w SecurityConfig:
protected void configure(final AuthenticationManagerBuilder auth) {
    auth.authenticationProvider(authProvider());
}

public DaoAuthenticationProvider authProvider() {
    final CustomAuthenticationProvider authProvider = new CustomAuthenticationProvider();
    authProvider.setUserDetailsService(userDetailsService);
    authProvider.setPasswordEncoder(passwordEncoder());
    return authProvider;
}
wystarczy napisać metodę tworzącą bean-a authManager w następujący sposób:
@Bean
public AuthenticationManager authManager(HttpSecurity http) throws Exception {
    final CustomAuthenticationProvider authProvider = new CustomAuthenticationProvider();
    authProvider.setUserDetailsService(userDetailsService);
    authProvider.setPasswordEncoder(passwordEncoder());
    return http.getSharedObject(AuthenticationManagerBuilder.class)
                .authenticationProvider(authProvider).build();
}
Jak widać, tworzymy tutaj obiekt własnego providera i ustawiamy w nim wymagane zależności, wśród których znajduje się userDetailsService wstrzyknięty wcześniej do klasy SecurityConfig. Finalnie całego providera wrzucamy do AuthenticationManagerBuilder pobranego bezpośrednio z obiektu security.
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private CustomAuthSuccessHandler authSuccessHandler;
    private CustomAuthFailureHandler authFailureHandler;
    private CustomLogoutSuccessHandler logoutSuccessHandler;
    private CustomAuthEntryPoint authEntryPoint;
    private final UserDetailsService userDetailsService;

    public SecurityConfig(CustomAuthSuccessHandler authSuccessHandler,
                                CustomAuthFailureHandler authFailureHandler,
                                    CustomLogoutSuccessHandler logoutSuccessHandler,
                                        UserDetailsService userDetailsService) {
        this.authSuccessHandler = authSuccessHandler;
        this.authFailureHandler = authFailureHandler;
        this.logoutSuccessHandler = logoutSuccessHandler;
        this.userDetailsService = userDetailsService;
        ...
    }

    ...

    @Bean
    public AuthenticationManager authManager(HttpSecurity http) throws Exception {
        final CustomAuthenticationProvider authProvider = new CustomAuthenticationProvider();
        authProvider.setUserDetailsService(userDetailsService);
        authProvider.setPasswordEncoder(passwordEncoder());
        return http.getSharedObject(AuthenticationManagerBuilder.class)
                    .authenticationProvider(authProvider).build();
    }

    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return (web) -> web.ignoring().antMatchers("/resources/**");
    }
}
Dołącz do grupy, w której znajdziesz ciekawe posty oraz poznasz odpowiedzi na swoje pytania!
Grupa Portalu Javappa
  • Regularnie publikowane posty dotyczące, Springa i Hibernate'a oraz samej Javy.
  • Możliwość zadawania pytań osobom tworzącym społeczność budowaną wokół tych samych zainteresowań
  • Bezpośredni kontakt z autorem portalu i kursów Javappa!
  • Wymiana doświadczeń między członkami grupy
  • Przyjazna atmosfera w zamkniętej grupie
Appa Notka. Zajrzyj do naszego Kursu Aplikacji Web - Mega pakiet. Pierwszy w polskim internecie tak rozbudowany kurs tworzenia aplikacji webowej w Springu na podstawie gotowej aplikacji, którą ściągniesz na swój komputer. Implementacja krok po kroku, drogowskazy do miejsc, w których uzupełnisz wiedzę teoretyczną, a także inne pomocne rozwiązania.

Podsumowanie

Tym samym zarówno w przypadku prostej konfiguracji opartej o uwierzytelnienie zapytaniem jdbc, jak i w przypadku konfiguracji powiązanej z dodatkowym serwisem UserDetailsService, możemy łatwo wdrożyć nowe rozwiązania i nie musimy przejmować się tym, że klasa WebSecurityConfigurerAdapter została przeznaczona do usunięcia.
Autor: Jarek Klimas
Data: 08 października 2022
Labele:Backend, Poziom średniozaawansowany, Java
Masz swoje przemyślenia na temat artykułu? Podziel się nimi!
Masz pytanie odnośnie zagadnienia omawianego w artykule?
Coś, co napisaliśmy, nie zaspokoiło Twojego głodu wiedzy?
Daj nam znać co myślisz i skomentuj artykuł na facebooku!

Stale się rozwijamy, a więc bądź na bieżąco!
Na ten adres będziemy przesyłać informacje o ważniejszych aktualizacjach, a także o nowych materiałach pojawiających się na stronie.
Polub nas na Facebooku:
Nasi partnerzy: stackshare
Javappa to również profesjonalne usługi programistyczne oparte o technologie JAVA. Jeśli chesz nawiązać z nami kontakt w celu uzyskania doradztwa bądź stworzenia aplikacji webowej powinieneś poznać nasze doświadczenia.
Kliknij O nas .


Pozycjonowanie stron: Grupa TENSE