Mam aplikację internetową, która używa Spring Boot and Security skonfigurowaną do używania formularza logowania z uwierzytelnianiem JDBC.

Logowanie i wylogowywanie działają dobrze i ogólnie wydaje się, że uwierzytelnianie działa.

Z wyjątkiem jednego przypadku ... kiedy próbuję zmienić hasło, zauważam, że chociaż sama zmiana hasła się powiodła, AuthenticationManager, który chcę zweryfikować istniejące hasło ... jest zerowy!

enter image description here

Jak mogę skonfigurować AuthenticationManager (być może z DaoAuthenticationProvider i / lub DaoAuthenticationManager?), Aby AuthenticationManager nie miał wartości NULL i zweryfikował istniejące hasło?

Odpowiednia konfiguracja:

@EnableGlobalMethodSecurity(prePostEnabled = true)
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private RESTAuthenticationEntryPoint authenticationEntryPoint;

    @Autowired
    private RESTAuthenticationSuccessHandler authenticationSuccessHandler;

    @Autowired
    private RESTAuthenticationFailureHandler authenticationFailureHandler;

    @Autowired
    private RESTLogoutSuccessHandler restLogoutSuccessHandler;

    @Autowired
    private DataSource dataSource;

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.authorizeRequests().antMatchers("/h2-console/**")
                .permitAll();
        httpSecurity.authorizeRequests().antMatchers("/auth/**").authenticated();
        httpSecurity.cors().configurationSource(corsConfigurationSource());
        httpSecurity.csrf()
                .ignoringAntMatchers("/h2-console/**")
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
        httpSecurity.headers()
                .frameOptions()
                .sameOrigin();
        httpSecurity.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
        httpSecurity.formLogin().successHandler(authenticationSuccessHandler);
        httpSecurity.formLogin().failureHandler(authenticationFailureHandler);
        httpSecurity.logout().logoutSuccessHandler(restLogoutSuccessHandler);
    }

    @Autowired
    @Bean
    public UserDetailsManager configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        JdbcUserDetailsManagerConfigurer jdbcUserDetailsManagerConfigurer = auth.jdbcAuthentication()
                .dataSource(dataSource)
                .withDefaultSchema();

        jdbcUserDetailsManagerConfigurer.withUser("user1")
                .password(passwordEncoder().encode("user1"))
                .roles("USER");

        return jdbcUserDetailsManagerConfigurer.getUserDetailsService();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(12); // Strength increased as per OWASP Password Storage Cheat Sheet
    }

    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList("http://localhost:4200"));
        configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT"));
        configuration.setAllowedHeaders(List.of("X-XSRF-TOKEN", "Content-Type"));
        configuration.setExposedHeaders(List.of("Content-Disposition"));
        configuration.setAllowCredentials(true);
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        source.registerCorsConfiguration("/login", configuration);
        source.registerCorsConfiguration("/logout", configuration);
        return source;
    }
}

AuthController, a tutaj chcę, aby UserDetailsManager został wstrzyknięty celowo - aby móc łatwo zmienić hasło do konta:

import org.adventure.inbound.ChangePasswordData;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.UserDetailsManager;
import org.springframework.web.bind.annotation.*;

import java.security.Principal;

@RestController
@CrossOrigin(origins = "http://localhost:4200")
@RequestMapping("/auth")
public class AuthController {
    private UserDetailsManager userDetailsManager;
    private PasswordEncoder passwordEncoder;

    public AuthController(UserDetailsManager userDetailsManager, PasswordEncoder passwordEncoder) {
        this.userDetailsManager = userDetailsManager;
        this.passwordEncoder = passwordEncoder;
    }

