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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 13:22:11