Spring Security circular bean dependency
Asked Answered
D

9

51

I'm currently working on a Vaadin spring application. According to the app specifications, authentication/authorization of the users must be completed by querying database via jdbcTemplate. How to solve this issue? I'm using Spring Boot 1.4.2.RELEASE.

UPDATE: This approach works with Spring Boot 1.1.x.RELEASE, however on the latest versions of it produces following error message.

Description:
The dependencies of some of the beans in the application context form a cycle:
┌─────┐
|  jdbcAccountRepository defined in file [repositories\JdbcAccountRepository.class]
↑     ↓
|  securityConfiguration.WebSecurityConfig (field services.JdbcUserDetailsServicessecurity.SecurityConfiguration$WebSecurityConfig.userDetailsService)
↑     ↓
|  jdbcUserDetailsServices (field repositories.JdbcAccountRepository services.JdbcUserDetailsServices.repository)
└─────┘

The original code looks like this:

AccountRepository:

public interface AccountRepository {
    void createAccount(Account user) throws UsernameAlreadyInUseException;
    Account findAccountByUsername(String username);
}

JdbcAccountRepository:

@Repository
public class JdbcAccountRepository implements AccountRepository {

    private final Logger LOGGER = LoggerFactory.getLogger(this.getClass());

    private final JdbcTemplate jdbcTemplate;
    private final PasswordEncoder passwordEncoder;

    @Autowired
    public JdbcAccountRepository(JdbcTemplate jdbcTemplate, PasswordEncoder passwordEncoder) {
        this.jdbcTemplate = jdbcTemplate;
        this.passwordEncoder = passwordEncoder;
    }

    @Transactional
    @Override
    public void createAccount(Account user) throws UsernameAlreadyInUseException {
        try {
            jdbcTemplate.update(
                "insert into Account (firstName, lastName, username, password, role) values (?, ?, ?, ?, ?)",
                user.getFirstName(),
                user.getLastName(),
                user.getUsername(),
                passwordEncoder.encode(
                        user.getPassword()),
                        user.getRole()
            );
        } catch (DuplicateKeyException e) {
            throw new UsernameAlreadyInUseException(user.getUsername());
        }
    }

    @Override
    public Account findAccountByUsername(String username) {
        return jdbcTemplate.queryForObject(
            "select username, password, firstName, lastName, role from Account where username = ?",
            (rs, rowNum) -> new Account(
                    rs.getString("username"),
                    rs.getString("password"),
                    rs.getString("firstName"),
                    rs.getString("lastName"),
                    rs.getString("role")),
            username
        );
    }
}

JdbcUserDetailsServices:

@Service
public class JdbcUserDetailsServices implements UserDetailsService {
    private final Logger LOGGER = LoggerFactory.getLogger(this.getClass());

    @Autowired
    JdbcAccountRepository repository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        try {
            Account account = repository.findAccountByUsername(username);
            User user = new User(
                account.getUsername(),
                account.getPassword(),
                AuthorityUtils.createAuthorityList(
                        account.getRole()
                )
            );
            return user;
        } catch (DataAccessException e) {
            LOGGER.debug("Account not found", e);
            throw new UsernameNotFoundException("Username not found.");
        }
    }
}

SecurityConfiguration:

@Configuration
@ComponentScan
public class SecurityConfiguration {

    @Autowired
    ApplicationContext context;

    @Autowired
    VaadinSecurity security;

    @Bean
    public PreAuthorizeSpringViewProviderAccessDelegate preAuthorizeSpringViewProviderAccessDelegate() {
        return new PreAuthorizeSpringViewProviderAccessDelegate(security, context);
    }

    @EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
    public static class GlobalMethodSecurity extends GlobalMethodSecurityConfiguration {

        @Bean
        @Override
        protected AccessDecisionManager accessDecisionManager() {
            return super.accessDecisionManager();
        }
    }

    @Configuration
    @EnableWebSecurity
    public static class WebSecurityConfig extends WebSecurityConfigurerAdapter {

        @Autowired
        JdbcUserDetailsServices userDetailsService;

        @Autowired
        DataSource dataSource;

        @Bean
        public PasswordEncoder passwordEncoder() {
            return NoOpPasswordEncoder.getInstance();
        }

        @Bean
        public TextEncryptor textEncryptor() {
            return Encryptors.noOpText();
        }

        /*
         * (non-Javadoc)
         * @see org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
         * #configure(org.springframework.security.config.annotation.web.builders.WebSecurity)
         */
        @Override
        public void configure(WebSecurity web) throws Exception {
            //Ignoring static resources
            web.ignoring().antMatchers("/VAADIN/**");
        }

        @Override
        protected void configure(AuthenticationManagerBuilder auth)
            throws Exception {
            auth.userDetailsService(userDetailsService);
        }

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

        /*
         * (non-Javadoc)
         * @see org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
         * #configure(org.springframework.security.config.annotation.web.builders.HttpSecurity)
         */
        @Override
        protected void configure(HttpSecurity http) throws Exception {

            http
                .exceptionHandling()
                    .authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/"))
                    .and()
                .authorizeRequests()
                    .antMatchers("/**").permitAll()
                    .and()
                .csrf().disable();
        }
    }
}

P.S If downgrade Spring Boot version to [1.1.5,1.2.0) , this problem will not occur ( due to other dependency, I must to use the latest)

Drafty answered 19/11, 2016 at 17:33 Comment(1)
You are not using the (DataSource dataSource) in your configuration, why inject ??External
W
64

You could replace constructor-based dependency injection with setter-based dependency injection to resolve the cycle, see Spring Framework Reference Documentation:

Circular dependencies

If you use predominantly constructor injection, it is possible to create an unresolvable circular dependency scenario.

