【问题标题】:How can I configure a Spring Security AuthenticationManager to be used with a JdbcUserDetailsManager?如何配置 Spring Security AuthenticationManager 以与 JdbcUserDetailsManager 一起使用?
【发布时间】:2020-07-12 10:18:21
【问题描述】:

我有一个使用 Spring Boot 和 Security 的 Web 应用程序,配置为使用带有 JDBC 身份验证的表单登录。

登录和注销工作正常,一般来说,auth 似乎工作正常。

除了一种情况...当我尝试更改密码时,我注意到虽然密码更改本身成功,但我要验证现有密码的 AuthenticationManager...为空!

如何配置 AuthenticationManager(可能使用 DaoAuthenticationProvider 和/或 DaoAuthenticationManager?)以使 AuthenticationManager 不会为空并验证现有密码?

相关配置:

@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,在这里我想要一个故意注入的 UserDetailsManager - 以便能够轻松更改帐户密码:

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();
            }
        }
    }
}

如果我尝试在回答中提到below 的配置,我会得到:

***************************
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

【问题讨论】:

    标签: java spring spring-boot authentication spring-security


    【解决方案1】:

    这个配置似乎工作正常。更改密码时,现有用户由 DaoAuthenticationProvider 进行身份验证,它引用了 AuthController 使用的 JdbcUserDetailsManager

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

    【讨论】:

      【解决方案2】:

      您的设置存在缺陷,因为您太早地启用了 UserDetailsService 的实例化您应该做的事情

      1. 您的configureGlobal 方法应返回void
      2. 覆盖userDetailsServiceBean,调用super 并使用@Bean 对其进行注释。如记录的here
      3. 您应该在您的userDetailsService 配置中设置passwordEncoder,并且在创建用户时不要自己对密码进行编码。
      4. 你的班级也应该有@EnableWebSecurity

      这样 Spring Security 将正确初始化和配置所有组件。 (虽然 3 和 4 不相关,但它们应该设置为一般意义上的正确配置)。

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

      【讨论】:

      • 我在问题中添加了更多信息。
      • 我认为这不是问题所有者想要的,它没有提供 UserDetailsManager 的实例...
      • 它曾经公开过一个,但显然在最近的版本中发生了变化(现在有一些惰性包装器围绕它)。这只暴露了userDetailsService
      【解决方案3】:

      不知道为什么会这样,但你不能只做类似的事情,而不是通过 authmanager:直接使用当前登录的用户(详细信息)。

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

      【讨论】:

      • 这不起作用,因为注入没有发生在 Spring Security 类中。
      猜你喜欢
      • 1970-01-01
      • 2015-02-27
      • 2014-12-06
      • 1970-01-01
      • 1970-01-01
      • 2011-01-20
      • 2015-12-28
      • 2017-08-19
      • 2014-12-11
      相关资源
      最近更新 更多