Spring应用上下文Bean依赖循环问题求助(JWT授权场景)
遇到的问题
给项目加JWT授权时,碰到Spring上下文里的Bean循环依赖:JwtAuthenticationFilter和SecurityConfig互相依赖。试了@Lazy注解和开spring.main.allow-circular-references=true都没用,得找个彻底解决循环的办法。
控制台报错
应用上下文中部分Bean的依赖形成循环:
┌─────┐
| jwtAuthenticationFilter 定义在文件 [C:\Users\User\Desktop\PizzaCloud2\target\classes\pizzas\security\authentication\JwtAuthenticationFilter.class]
↑ ↓
| securityConfig 定义在文件 [C:\Users\User\Desktop\PizzaCloud2\target\classes\pizzas\security\SecurityConfig.class]
└─────┘操作建议:
不建议用循环引用,默认是禁止的。请修改代码移除Bean间的循环依赖。实在不行可以开
spring.main.allow-circular-references=true尝试自动打破循环,但这是下策。
原代码
JwtAuthenticationFilter类
@Component @RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter{ private final JwtService jwtService; private final UserDetailsService userDetailsService; @Override protected void doFilterInternal( @NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException { final String authHeader = request.getHeader("Äuthorization"); // 这里拼写错了,应该是Authorization final String jwt; final String username; if (authHeader == null || !authHeader.startsWith("Bearer")) { filterChain.doFilter(request, response); return; } jwt = authHeader.substring(7); username = jwtService.extractUsername(jwt); if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); if (jwtService.isTokenValid(jwt, userDetails)) { UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( userDetails, userDetails.getAuthorities() ); authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authToken); } } filterChain.doFilter(request, response); } }
SecurityConfig类
@Configuration @EnableWebSecurity @RequiredArgsConstructor public class SecurityConfig { private final JwtAuthenticationFilter jwtAuthFilter; private final AuthenticationProvider authenticationProvider; @Bean public UserDetailsService userDetailsService(UserRepository userRepo) { return username -> { User2 user = userRepo.findByUsername(username); if (user != null) { return user; } throw new UsernameNotFoundException( "User '" + username + "' not found"); }; } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authenticationProvider(authenticationProvider) .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); return http.csrf() .disable() .authorizeHttpRequests() .requestMatchers("/design", "/orders").hasRole("USER") .anyRequest().permitAll() .and() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .formLogin() .loginPage("/login") .and() .logout() .logoutSuccessUrl("/") .and() .headers() .frameOptions() .sameOrigin() .and() .build(); } }
为啥会循环
JwtAuthenticationFilter标了@Component,Spring会提前实例化它,它需要UserDetailsService——这个Bean刚好是在SecurityConfig里定义的。同时SecurityConfig又要注入JwtAuthenticationFilter,就形成了死循环:JwtAuthenticationFilter要UserDetailsService,UserDetailsService属于SecurityConfig,SecurityConfig又要JwtAuthenticationFilter。
解决办法
办法一:把JwtAuthenticationFilter改成在SecurityConfig里用@Bean创建
- 先删掉
JwtAuthenticationFilter上的@Component注解,不让Spring提前实例化它 - 在
SecurityConfig里加个方法创建JwtAuthenticationFilter的Bean,直接传它需要的依赖,这样就不用提前注入整个Filter了
修改后的代码:
JwtAuthenticationFilter类(去掉@Component)
@RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter{ private final JwtService jwtService; private final UserDetailsService userDetailsService; @Override protected void doFilterInternal( @NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException { final String authHeader = request.getHeader("Authorization"); // 修正拼写错误 final String jwt; final String username; if (authHeader == null || !authHeader.startsWith("Bearer")) { filterChain.doFilter(request, response); return; } jwt = authHeader.substring(7); username = jwtService.extractUsername(jwt); if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); if (jwtService.isTokenValid(jwt, userDetails)) { // 修正构造方法:第三个参数是权限,第二个参数传null(因为JWT不需要密码凭证) UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities() ); authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authToken); } } filterChain.doFilter(request, response); } }
SecurityConfig类(新增@Bean方法,移除成员变量注入)
@Configuration @EnableWebSecurity @RequiredArgsConstructor public class SecurityConfig { private final AuthenticationProvider authenticationProvider; @Bean public UserDetailsService userDetailsService(UserRepository userRepo) { return username -> { User2 user = userRepo.findByUsername(username); if (user != null) { return user; } throw new UsernameNotFoundException( "User '" + username + "' not found"); }; } // 新增:创建JwtAuthenticationFilter的Bean @Bean public JwtAuthenticationFilter jwtAuthFilter(JwtService jwtService, UserDetailsService userDetailsService) { return new JwtAuthenticationFilter(jwtService, userDetailsService); } // 直接在方法参数里注入jwtAuthFilter,不用存在成员变量里 @Bean public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthenticationFilter jwtAuthFilter) throws Exception { http .authenticationProvider(authenticationProvider) .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); return http.csrf() .disable() .authorizeHttpRequests() .requestMatchers("/design", "/orders").hasRole("USER") .anyRequest().permitAll() .and() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .formLogin() .loginPage("/login") .and() .logout() .logoutSuccessUrl("/") .and() .headers() .frameOptions() .sameOrigin() .and() .build(); } }
办法二:把UserDetailsService移到单独的配置类
把UserDetailsService的Bean定义从SecurityConfig里抽出来,放到新的配置类比如UserConfig里,这样JwtAuthenticationFilter依赖的UserDetailsService就和SecurityConfig没关系了,循环自然就断了。
新建UserConfig类:
@Configuration @RequiredArgsConstructor public class UserConfig { private final UserRepository userRepo; @Bean public UserDetailsService userDetailsService() { return username -> { User2 user = userRepo.findByUsername(username); if (user != null) { return user; } throw new UsernameNotFoundException( "User '" + username + "' not found"); }; } }
然后SecurityConfig里删掉原来的userDetailsService方法,正常注入JwtAuthenticationFilter就行,因为现在JwtAuthenticationFilter的依赖不依赖SecurityConfig了。
额外提醒
- 原代码里
Authorization拼写错成Äuthorization,这个一定要改,不然拿不到认证头 UsernamePasswordAuthenticationToken的构造方法要传三个参数:用户信息、凭证(JWT场景传null)、权限,原代码少了凭证参数,会导致认证信息不全
内容的提问来源于stack exchange,提问作者leokarelsky