    @PreAuthorize("hasRole('USER')")
    @PutMapping(path = "changePassword", consumes = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<String> changePassword(@RequestBody ChangePasswordData changePasswordData, Principal principal) {
        if (principal == null) {
            return ResponseEntity.badRequest().body("Only logged in users may change their password");
        } else {
            if (changePasswordData.getCurrentPassword() == null || changePasswordData.getNewPassword() == null) {
                return ResponseEntity.badRequest().body("Either of the supplied passwords was null");
            } else {
                String encodedPassword = passwordEncoder.encode(changePasswordData.getNewPassword());
                userDetailsManager.changePassword(
                        changePasswordData.getCurrentPassword(), encodedPassword);
                return ResponseEntity.ok().build();
            }
        }
    }
}

Jeśli spróbuję konfiguracji wymienionej poniżej w odpowiedzi, otrzymam:

***************************
APPLICATION FAILED TO START
***************************

Description:

Parameter 0 of constructor in org.adventure.controllers.AuthController required a  
bean of type 'org.springframework.security.provisioning.UserDetailsManager' that  
could not be found.

The following candidates were found but could not be injected:
    - Bean method 'inMemoryUserDetailsManager' in  
    'UserDetailsServiceAutoConfiguration' not loaded because @ConditionalOnBean  
    (types: org.springframework.security.authentication.AuthenticationManager,  
    org.springframework.security.authentication.AuthenticationProvider,  
    org.springframework.security.core.userdetails.UserDetailsService,  
    org.springframework.security.oauth2.jwt.JwtDecoder  
    org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector;  
    SearchStrategy: all) found beans of type  
    'org.springframework.security.core.userdetails.UserDetailsService' userDetailsServiceBean

0
Monopole Magnet 31 marzec 2020, 21:48

3 odpowiedzi

Najlepsza odpowiedź

Ta konfiguracja wydaje się działać OK. Podczas zmiany haseł istniejący użytkownik jest uwierzytelniany przez dostawcę DaoAuthenticationProvider, który ma odniesienie do JdbcUserDetailsManager używanego przez AuthController

enter image description here enter image description here

@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableWebSecurity
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private RESTAuthenticationEntryPoint authenticationEntryPoint;

    @Autowired
    private RESTAuthenticationSuccessHandler authenticationSuccessHandler;

    @Autowired
    private RESTAuthenticationFailureHandler authenticationFailureHandler;

    @Autowired
    private RESTLogoutSuccessHandler restLogoutSuccessHandler;

    @Autowired
    private DataSource dataSource;

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.authorizeRequests().antMatchers("/h2-console/**")
                .permitAll();
        httpSecurity.authorizeRequests().antMatchers("/auth/**").authenticated();
        httpSecurity.cors().configurationSource(corsConfigurationSource());
        httpSecurity.csrf()
                .ignoringAntMatchers("/h2-console/**")
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
        httpSecurity.headers()
                .frameOptions()
                .sameOrigin();
        httpSecurity.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
        httpSecurity.formLogin().successHandler(authenticationSuccessHandler);
        httpSecurity.formLogin().failureHandler(authenticationFailureHandler);
        httpSecurity.logout().logoutSuccessHandler(restLogoutSuccessHandler);
    }

    @Autowired
    @Bean
    public JdbcUserDetailsManager configureGlobal(AuthenticationManager authenticationManager,
                                                  AuthenticationManagerBuilder auth) throws Exception {
        JdbcUserDetailsManagerConfigurer jdbcUserDetailsManagerConfigurer = auth.jdbcAuthentication()
                .dataSource(dataSource)
                .passwordEncoder(passwordEncoder())
                .withDefaultSchema();

        jdbcUserDetailsManagerConfigurer.withUser("user1")
                .password(passwordEncoder().encode("user1"))
                .roles("USER");

        JdbcUserDetailsManager jdbcUserDetailsManager = jdbcUserDetailsManagerConfigurer.getUserDetailsService();
        jdbcUserDetailsManager.setAuthenticationManager(authenticationManager);

        return jdbcUserDetailsManager;
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(12); // Strength increased as per OWASP Password Storage Cheat Sheet
    }

    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList("http://localhost:4200"));
        configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT"));
        configuration.setAllowedHeaders(List.of("X-XSRF-TOKEN", "Content-Type"));
        configuration.setExposedHeaders(List.of("Content-Disposition"));
        configuration.setAllowCredentials(true);
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        source.registerCorsConfiguration("/login", configuration);
        source.registerCorsConfiguration("/logout", configuration);
        return source;
    }
}
0
Monopole Magnet 3 kwiecień 2020, 06:17

