security6.0循环嵌套

**security6.0登录出现循环嵌套抛出异常Handler dispatch failed: java.lang.StackOverflowError

错误代码位置

 Authentication   authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));

起因:从SpringBoot2.7迁移SpringBoot3.0,同步升级security到6.0版本。
升级后因WebSecurityConfigurerAdapter弃用,重写了SecurityConfig类,现在启动正常,第三方免密登陆正常,只有login接口登录出现问题。

img

代码如下
配置类


@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private AuthenticationEntryPointImpl authenticationEntryPoint;
    /**
     * 退出处理类
     */
    @Autowired
    private LogoutSuccessHandlerImpl logoutSuccessHandler;
    
    @Autowired
    private AuthenticationProvider authenticationProvider;
    /**
     * token认证过滤器
     */
    @Autowired
    private JwtAuthenticationTokenFilter authenticationTokenFilter;


    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            // 禁用basic明文验证
            .httpBasic().disable()
            // 前后端分离架构不需要csrf保护
            .csrf().disable()
            // 禁用默认登录页
            .formLogin().disable()
            // 禁用默认登出页
            .logout().disable()
            // 设置异常的EntryPoint,如果不设置,默认使用Http403ForbiddenEntryPoint
            .exceptionHandling(exceptions -> exceptions.authenticationEntryPoint(authenticationEntryPoint))
            // 前后端分离是无状态的,不需要session了,直接禁用。
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(authorizeHttpRequests -> authorizeHttpRequests
                // 允许所有OPTIONS请求
                .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                .requestMatchers(HttpMethod.POST, "/refresh_token").permitAll()
                .requestMatchers("/refresh_token1").permitAll()
                // 过滤请求
                // 对于登录login 注册register 验证码captchaImage 允许匿名访问
                .requestMatchers("/login").anonymous()
                .requestMatchers("/captchaImage").anonymous()
                .requestMatchers("/register").anonymous()
                //aj_report 图片下载使用..
                // 其他所有接口必须有Authority信息,Authority在登录成功后的UserDetailsImpl对象中默认设置“ROLE_USER”
                //.requestMatchers("/**").hasAnyAuthority("ROLE_USER")
                // 允许任意请求被已登录用户访问,不检查Authority
                .anyRequest().authenticated())
            .authenticationProvider(authenticationProvider)
            // 加我们自定义的过滤器,替代UsernamePasswordAuthenticationFilter
            .addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
            .logout(lo -> lo.logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler));
        return http.build();
    }


    /**
     * @return 用户详细信息  -> jwt身份验证过滤器
     */
    @Bean
    public UserDetailsService userDetailsService() {
        return username -> userDetailsService.loadUserByUsername(username);
    }

    /**
     * TODO 四 4.2
     *
     * @return 身份校验机制、身份验证提供程序
     */
    @Bean
    public AuthenticationProvider authenticationProvider() {
        // 创建一个用户认证提供者
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        // 设置用户相信信息,可以从数据库中读取、或者缓存、或者配置文件
        authProvider.setUserDetailsService(userDetailsService);
        // 设置加密机制,若想要尝试对用户进行身份验证,我们需要知道使用的是什么编码
        authProvider.setPasswordEncoder(passwordEncoder());
        return authProvider;
    }


    /**
     * TODO 四 4.3提供编码机制
     *
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * TODO 四 4.4 基于用户名和密码或使用用户名和密码进行身份验证
     *
     * @param config
     *
     * @return
     *
     * @throws Exception
     */
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }

}


/**
* 用户验证处理
*
* @author able
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
   private static final Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class);

   @Autowired
   private ISysUserService sysUserService;

   @Autowired
   private SysPermissionService permissionService;

   @Override
   public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
       SysUser user = sysUserService.selectUserByUserName(username);
       if(StringUtils.isNull(user)) {
           log.info("登录用户:{} 不存在.", username);
           throw new ServiceException("登录用户:" + username + " 不存在");
       } else if(UserStatus.DELETED.getCode().equals(user.getDelFlag())) {
           log.info("登录用户:{} 已被删除.", username);
           throw new ServiceException("对不起,您的账号:" + username + " 已被删除");
       } else if(UserStatus.DISABLE.getCode().equals(user.getStatus())) {
           log.info("登录用户:{} 已被停用.", username);
           throw new ServiceException("对不起,您的账号:" + username + " 已停用");
       }

       return createLoginUser(user);
   }

   public UserDetails createLoginUser(SysUser user) {
       return new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user));
   }
}
```java

/**
* 自定义退出处理类 返回成功
*
* @author able
*/
@Configuration
public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler
{
   @Autowired
   private TokenService tokenService;

   /**
    * 退出处理
    *
    * @return
    */
   @Override
   public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
           throws IOException, ServletException
   {
       LoginUser loginUser = tokenService.getLoginUser(request);
       if (StringUtils.isNotNull(loginUser))
       {
           String userName = loginUser.getUsername();
           // 删除用户缓存记录
           tokenService.delLoginUser(loginUser.getToken());
           // 记录用户退出日志
           AsyncManager.me().execute(AsyncFactory.recordLogininfor(userName, Constants.LOGOUT, "退出成功"));
       }
       ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(HttpStatus.SUCCESS, "退出成功")));
   }
}

img

下面是冲突开始的地方 ,我删掉了大部分业务代码,只保留了出错问题的代码
执行这段代码的时候出的问题,能跟跟踪到的是调用authenticate出错误的。调用该方法没有进入实现类,而是进入了JdkDynamicAopProxy然后就开始循环调用抛出异常
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));

@Component
public class SysLoginService {
 @Autowired
    private org.springframework.security.authentication.AuthenticationManager authenticationManager;

/**
     * 登录验证
     *
     * @param username 用户名
     * @param password 密码
     * @param code     验证码
     * @param uuid     唯一标识
     *
     * @return 结果
     */
    public String login(String username, String password, String code, String uuid) {
// 用户验证
        Authentication authentication = null;
        try {
            // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
            authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
        } catch (Exception e) {
            //错误次数
            Object object = redisCache.getCacheObject(key);
            if(null != object) {
                Long increment = redisCache.increment(key);
            } else {
                redisCache.setCacheObject(key, 1);
            }
            if(e instanceof BadCredentialsException) {
                AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
                throw new UserPasswordNotMatchException();
            } else {
                AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));
                throw new ServiceException(e.getMessage());
            }
        }
  return token;
    }

错误日志的最后一点是来回循环报错,太长了后面都一样的,所以没过多截取

StackOverflowError指的就是栈内存溢出

看下这个是不是一直在执行形成了递归调用,日志再往下点还有吗
authenticationManager.authenticate

这个错误通常是由于应用程序中的循环嵌套过多导致的。当应用程序中的循环嵌套太多时,会导致栈溢出错误。应该是你用于验证的authenticationManager相关的代码或配置出现了问题,导致访问登录页面的时候,后台进行拦截验证的时候,路由的跳转出现了问题,导致出现了死循环出不来,一直无法完成验证。建议你如果无法直观的检查到是哪里的问题的情况下,直接调试下拦截请求那部分的代码,看下进入了哪个循环。

可能发生了无限递归,导致栈空间被耗尽而抛出

回答部分参考、引用ChatGpt以便为您提供更准确的答案:

根据您提供的代码和错误信息,问题似乎出现在登录验证部分的代码中。具体来说,问题可能是由于身份验证管理器(authenticationManager)无法正常工作而导致的循环嵌套异常。

在代码中,您使用了org.springframework.security.authentication.AuthenticationManager接口的实现来执行身份验证操作。然而,根据错误日志,似乎在调用authenticationManager.authenticate()方法时,出现了循环调用的情况,并最终导致堆栈溢出异常(java.lang.StackOverflowError)。

这种情况通常发生在AuthenticationManager的实现类中存在循环依赖或配置问题时。可能的原因之一是,您在配置文件中未正确设置或注入AuthenticationManager的实现类。

为了解决此问题,您可以尝试以下步骤:

  1. 确保AuthenticationManager的实现类(例如,DaoAuthenticationProvider)已正确配置并与Spring Security集成。
  2. 检查SecurityConfig类中的authenticationManager bean的注入方式和配置是否正确。确保它与AuthenticationManager的实现类正确关联。
  3. 检查依赖关系是否正确注入。确保authenticationManager正确注入到SysLoginService中。
  4. 确保您的代码和配置文件与Spring Security 6.0的要求兼容。根据您的问题描述,您从Spring Boot 2.7迁移到Spring Boot 3.0,并升级了Spring Security版本。确保相关的依赖项和配置已经更新为与新版本兼容。

仔细检查上述步骤,解决任何配置错误或依赖关系问题,并确保AuthenticationManager的实现类能够正确执行身份验证操作,以避免循环嵌套异常的发生。

出现这个原因是因为循环依赖。
简单来说,A对象有一个属性是B,B对象有一个属性是A,当A对象生成的时候依赖B对象,
B对象生成的时候需要依赖A对象,这个时候就很容易形成一个闭环,如果这样一直死循环下去

问题原因:
栈内存溢出
增加下JVM堆内存大小。例如:java -Xmx512m YourMainClass。然后再测试下。
不行的话,一步一步排查,最好拿日志看下。下面是AI给出的排查方向

  1. 检查代码中是否存在无限递归调用的情况,例如递归函数、嵌套循环等。如果有,请修改代码以避免无限递归。

  2. 检查代码中是否存在死循环的情况,例如无限循环的if语句、while循环等。如果有,请修改代码以避免死循环。

  3. 检查代码中是否存在过多的线程或并发操作,这可能会导致栈溢出异常。如果是这种情况,请考虑使用线程池或其他并发控制技术来减少线程数量。

  4. 如果以上步骤都无法解决问题,您可以尝试增加JVM堆内存大小,以便为程序提供更多的内存空间。您可以通过在启动脚本中设置-Xmx参数来增加JVM堆内存大小。例如:java -Xmx512m YourMainClass。