Spring Security 循环依赖问题

48

我目前正在开发一个Vaadin Spring应用程序。根据应用程序规格,必须通过查询数据库使用jdbcTemplate完成用户的身份验证/授权。如何解决这个问题?我正在使用Spring Boot 1.4.2.RELEASE。

更新:该方法适用于Spring Boot 1.1.x.RELEASE,但在最新版本中会产生以下错误消息。

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)
└─────┘

原始代码如下:

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.");
        }
    }
}

安全配置:

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

降级Spring Boot版本至[1.1.5,1.2.0)即可解决该问题(由于其他依赖性,我必须使用最新版本)


你在配置中没有使用 (DataSource dataSource),为什么注入它? - Milton Jacomini Neto
9个回答

62
您可以使用基于setter的依赖注入替换基于构造函数的依赖注入来解决循环依赖问题,具体请参见Spring框架参考文档循环依赖 如果您主要使用构造函数注入,就可能会出现无法解决的循环依赖场景。
例如:类A通过构造函数注入需要类B的实例,而类B通过构造函数注入需要类A的实例。如果您将类A和类B的bean配置为互相注入,则Spring IoC容器在运行时检测到该循环引用,并抛出BeanCurrentlyInCreationException异常。
一种可能的解决方法是编辑某些类的源代码以通过setter进行配置。或者避免使用构造函数注入,仅使用setter注入。换句话说,虽然不推荐,但您可以使用setter注入来配置循环依赖关系。
与典型情况(没有循环依赖)不同,bean A和bean B之间的循环依赖关系强制其中一个bean在完全初始化之前被注入到另一个bean中(经典的鸡/蛋场景)。

我曾经遇到过同样的问题,使用了setter注入。然而,循环依赖问题仍然存在。 - valijon
我已经通过将构造注入转换为字段注入来解决了这个问题。 - yunus kula
2
如果您正在使用Lombok,请移除“@AllArgsConstructor”。 这也强制执行构造函数注入。 - Himadri Mandal

42

2
你应该在答案中提到,只有在可以创建所依赖对象的代理(Java代理或CGLIB代理)的情况下,这个解决方案才能起作用。 - dur
有趣的事实:在你引用的那篇文章中,第5段“结论”(https://www.baeldung.com/circular-dependencies-in-spring#Conclusion)中写道:“首选方法是使用setter注入。”但无论如何,感谢你提供的链接! - MyBrainHurts
对我来说很有效,谢谢! - times29

25
您的PasswordEncoder bean定义在WebSecurityConfig中,它需要JdbcUserDetailsServices。而JdbcUserDetailsServices又依赖于JdbcAccountRepository,后者需要PasswordEncoder。因此形成了循环依赖问题。一个简单的解决方案是将PasswordEncoder bean定义移出WebSecurityConfig,即使在SecurityConfiguration类中也能解决循环问题。

在我看来,这个线程中最干净的解决方案 - Alexandre GC

9

来自Zeeshan的回答:

你的PasswordEncoder bean定义在WebSecurityConfig中,需要JdbcUserDetailsServices。而JdbcUserDetailsServices又依赖于JdbcAccountRepository,需要使用PasswordEncoder。这样就形成了一个循环依赖关系。一个简单的解决方案是将PasswordEncoder bean定义移出WebSecurityConfig,即使在SecurityConfiguration类内部也可以解决循环问题。

另一个简单的建议是将PasswordEncoder定义从public改为public static:

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

To:

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

为什么将PasswordENcoder更改为静态可以解决问题? - HelloWorld
1
@HelloWorld https://stackoverflow.com/questions/22829186/could-we-use-static-method-outside-class-in-java#:~:text=If%20that%20is%20a%20public,only%20it%20within%20that%20package. 基本上,静态方法不需要实例化,如果它是公共的,那么可以在类外部使用,但只能在该包内使用。 - V H
1
当你注入某些东西时,你可以节省对象实例化的时间。而当你将一个方法声明为静态时,它就有点像在标准实例化对象之外,并且你可以通过类似 SomeClass.StaticMethod(doSomething) 的方式直接调用类中的静态方法。 - V H
@VH 非常有趣,感谢您在评论中回答我的问题! - HelloWorld
1
@HelloWorld 不用担心,通常 classAa function,而 classB 需要这个函数,同时 classA 也需要 classB 拥有的某些东西,因此它们彼此注入,最终导致循环运动。最好的解决方案是将 classA 中的 function of interest 移到 classC 中,并将 classC 注入到 classB 中,同时继续注入 classA。如果 classA 也需要该 function,则可以将 classC 注入到 classA 中。这样就消除了循环问题。 - V H

7

我在一个类的构造函数中使用了@Lazy,它解决了我的问题:

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

}


2

@Zeeshan Adnan是正确的。将PasswordEncoderWebSecurityConfig中移除解决了循环依赖问题


1
关于Spring Boot 3.0.x:如前所述,可以通过将PasswordEncoder移除到外部bean(不在您的SecurityConfiguration bean中)来解决。
例如:
@Configuration
public class PasswordEncoderConfig {

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

然后在需要的地方(例如在您的SecurityConfig中)进行自动装配。
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
    ...
    ...
    
    @Autowired
    PasswordEncoder passwordEncoder;
    ...
    ...

}

0

-1

其中一种解决方案是不使用构造函数。例如,可以使用以下方式:

private final JdbcTemplate jdbcTemplate;
private final PasswordEncoder passwordEncoder;

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

您可以使用:

@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private PasswordEncoder passwordEncoder;

我知道这种方法,但由于某些原因编译器有时会抛出NoBeanException异常。这就是为什么它迫使我们寻找一种新的、更好的定义方式。 - Malakai
字段注入不被推荐。 - simibac

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接