Twoja konfiguracja jest wadliwa, ponieważ włączasz zbyt wczesną instancję UserDetailsService tego, co powinieneś robić

  1. Twoja metoda configureGlobal powinna zwrócić void.
  2. Zastąp userDetailsServiceBean, zadzwoń super i dodaj adnotację @Bean. Zgodnie z dokumentacją tutaj
  3. Powinieneś ustawić passwordEncoder w swojej konfiguracji userDetailsService i nie kodować samodzielnie hasła podczas tworzenia użytkownika.
  4. Powinieneś także mieć na swojej klasie @EnableWebSecurity.

W ten sposób Spring Security poprawnie zainicjuje i skonfiguruje wszystkie komponenty. (Chociaż 3 i 4 nie są ze sobą powiązane, powinny być ustawione na właściwą konfigurację w sensie ogólnym).

@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableWebSecurity
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private RESTAuthenticationEntryPoint authenticationEntryPoint;

    @Autowired
    private RESTAuthenticationSuccessHandler authenticationSuccessHandler;

    @Autowired
    private RESTAuthenticationFailureHandler authenticationFailureHandler;

    @Autowired
    private RESTLogoutSuccessHandler restLogoutSuccessHandler;

    @Autowired
    private DataSource dataSource;

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.authorizeRequests().antMatchers("/h2-console/**")
                .permitAll();
        httpSecurity.authorizeRequests().antMatchers("/auth/**").authenticated();
        httpSecurity.cors().configurationSource(corsConfigurationSource());
        httpSecurity.csrf()
                .ignoringAntMatchers("/h2-console/**")
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
        httpSecurity.headers()
                .frameOptions()
                .sameOrigin();
        httpSecurity.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
        httpSecurity.formLogin().successHandler(authenticationSuccessHandler);
        httpSecurity.formLogin().failureHandler(authenticationFailureHandler);
        httpSecurity.logout().logoutSuccessHandler(restLogoutSuccessHandler);
    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        JdbcUserDetailsManagerConfigurer jdbcUserDetailsManagerConfigurer = auth.jdbcAuthentication()
                .dataSource(dataSource)
                .passwordEncoder(passwordEncoder());
                .withDefaultSchema();

        jdbcUserDetailsManagerConfigurer.withUser("user1")
                .password("user1")
                .roles("USER");

    }

    @Override
    @Bean
    public UserDetailsService userDetailsServiceBean() {
        super.userDetailsServiceBean();
    }

}
0
M. Deinum 1 kwiecień 2020, 06:42

Nie jestem pewien, dlaczego tak jest, ale nie mógłbyś po prostu zrobić czegoś podobnego, zamiast przechodzić przez authmanager: Bezpośrednia praca z aktualnie zalogowanym użytkownikiem (szczegóły).

   @Autowired
    private PasswordEncoder passwordEncoder;

    public void changeUserPassword(@AuthenticationPrincipal UserDetails userDetails // whatever your userdetails implementation is..
                                               String newPassword,
                                               String oldPassword) {

        if (userDetails == null) {
            // user not logged in
        }

        String currentEncryptedPassword = userDetails.getPassword();
        if (!currentEncryptedPassword.equals(passwordEncoder.encode(oldPassword))) {
            // wrong password
        }

        dao.updatepasswd(user, passwordencoder.encode(newsPassword)) //change password in db..
    }
0
Marco Behler 31 marzec 2020, 20:15