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
相关产品推荐
相关产品推荐

