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
- Avoid Circular Dependencies: Ensure there are no circular dependencies. In this case, injecting
UserMgmt
intoCustomUserDetailsServiceImpl
might cause such an issue when@PreAuthorize
is used. - Separate Concerns: Consider separating the concerns of user management and security configuration.
- 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.