Spring Security 循环 bean 依赖

Posted

技术标签:

【中文标题】Spring Security 循环 bean 依赖【英文标题】:Spring Security circular bean dependency 【发布时间】:2017-04-03 09:57:42 【问题描述】:

我目前正在开发一个 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
        );
    

JdbcUserDetailsS​​ervices:

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

P.S 如果将 Spring Boot 版本降级为 [1.1.5,1.2.0) ,则不会出现此问题(由于其他依赖,我必须使用最新的)

【问题讨论】:

你的配置中没有使用(DataSource dataSource),为什么要注入?? 【参考方案1】:

您可以将constructor-based dependency injection 替换为setter-based dependency injection 以解决循环问题,请参阅Spring Framework Reference Documentation:

循环依赖

如果您主要使用构造函数注入,则可能会创建无法解决的循环依赖场景。

例如:A类通过构造函数注入需要B类的实例,B类通过构造函数注入需要A类的实例。如果您为 A 类和 B 类配置 bean 以相互注入,Spring IoC 容器会在运行时检测到此循环引用,并抛出 BeanCurrentlyInCreationException

一种可能的解决方案是编辑某些类的源代码以由设置器而不是构造器配置。或者,避免构造函数注入并仅使用 setter 注入。也就是说,虽然不推荐,但是可以通过setter注入来配置循环依赖。

与典型情况(没有循环依赖)不同,bean A 和 bean B 之间的循环依赖会强制其中一个 bean 在完全初始化之前注入另一个 bean(典型的鸡/蛋场景)。

【讨论】:

我遇到了同样的问题并使用了 setter 注入。然而,循环依赖问题依然存在 我通过将构造注入转换为现场注入解决了这个问题 如果您使用的是 Lombok,请删除“@AllArgsConstructor”。这也强制执行构造函数注入。【参考方案2】:

我更喜欢@Lazy 方法。这样我就可以坚持一种模式。

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

【讨论】:

您应该在回答中提到,此解决方案仅在可以创建依赖对象的代理(Java 代理或 CGLIB 代理)时才有效。 有趣的事实:在您引用的那篇文章中,它在 5. In Conclusion 段中说,我引用了“首选方法是使用 setter 注入”。但是首先感谢您的链接!【参考方案3】:

您的PasswordEncoder bean 定义在WebSecurityConfig 中,需要JdbcUserDetailsServicesJdbcUserDetailsServices 再次依赖于需要PasswordEncoderJdbcAccountRepository。于是形成了循环。一个简单的解决方案是从WebSecurityConfig 中取出PasswordEncoder bean 定义。即使在SecurityConfiguration 类内部也能解决循环问题。

【讨论】:

【参考方案4】:

我在一个类的构造函数中使用了@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;  
    

【讨论】:

【参考方案5】:

@Zeeshan Adnan 是对的。从WebSecurityConfig 中取出PasswordEncoder 解决了循环依赖问题

【讨论】:

【参考方案6】:

来自 Zeeshan 的回答:

您的 PasswordEncoder bean 定义在需要 JdbcUserDetailsS​​ervices 的 WebSecurityConfig 中。 JdbcUserDetailsS​​ervices 再次依赖于需要 PasswordEncoder 的 JdbcAccountRepository。于是形成了循环。一个简单的解决方案是从 WebSecurityConfig 中取出 PasswordEncoder bean 定义。即使在 SecurityConfiguration 类中也能解决循环问题。

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

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

收件人:

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

【讨论】:

【参考方案7】:

其中一个解决方案是不要使用构造函数。例如,而不是:

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。这就是为什么它必须找到一种新的、更好的定义方式。 不推荐现场注入

以上是关于Spring Security 循环 bean 依赖的主要内容,如果未能解决你的问题,请参考以下文章

Spring系列五:Spring怎么解决循环依赖

Spring Security:创建 bean 时出错/未定义 bean

Spring security DefaultMethodSecurityExpressionHandler bean 未注册 Integration Test 的默认 spring security

建议收藏毕设/私活/大佬必备,一个挣钱的标准开源前后端分离springboot+vue+redis+Spring Security脚手架--若依框架

建议收藏毕设/私活/大佬必备,一个挣钱的标准开源前后端分离springboot+vue+redis+Spring Security脚手架--若依框架

Spring Security 动态登录