Spring Security启动循环依赖错误及添加@Lazy后403问题排查
1. 彻底解决循环依赖(别只靠@Lazy凑活)
从循环链accountAuthenticationProvider → accountDetailsUserServiceImp → accountSecurityConfig → accountAuthenticationProvider来看,核心是AccountDetailsUserServiceImp直接依赖了AccountSecurityConfig,而Security配置类又引用了依赖UserService的AuthenticationProvider,形成了闭环。
最优修复方式:拆分依赖
大概率你是在UserService里用了SecurityConfig中的某个Bean(比如密码编码器),别直接注入整个配置类,把需要的Bean单独抽出来:
比如原来的UserService依赖SecurityConfig拿PasswordEncoder:
@Service public class AccountDetailsUserServiceImp implements UserDetailsService { private final AccountSecurityConfig securityConfig; public AccountDetailsUserServiceImp(AccountSecurityConfig securityConfig) { this.securityConfig = securityConfig; } }
改成单独配置PasswordEncoder:
@Configuration public class PasswordEncoderConfig { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } } @Service public class AccountDetailsUserServiceImp implements UserDetailsService { private final PasswordEncoder passwordEncoder; public AccountDetailsUserServiceImp(PasswordEncoder passwordEncoder) { this.passwordEncoder = passwordEncoder; } }
这样直接切断循环链,不用依赖@Lazy。
临时兼容方案:精准加@Lazy
如果必须保留现有依赖结构,别给AccountAuthenticationProvider加@Lazy,而是在AccountDetailsUserServiceImp中对AccountSecurityConfig的注入加@Lazy——这样能避免Provider延迟初始化导致的权限逻辑失效。
2. 403 Forbidden 问题排查步骤
加@Lazy后出现403,基本是权限相关Bean没正确初始化,或者配置有坑,按下面的步骤查:
2.1 检查角色前缀坑
Spring Security默认给角色加ROLE_前缀,如果数据库里存的是ADMIN,框架会把它当成ROLE_ADMIN。如果你的权限规则用的是hasRole("ADMIN")(实际匹配ROLE_ADMIN),那没问题;但如果用hasAuthority("ADMIN"),就需要关闭前缀:
@Bean public GrantedAuthorityDefaults grantedAuthorityDefaults() { return new GrantedAuthorityDefaults(""); // 去掉ROLE_前缀 }
或者直接在规则里用hasRole("ADMIN"),对应数据库里的ADMIN(框架自动补前缀)。
2.2 确认UserDetails的角色加载正确
在AccountDetailsUserServiceImp的loadUserByUsername方法里加日志,看看返回的用户有没有带ADMIN角色:
@Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { Account account = accountRepository.findByUsername(username) .orElseThrow(() -> new UsernameNotFoundException("User not found")); // 打印角色,确认是否正确加载 System.out.println("User " + username + " has roles: " + account.getRoles()); return User.builder() .username(account.getUsername()) .password(account.getPassword()) // 确保角色转成GrantedAuthority,用AuthorityUtils更省心 .authorities(AuthorityUtils.createAuthorityList(account.getRoles().toArray(new String[0]))) .build(); }
如果日志里没看到ADMIN,那就是数据库查询或角色映射的问题。
2.3 检查AuthenticationProvider的认证逻辑
确保authenticate方法返回的Authentication对象带了用户权限:
@Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { String username = authentication.getName(); String password = authentication.getCredentials().toString(); UserDetails userDetails = userDetailsService.loadUserByUsername(username); if (!passwordEncoder.matches(password, userDetails.getPassword())) { throw new BadCredentialsException("Wrong password"); } // 必须把userDetails的权限传进去,否则认证后用户没权限 return new UsernamePasswordAuthenticationToken(userDetails, password, userDetails.getAuthorities()); }
如果这里漏了第三个参数(authorities),用户就算登录成功也没权限,直接403。
2.4 核对SecurityFilterChain配置
确保没把登录接口拦截了,权限规则也没写错:
@Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()) // 开发环境先关csrf,省得麻烦 .authorizeHttpRequests(auth -> auth .requestMatchers("/login").permitAll() // 登录接口必须放行 .anyRequest().hasAuthority("ADMIN") // 确保ADMIN能访问所有接口 ) .formLogin(form -> form.permitAll()); // 表单登录的路径也要放行 return http.build(); }
要是登录接口被拦截,用户根本没法完成认证,自然返回403。
内容的提问来源于stack exchange,提问作者Recep Baykan

