You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot JWT集成Spring Security遇循环Bean依赖问题求助

解决Spring Boot JWT认证中的循环Bean依赖问题

问题根源

从错误日志可以看到循环依赖链:
jwtRequestFilter → jwtUserDetailsService → webSecurityConfig → jwtUserDetailsService
核心原因是WebSecurityConfig既注入了JwtUserDetailsService,又定义了PasswordEncoder Bean,而JwtUserDetailsService依赖这个PasswordEncoder,同时WebSecurityConfig还注入了依赖JwtUserDetailsService的JwtRequestFilter,最终形成闭环。

修复方案

最干净的解决方式是将PasswordEncoder Bean从WebSecurityConfig中拆分到独立的配置类,打破循环依赖。同时替换已过时的WebSecurityConfigurerAdapter为Spring Security 5.7+推荐的组件式配置。

步骤1:创建独立的安全基础Bean配置类

新建SecurityBeansConfig类,专门负责密码编码器、认证管理器这类基础Bean的定义:

@Configuration
public class SecurityBeansConfig {

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

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
        return authConfig.getAuthenticationManager();
    }
}

步骤2:重构WebSecurityConfig(替换过时API)

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig {

    private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
    private final UserDetailsService jwtUserDetailsService;
    private final JwtRequestFilter jwtFilter;
    private final PasswordEncoder passwordEncoder;

    // 用构造注入替代字段注入,符合Spring最佳实践
    public WebSecurityConfig(JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint,
                             UserDetailsService jwtUserDetailsService,
                             JwtRequestFilter jwtFilter,
                             PasswordEncoder passwordEncoder) {
        this.jwtAuthenticationEntryPoint = jwtAuthenticationEntryPoint;
        this.jwtUserDetailsService = jwtUserDetailsService;
        this.jwtFilter = jwtFilter;
        this.passwordEncoder = passwordEncoder;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.csrf(csrf -> csrf.disable())
                .authorizeHttpRequests(auth -> auth
                        .antMatchers("/api/public/**").permitAll()
                        .antMatchers("/api/member/**").hasAnyRole("MEMBER", "ADMIN")
                        .antMatchers("/api/admin/**").hasRole("ADMIN")
                        .anyRequest().authenticated())
                .exceptionHandling(ex -> ex.authenticationEntryPoint(jwtAuthenticationEntryPoint))
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

        http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }

    @Bean
    public AuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(jwtUserDetailsService);
        authProvider.setPasswordEncoder(passwordEncoder);
        return authProvider;
    }
}

步骤3:调整JwtUserDetailsService的依赖注入

根据错误日志,你的JwtUserDetailsService依赖PasswordEncoder,改用构造注入获取独立配置类中的Bean:

@Service
public class JwtUserDetailsService implements UserDetailsService {
    
    private final IAccountRepository accountRepository;
    private final PasswordEncoder passwordEncoder;

    public JwtUserDetailsService(IAccountRepository accountRepository, PasswordEncoder passwordEncoder) {
        this.accountRepository = accountRepository;
        this.passwordEncoder = passwordEncoder;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Account existedAccount = accountRepository.findAccountByUserNameAndAndIsDeletedIsFalse(username);

        if (existedAccount == null) {
            throw new UsernameNotFoundException("用户不存在:" + username);
        }
        return new org.springframework.security.core.userdetails.User(
                existedAccount.getUserName(),
                existedAccount.getPassword(),
                new ArrayList<>());
    }
}

步骤4:优化JwtRequestFilter(修复功能+规范注入)

原有代码遗漏了将认证信息存入SecurityContextHolder的关键步骤,同时改用构造注入:

@Component
public class JwtRequestFilter extends OncePerRequestFilter {

    private final JwtUserDetailsService jwtUserDetailsService;
    private final JwtTokenUtil jwtTokenUtil;

    public JwtRequestFilter(JwtUserDetailsService jwtUserDetailsService, JwtTokenUtil jwtTokenUtil) {
        this.jwtUserDetailsService = jwtUserDetailsService;
        this.jwtTokenUtil = jwtTokenUtil;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain)
            throws ServletException, IOException {
        final String requestTokenHeader = request.getHeader("Authorization");

        String username = null;
        String jwtToken = null;

        if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {
            jwtToken = requestTokenHeader.substring(7);
            try {
                username = jwtTokenUtil.getUsernameFromToken(jwtToken);
            } catch (IllegalArgumentException e) {
                System.out.println("无法获取JWT令牌");
            } catch (ExpiredJwtException e) {
                System.out.println("JWT令牌已过期");
            }
        } else {
            logger.warn("请求令牌未以'Bearer '开头");
        }

        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = this.jwtUserDetailsService.loadUserByUsername(username);

            if (jwtTokenUtil.validateToken(jwtToken, userDetails)) {
                UsernamePasswordAuthenticationToken authToken
                        = new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities());
                authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                // 关键:将认证信息存入上下文,确保后续接口能获取用户身份
                SecurityContextHolder.getContext().setAuthentication(authToken);
            }
        }
        filterChain.doFilter(request, response);
    }
}

关键说明

  • 拆分PasswordEncoder到独立配置类后,WebSecurityConfig和JwtUserDetailsService不再互相依赖,彻底打破循环。
  • 替换过时的WebSecurityConfigurerAdapter为SecurityFilterChain,适配Spring Security最新规范。
  • 构造注入替代字段注入,不仅避免循环依赖风险,也提升代码可测试性和可读性。

内容的提问来源于stack exchange,提问作者Bảo Thiện

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 10:45:12