我无法将我的 webapp 更新到 Spring Boot 2.6.0(2.5.7 可用,但 2.6.0 不可用)

I can't update my webapp to Spring Boot 2.6.0 (2.5.7 works but 2.6.0 doesn't)

如标题中所述,我无法将我的 webapp 更新到 Spring Boot 2.6.0。我使用 Spring Boot 2.5.5 编写了我的 webapp,一切正常。如果我用这个新标签更新 pom.xml 文件:

<version>2.5.7</version>

我的网络应用程序运行完美。所有测试都有效。 如果我执行此更新,webapp 不会启动:

<version>2.6.0</version>

启动 DEBUG 模式 IDE 向我显示一个错误和 2 个指向我的 webapp 的 2 classes 的链接。

2021-11-23 00:31:45.419 ERROR 21884 --- [  restartedMain] o.s.boot.SpringApplication               : Application run failed

org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'configurazioneSpringSecurity': Requested bean is currently in creation: Is there an unresolvable circular reference?

看来问题出在这个class:

@Configuration
@EnableWebSecurity
public class ConfigurazioneSpringSecurity extends WebSecurityConfigurerAdapter {

    @Autowired
    LivelliDeiRuoli livelliDeiRuoli;

    @Autowired
    GestioneUtentiSpringSecurity gestioneUtentiSpringSecurity;

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

    @Autowired
    public void crittografiaPassword(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(gestioneUtentiSpringSecurity).passwordEncoder(metodoCrittografia());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.csrf().disable();

        http.authorizeRequests().antMatchers(
                "/",
                "/login",
                "/benvenuto",
                "/registrazione",
                "/registrazione-eseguita",
                "/pagine-applicazione"
        ).permitAll();

        http.authorizeRequests().antMatchers("/area-riservata")
                .access("hasAnyRole('" + livelliDeiRuoli.elencoRuoli(1L) + "')");

        http.authorizeRequests().antMatchers("/cambio-password")
                .access("hasAnyRole('" + livelliDeiRuoli.elencoRuoli(1L) + "')");

        http.authorizeRequests().antMatchers("/cambio-nome")
                .access("hasAnyRole('" + livelliDeiRuoli.elencoRuoli(1L) + "')");

        http.authorizeRequests().antMatchers("/cancella-utente")
                .access("isAuthenticated()");

        http.authorizeRequests().antMatchers("/gestione-utenti")
                .access("hasAnyRole('" + livelliDeiRuoli.elencoRuoli(2L) + "')");

        http.authorizeRequests().antMatchers("/gestione-ruoli")
                .access("hasAnyRole('" + livelliDeiRuoli.elencoRuoli(3L) + "')");

        http.authorizeRequests().antMatchers("/pannello-di-controllo")
                .access("hasAnyRole('" + livelliDeiRuoli.elencoRuoli(3L) + "')");

        http.authorizeRequests().and().exceptionHandling().accessDeniedPage("/errore-403");

        http.authorizeRequests().and().formLogin()
                .loginProcessingUrl("/pagina-login")
                .loginPage("/login")
                .defaultSuccessUrl("/")
                .failureUrl("/login?errore=true")
                .usernameParameter("username")
                .passwordParameter("password")
                .and().logout().logoutUrl("/pagina-logout")
                .logoutSuccessUrl("/login?logout=true");

        http.authorizeRequests().and() //
                .rememberMe().tokenRepository(this.persistentTokenRepository()) //
                .tokenValiditySeconds(365 * 24 * 60 * 60);
                
        http.authorizeRequests().antMatchers("/gestione-eventi")
                .access("hasAnyRole('" + livelliDeiRuoli.elencoRuoli(2L) + "')");

        http.authorizeRequests().antMatchers(
                "/cerca-eventi",
                "/ultimi-eventi"
        ).permitAll();

    }

    @Autowired
    private DataSource dataSource;

    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
        JdbcTokenRepositoryImpl db = new JdbcTokenRepositoryImpl();
        db.setDataSource(dataSource);
        return db;
    }

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

}

或在此:

@SpringBootApplication
@Profile("sviluppo")
public class GestioneUtentiApplication extends SpringBootServletInitializer {

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(GestioneUtentiApplication.class);
    }

    public static void main(String[] args) {
        System.setProperty("server.servlet.context-path", "/gestioneutenti");
        SpringApplication.run(GestioneUtentiApplication.class, args);
    }

}

这些 class 有什么问题?

Spring Boot 2.6.0 有什么变化?

GestioneUtentiSpring安全实现 UserDetailsS​​ervice:

@Service
public class GestioneUtentiSpringSecurity implements UserDetailsService {

    @Autowired
    private UtenteRepository utenteRepository;

    @Autowired
    private RuoloRepository ruoloRepository;

    @Autowired
    EseguiVariabiliDiSistema eseguiVariabiliDiSistema;
    
    @Autowired
    LivelliDeiRuoli livelliDeiRuoli;

    @Override
    public UserDetails loadUserByUsername(String nomeUtente) throws UsernameNotFoundException {

        Utente utente = trovaUtenteConPrivilegiDiAutenticazione(nomeUtente);

        if (utente == null) {
            throw new UsernameNotFoundException("L'utente " + nomeUtente + " non è stato trovato nel database.");
        }

        List<String> ruoliUtente = null;
        try {
            ruoliUtente = this.ruoloRepository.trovaRuoliUtente(utente.getId());
        }catch (Exception b){
            ruoliUtente = null;
        }

        List<GrantedAuthority> grantList = null;
        try{
            grantList = new ArrayList<GrantedAuthority>();
            if (ruoliUtente != null) {
                for (String ruolo : ruoliUtente) {
                    GrantedAuthority authority = new SimpleGrantedAuthority(ruolo);
                    grantList.add(authority);
                }
            }
        }catch (Exception c){
            grantList = null;
        }

        UserDetails userDetails = null;
        if((utente != null) && (ruoliUtente != null) && (grantList != null)){
            userDetails = (UserDetails) new User(utente.getNome(), utente.getPassword(), grantList);
        }
        return userDetails;
    }

    public Utente trovaUtenteConPrivilegiDiAutenticazione(String nomeUtente){
        try{
            Utente utente = utenteRepository.trovaUtente(nomeUtente);
            if(livelliDeiRuoli.requisitiUtenteConRuoloMassimo(utente)){
                return utente;
            } else{
                eseguiVariabiliDiSistema.trovaVariabileSenzaVerificaUtente(
                        new VariabileSistema(0L, "login", "")
                );
                if(eseguiVariabiliDiSistema.getVariabileDiSistema().getValore().equals("true")){
                    return utente;
                }else if(eseguiVariabiliDiSistema.getVariabileDiSistema().getValore().equals("false")){
                    return null;
                }else{
                    return null;
                }
            }
        }catch (Exception e){
            return null;
        }
    }

}

从 Spring Boot 2.6 开始,默认禁止循环依赖。您可以通过设置以下 属性:

再次允许循环引用
spring.main.allow-circular-references = true

您可以在 Spring Boot 2.6 Release Notes.

中阅读有关此内容的更多详细信息

我在迁移到 spring 启动 2 的过程中遇到了这个问题。6.x 使用 WebSecurityConfig 代码:

public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

  @Bean 
  public UserDetailsService userDetailsService() {
    return email -> {
          ....
    };
  }

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

根据 WebSecurityConfigurerAdapter#userDetailsServiceBean javadoc:

修复

覆盖此方法以公开从 configure(AuthenticationManagerBuilder) 创建的 UserDetailsService 作为 bean ... 要更改返回的实例,开发人员应更改 userDetailsService() 而不是

public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

  @Bean
  @Override
  public UserDetailsService userDetailsServiceBean() throws Exception {
    return super.userDetailsServiceBean();
  }

  @Override
  public UserDetailsService userDetailsService() {
    return email -> {
          ....
    };
  }

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

Spring这里面临的问题,导致无法从spring boot 2.6 默认配置spring.main.allow-circular-references = false 开始前进的问题位于以下部分

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

    @Autowired
    public void crittografiaPassword(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(gestioneUtentiSpringSecurity).passwordEncoder(metodoCrittografia());
    }

我相信这是因为 WebSecurityConfig extends WebSecurityConfigurerAdapter 在 class.

中有一些与 BCryptPasswordEncoder 结合的循环引用

解决方案是创建另一个配置 class,您可以在其中拆分配置,以便 spring 能够正确创建 bean,避免循环引用。

因此您可以创建以下额外内容class

@Configuration
public class CustomSecurityConfig {
    
   @Bean
   public BCryptPasswordEncoder metodoCrittografia() {
      return new BCryptPasswordEncoder();
   }
}

然后在你原来的ConfigurazioneSpringSecurity.class你可以替换失败

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

    @Autowired
    public void crittografiaPassword(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(gestioneUtentiSpringSecurity).passwordEncoder(metodoCrittografia());
    }

    @Autowired
    private PasswordEncoder passwordEncoder;
 
    @Autowired
    public void crittografiaPassword(AuthenticationManagerBuilder auth) throws Exception {
     
        auth.userDetailsService(gestioneUtentiSpringSecurity)
                            .passwordEncoder(passwordEncoder);
        }

虽然设置 application.properties 有效,但它可能使用了将被弃用的功能。我能够通过使用基于 setter 的注入来解决这个问题。它有点冗长,但对于那些希望保持最新并且不使用可能会被弃用的功能的人来说可能是一个很好的起点。

这当然是一个可以改进的答案,我希望其他人可以提供更简洁的答案。如果我发现任何更清洁的东西,我会更新它。

之前

@Component
public class CustomFilter extends OncePerRequestFilter {

    @Autowired
    private MyUserDetailsService myUserDetailsService;

    @Autowired
    private JWTUtils jwtUtils;

    //When any api will be called this method will be called first and this will extract
    // Token from header pass to JWT Util calls for token details extraction
    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest,
                                    HttpServletResponse httpServletResponse, FilterChain filterChain)
            throws ServletException, IOException {
        //implementation
    }
}

之后

@Component
public class CustomFilter extends OncePerRequestFilter {

    private MyUserDetailsService myUserDetailsService;

    public void setMyUserDetailsService(MyUserDetailsService myUserDetailsService) {
        this.myUserDetailsService = myUserDetailsService;
    }

    public void setJwtUtils(JWTUtils jwtUtils) {
        this.jwtUtils = jwtUtils;
    }

    private JWTUtils jwtUtils;

    //When any api will be called this method will be called first and this will extract
    // Token from header pass to JWT Util calls for token details extraction
    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest,
                                    HttpServletResponse httpServletResponse, FilterChain filterChain)
            throws ServletException, IOException {

       //implementation
    }
}

参考: https://theintuitiveprogrammer.com/post-eight.html