For example: Class A requires an instance of class B through constructor injection, and class B requires an instance of class A through constructor injection. If you configure beans for classes A and B to be injected into each other, the Spring IoC container detects this circular reference at runtime, and throws a BeanCurrentlyInCreationException.

One possible solution is to edit the source code of some classes to be configured by setters rather than constructors. Alternatively, avoid constructor injection and use setter injection only. In other words, although it is not recommended, you can configure circular dependencies with setter injection.

Unlike the typical case (with no circular dependencies), a circular dependency between bean A and bean B forces one of the beans to be injected into the other prior to being fully initialized itself (a classic chicken/egg scenario).

Watermark answered 20/11, 2016 at 9:31 Comment(3)
I had the same issue and used setter injection. However, circular dependency problem continued existingUpbeat
I have solved this problem by converting construction injections to field injectionsCoom
In case you are using Lombok, do remove "@AllArgsConstructor". This also enforces constructor injections.Noctambulism
M
43

I prefer the @Lazy method. That way I can stick to one pattern.

See http://www.baeldung.com/circular-dependencies-in-spring

Menam answered 3/8, 2017 at 11:8 Comment(2)
You should mention in your answer, that this solution only works, if it is possible to create a proxy (Java proxy or CGLIB proxy) of the dependent object.Watermark
Fun fact: In the very article you referenced it says at paragraph 5. In Conclusion and I quote 'The preferred method is using setter injections.' But thanks for the link in the first place!Terina
C
27

Your PasswordEncoder bean definition is in WebSecurityConfig which needs JdbcUserDetailsServices. JdbcUserDetailsServices again is dependent on JdbcAccountRepository which needs PasswordEncoder. So the cycle forms. A simple solution is to take out the PasswordEncoder bean definition out of WebSecurityConfig. Even inside SecurityConfiguration class will solve the cyclic problem.

Contribution answered 3/2, 2020 at 9:0 Comment(1)
Cleanest solution in the thread imoDorie
S
9

From Zeeshan answer:

Your PasswordEncoder bean definition is in WebSecurityConfig which needs JdbcUserDetailsServices. JdbcUserDetailsServices again is dependent on JdbcAccountRepository which needs PasswordEncoder. So the cycle forms. A simple solution is to take out the PasswordEncoder bean definition out of WebSecurityConfig. Even inside SecurityConfiguration class will solve the cyclic problem.

Another simple suggestion would be to change PasswordEncoder definition to public static from public only:

@Bean(name = "passwordEncoder")
public PasswordEncoder passwordencoder() {
    return new CustomPasswordEncoder();
}

To:

@Bean(name = "passwordEncoder")
public static PasswordEncoder passwordencoder() {
    return new CustomPasswordEncoder();
}
Succinylsulfathiazole answered 4/1, 2022 at 23:12 Comment(5)
Why does changing the PasswordENcoder to static solve the problem ?Gregoriogregorius
@Gregoriogregorius #22829686. basically a static method doesn't need instantiationColet
@Gregoriogregorius softwareengineering.stackexchange.com/questions/361893/… when you inject something you are saving on instantiation of object and when you declare a method as static it kind of becomes outside of the standard instantiated object & u can call directly calls stattic method in a class by something like SomeClass.StaticMethod(doSomething)Colet
@VH very interesting thanks for answering my question in comment!Gregoriogregorius
@Gregoriogregorius no worries, typically classA has a function that classB is in need of and classA also needs something that classB has so in effect they are both injecting one another and you end up in circular motion. The best solution would be to remove that function of interest in classA to classC and then inject classC in classB as well as continuing to inject classA. classA can also inject classC if it needs the function too. This removes the circular issueColet
S
8

I used @Lazy in the constructor of one class and it solved my problem:

public class AService {  
    private BService b;   
    public ApplicantService(@NonNull @Lazy BService b) {    
        this.b = b;  
    }
}  

public class BService {  
    private AService a;   
    public ApplicantService(@NonNull BService a) {  
        this.a = a;  
    }

}

Slosh answered 24/7, 2020 at 14:15 Comment(0)
Y
2

@Zeeshan Adnan is right. Taking the PasswordEncoder out of WebSecurityConfig solves the Cyclic dependency problem.

Yasukoyataghan answered 5/7, 2021 at 15:36 Comment(0)
C
1

As for Spring boot 3.0.x: as stated before, it can be solved by removing the PasswordEncoder to an external bean (out of your SecurityConfiguration bean).

E.g.

@Configuration
public class PasswordEncoderConfig {

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

And than autowire it wherever needed (e.g. in your SecurityConfig):

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
    ...
    ...
    
    @Autowired
    PasswordEncoder passwordEncoder;
    ...
    ...

}
Changeless answered 29/11, 2023 at 8:43 Comment(0)
L
0

I created a separate config class for PasswordEncoder and then import it in securityconfig class and it resolve my problem. SourceCode here with spring boot latest -> https://github.com/juyelhushen/JWT-springboot-3-Latest.git

Loudmouth answered 13/12, 2022 at 7:24 Comment(0)
M
-1

One of the solution is don't use constructor. For exaple instead of:

private final JdbcTemplate jdbcTemplate;
private final PasswordEncoder passwordEncoder;

@Autowired
public JdbcAccountRepository(JdbcTemplate jdbcTemplate, PasswordEncoder passwordEncoder) {
    this.jdbcTemplate = jdbcTemplate;
    this.passwordEncoder = passwordEncoder;
}

You can use:

@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private PasswordEncoder passwordEncoder;
Mra answered 4/7, 2020 at 19:57 Comment(2)
I am aware of this approach, but for some reason compiler throws out NoBeanException sometimes. Which is why it forces to find a new, better way of defining it.Drafty
field injection is not recommendedWreck

© 2022 - 2024 — McMap. All rights reserved.