Spring Security Caused by: org.springframework.security.config.annotation.AlreadyBuiltException: This object has already been built

Spring Security Caused by: org.springframework.security.config.annotation.AlreadyBuiltException: This object has already been built

When securing your Spring Boot application, you might encounter errors like “This object has already been built” when using @PreAuthorize on a service layer class that’s injected into a UserDetailsService implementation. This guide will walk you through a common scenario and provide a solution to avoid such issues.

Understanding the Problem

The error stack trace indicates an issue with the springSecurityFilterChain bean and methodSecurityInterceptor bean during application context initialization. The problem typically arises from a circular dependency or misconfiguration in your Spring Security setup.

Example Setup

Security Configuration

Your security configuration might look something like this:

@Configuration
@EnableWebMvcSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MultiHttpSecurityConfig {

    @Autowired
    CustomUserDetailsService customUserDetailsService;

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(customUserDetailsService).passwordEncoder(passwordEncoder());
    }

    @Configuration
    @Order(1)
    public static class ApiWebSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests().antMatchers("/api/sy/users/requestPassword").permitAll();
            http.antMatcher("/api/**").authorizeRequests().anyRequest().authenticated()
                .and().exceptionHandling()
                .authenticationEntryPoint(new RestAuthenticationEntryPoint())
                .accessDeniedHandler(new RestAccessDeniedHandler())
                .and().csrf().disable();
        }
    }

    @Configuration
    public static class FormLoginWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
        @Override
        public void configure(WebSecurity web) throws Exception {
            String[] unsecuredResources = { "/css/**", "/js/**", "/img/**", "/fonts/**" };
            web.ignoring().antMatchers(unsecuredResources);
        }

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            String[] unsecuredUrls = { "login.html", "/login", "/home", "/appPwd.html", "/partials/pwdRequest.html" };
            http.authorizeRequests().antMatchers(unsecuredUrls).permitAll();
            http.authorizeRequests().anyRequest().authenticated()
                .and().formLogin().loginPage("/login").permitAll()
                .successHandler(myAuthenticationSuccessHandler)
                .defaultSuccessUrl("/", true)
                .and().logout().permitAll();
        }
    }

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

Custom User Details Service

You have a custom UserDetailsService:

public interface CustomUserDetailsService extends UserDetailsService {
}

@Service
@Transactional(readOnly = true)
public class CustomUserDetailsServiceImpl implements CustomUserDetailsService {

    @Autowired
    UserMgmt userMgmt;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        MyUserDetails user = userMgmt.findByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("UserName " + username + " not found");
        }
        return user;
    }
}

Service with Method Security

And a service with method-level security:

@Service
public class UserMgmt {

    @Autowired
    UserDetailsRepository userDetailsRepository;

    @PreAuthorize("hasAuthority('ROLE_PI_BAS_CREATE')")
    @Transactional
    public MyUserDetails create(MyUserDetails item) {
        // Create logic
    }
}

Solution

  1. Avoid Circular Dependencies: Ensure there are no circular dependencies. In this case, injecting UserMgmt into CustomUserDetailsServiceImpl might cause such an issue when @PreAuthorize is used.
  2. Separate Concerns: Consider separating the concerns of user management and security configuration.
  3. Use @Lazy: Use @Lazy to break the dependency cycle if necessary.

Modified Setup

Security Configuration
@Configuration
@EnableWebMvcSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MultiHttpSecurityConfig {

    @Autowired
    @Lazy
    CustomUserDetailsService customUserDetailsService;

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(customUserDetailsService).passwordEncoder(passwordEncoder());
    }

    @Configuration
    @Order(1)
    public static class ApiWebSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests().antMatchers("/api/sy/users/requestPassword").permitAll();
            http.antMatcher("/api/**").authorizeRequests().anyRequest().authenticated()
                .and().exceptionHandling()
                .authenticationEntryPoint(new RestAuthenticationEntryPoint())
                .accessDeniedHandler(new RestAccessDeniedHandler())
                .and().csrf().disable();
        }
    }

    @Configuration
    public static class FormLoginWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
        @Override
        public void configure(WebSecurity web) throws Exception {
            String[] unsecuredResources = { "/css/**", "/js/**", "/img/**", "/fonts/**" };
            web.ignoring().antMatchers(unsecuredResources);
        }

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            String[] unsecuredUrls = { "login.html", "/login", "/home", "/appPwd.html", "/partials/pwdRequest.html" };
            http.authorizeRequests().antMatchers(unsecuredUrls).permitAll();
            http.authorizeRequests().anyRequest().authenticated()
                .and().formLogin().loginPage("/login").permitAll()
                .successHandler(myAuthenticationSuccessHandler)
                .defaultSuccessUrl("/", true)
                .and().logout().permitAll();
        }
    }

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
Custom User Details Service
@Service
@Transactional(readOnly = true)
public class CustomUserDetailsServiceImpl implements CustomUserDetailsService {

    @Autowired
    @Lazy
    UserMgmt userMgmt;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        MyUserDetails user = userMgmt.findByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("UserName " + username + " not found");
        }
        return user;
    }
}

Conclusion

By adjusting the injection and initialization order, we can resolve the “This object has already been built” issue. Using @Lazy helps in breaking the circular dependency. This approach ensures that the method security annotations work as expected without causing application context initialization errors.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *