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

Spring Boot 2迁移至3:LDAP与API安全配置异常问题

Spring Boot 2迁移至Spring Boot 3的安全配置问题

我正在将Spring Boot 2项目迁移至Spring Boot 3,安全配置遇到问题。应用包含需要LDAP认证的Web应用,以及需要HTTP Basic认证的API;预期/api路径跳转到Swagger界面并启用HTTP Basic认证,但所有URL都被重定向到登录页。

相关配置代码

WebSecurityConfig.java

@EnableWebSecurity
public class WebSecurityConfig {

    @Configuration
    @Order(1)
    public static class ApiWebSecurityConfigurationAdapter {

        @Autowired
        CustomAuthenticationProvider customAuthenticationProvider;

        @Bean
        protected SecurityFilterChain apiSecurityFilterChain(final HttpSecurity http) throws Exception {
            http.securityMatcher("/api/**").authorizeHttpRequests(auth -> {
                auth.anyRequest().authenticated();
            });
            http.headers(header -> header.frameOptions().disable());
            http.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
            http.httpBasic(withDefaults());
            return http.build();
        }

        @Autowired
        public void configure(final AuthenticationManagerBuilder auth) throws Exception {
            auth.authenticationProvider(customAuthenticationProvider);
        }
    }

    @Configuration
    @Order(2)
    public static class FormLoginWebSecurityConfigurerAdapter {

        @Autowired
        UserService userDetailsService;

        @Value("${ldap.urls}")
        private String ldapUrls;

        @Value("${ldap.base.dn}")
        private String ldapBaseDn;

        @Value("$ldap.search.ou")
        private String ldapSearchOu;

        @Value("${ldap.username}")
        private String ldapSecurityPrincipal;

        @Value("${ldap.password}")
        private String ldapPrincipalPassword;

        @Value("${ldap.user.dn.pattern}")
        private String ldapUserDnPattern;

        private static final String[] AUTH_WHITELIST = {"/swagger-resources/**", "/swagger-ui.html",
                "/swagger-ui/**", "/v3/api-docs", "/webjars/**", "/api"};

        @Bean
        protected SecurityFilterChain filterChain(final HttpSecurity http) throws Exception {
            http.csrf(csrf -> csrf.disable())
                    .authorizeHttpRequests(auth -> auth.shouldFilterAllDispatcherTypes(true).requestMatchers("/login*").permitAll()
                            .requestMatchers(AUTH_WHITELIST).permitAll().requestMatchers("/").authenticated())
                    .formLogin(
                            login -> login.loginPage("/login").loginProcessingUrl("/login").failureUrl("/login?error"))
                    .logout(logout -> logout.invalidateHttpSession(false).clearAuthentication(true)
                            .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
                            .logoutSuccessUrl("/login?logout").permitAll());
            http.headers(header -> header.frameOptions().disable());
            http.sessionManagement(session -> session.maximumSessions(1).and().invalidSessionUrl("/login?expired"));
            return http.build();
        }

        @Autowired
        public void configure(final AuthenticationManagerBuilder auth) throws Exception {
            auth.ldapAuthentication().ldapAuthoritiesPopulator(new CustomLdapAuthoritiesPopulator(userDetailsService))
                    .userSearchFilter(ldapUserDnPattern).groupSearchBase(ldapSearchOu).contextSource()
                    .url(ldapUrls + ldapBaseDn).managerDn(ldapSecurityPrincipal).managerPassword(ldapPrincipalPassword);
        }

    }

}

UserService.java

@Service
public class UserService implements UserDetailsService {

  private final UtilisateurRepository utilisateurRepository;

  public UserService(final UtilisateurRepository utilisateurRepository) {
    this.utilisateurRepository = utilisateurRepository;
  }

  /**
   * loadUserByUsername.
   *
   * @param username Le nom d'utilisateur
   * @return L'utilisateur
   */
  @Override
  public UserDetails loadUserByUsername(final String username) throws UsernameNotFoundException {
    final UtilisateurDao utilisateurDao = utilisateurRepository.findByLogin(username);
    return new User(utilisateurDao.getMatricule().toString(), null, null);
  }

  /**
   * loadUserAuthorities.
   *
   * @param username Le nom d'utilisateur
   * @return Le profil de l'utilisateur
   */
  public List<GrantedAuthority> loadUserAuthorities(final String username) {
    final UtilisateurDao utilisateurDao = utilisateurRepository.findByLogin(username);
    if (utilisateurDao != null && utilisateurDao.getProfil() != null) {
      return AuthorityUtils.createAuthorityList(utilisateurDao.getProfil());
    }
    return new ArrayList<GrantedAuthority>();
  }

}

日志信息

2023-07-13T09:15:52.921+02:00[0;39m [32mDEBUG[0;39m [35m5028[0;39m [2m---[0;39m [2m[nio-8081-exec-3][0;39m [36mo.s.security.web.FilterChainProxy       [0;39m [2m:[0;39m Securing GET /
[2m2023-07-13T09:15:52.929+02:00[0;39m [32mDEBUG[0;39m [35m5028[0;39m [2m---[0;39m [2m[nio-8081-exec-3][0;39m [36mo.s.s.w.a.AnonymousAuthenticationFilter [0;39m [2m:[0;39m Set SecurityContextHolder to anonymous SecurityContext
[2m2023-07-13T09:15:52.930+02:00[0;39m [32mDEBUG[0;39m [35m5028[0;39m [2m---[0;39m [2m[nio-8081-exec-3][0;39m [36mo.s.s.w.session.SessionManagementFilter [0;39m [2m:[0;39m Request requested invalid session id 87847793E2A3E57F9B41DE144174BE4D
[2m2023-07-13T09:15:52.930+02:00[0;39m [32mDEBUG[0;39m [35m5028[0;39m [2m---[0;39m [2m[nio-8081-exec-3][0;39m [36mw.s.SimpleRedirectInvalidSessionStrategy[0;39m [2m:[0;39m Starting new session (if required) and redirecting to '/login?expired'
[2m2023-07-13T09:15:52.976+02:00[0;39m [32mDEBUG[0;39m [35m5028[0;39m [2m---[0;39m [2m[nio-8081-exec-3][0;39m [36mo.s.s.web.DefaultRedirectStrategy       [0;39m [2m:[0;39m Redirecting to /login?expired
[2m2023-07-13T09:15:52.985+02:00[0;39m [32mDEBUG[0;39m [35m5028[0;39m [2m---[0;39m [2m[nio-8081-exec-2][0;39m [36mo.s.security.web.FilterChainProxy       [0;39m [2m:[0;39m Securing GET /login?expired
[2m2023-07-13T09:15:52.986+02:00[0;39m [32mDEBUG[0;39m [35m5028[0;39m [2m---[0;39m [2m[nio-8081-exec-2][0;39m [36mo.s.s.w.a.AnonymousAuthenticationFilter [0;39m [2m:[0;39m Set SecurityContextHolder to anonymous SecurityContext
[2m2023-07-13T09:15:52.987+02:00[0;39m [32mDEBUG[0;39m [35m5028[0;39m [2m---[0;39m [2m[nio-8081-exec-2][0;39m [36mo.s.security.web.FilterChainProxy       [0;39m [2m:[0;39m Secured GET /login?expired
[2m2023-07-13T09:16:16.945+02:00[0;39m [32mDEBUG[0;39m [35m5028[0;39m [2m---[0;39m [2m[nio-8081-exec-4][0;39m [36mo.s.security.web.FilterChainProxy       [0;39m [2m:[0;39m Securing GET /api
[2m2023-07-13T09:16:16.948+02:00[0;39m [32mDEBUG[0;39m [35m5028[0;39m [2m---[0;39m [2m[nio-8081-exec-4][0;39m [36mo.s.s.w.a.AnonymousAuthenticationFilter [0;39m [2m:[0;39m Set SecurityContextHolder to anonymous SecurityContext
[2m2023-07-13T09:16:16.949+02:00[0;39m [32mDEBUG[0;39m [35m5028[0;39m [2m---[0;39m [2m[nio-8081-exec-4][0;39m [36ms.w.a.DelegatingAuthenticationEntryPoint[0;39m [2m:[0;39m Trying to match using RequestHeaderRequestMatcher [expectedHeaderName=X-Requested-With, expectedHeaderValue=XMLHttpRequest]
[2m2023-07-13T09:16:16.950+02:00[0;39m [32mDEBUG[0;39m [35m5028[0;39m [2m---[0;39m [2m[nio-8081-exec-4][0;39m [36ms.w.a.DelegatingAuthenticationEntryPoint[0;39m [2m:[0;39m No match found. Using default entry point org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint@ffd6833
[2m2023-07-13T09:16:16.956+02:00[0;39m [32mDEBUG[0;39m [35m5028[0;39m [2m---[0;39m [2m[nio-8081-exec-4][0;39m [36mo.s.security.web.FilterChainProxy       [0;39m [2m:[0;39m Securing GET /error
[2m2023-07-13T09:16:16.958+02:00[0;39m [32mDEBUG[0;39m [35m5028[0;39m [2m---[0;39m [2m[nio-8081-exec-4][0;39m [36mo.s.s.w.a.AnonymousAuthenticationFilter [0;39m [2m:[0;39m Set SecurityContextHolder to anonymous SecurityContext
[2m2023-07-13T09:16:16.962+02:00[0;39m [32mDEBUG[0;39m [35m5028[0;39m [2m---[0;39m [2m[nio-8081-exec-4][0;39m [36mo.s.s.w.s.HttpSessionRequestCache       [0;39m [2m:[0;39m Saved request http://localhost:8081/error?continue to session
[2m2023-07-13T09:16:16.963+02:00[0;39m [32mDEBUG[0;39m [35m5028[0;39m [2m---[0;39m [2m[nio-8081-exec-4][0;39m [36mo.s.s.web.DefaultRedirectStrategy       [0;39m [2m:[0;39m Redirecting to http://localhost:8081/login
[2m2023-07-13T09:16:16.968+02:00[0;39m [32mDEBUG[0;39m [35m5028[0;39m [2m---[0;39m [2m[nio-8081-exec-1][0;39m [36mo.s.security.web.FilterChainProxy       [0;39m [2m:[0;39m Securing GET /login
[2m2023-07-13T09:16:16.968+02:00[0;39m [32mDEBUG[0;39m [35m5028[0;39m [2m---[0;39m [2m[nio-8081-exec-1][0;39m [36mo.s.s.w.a.AnonymousAuthenticationFilter [0;39m [2m:[0;39m Set SecurityContextHolder to anonymous SecurityContext
[2m2023-07-13T09:16:16.969+02:00[0;39m [32mDEBUG[0;39m [35m5028[0;39m [2m---[0;39m [2m[nio-8081-exec-1][0;39m [36mo.s.security.web.FilterChainProxy       [0;39m [2m:[0;39m Secured GET /login

问题分析与修复方案

核心问题点

  1. /api路径匹配范围不足:API配置的securityMatcher("/api/**")仅匹配/api开头的子路径,不匹配/api本身,导致/api请求被FormLogin的规则处理。
  2. LDAP属性注入错误:@Value("$ldap.search.ou")缺少大括号,应为@Value("${ldap.search.ou}"),会导致LDAP配置加载失败。
  3. 认证错误页被拦截:访问/api未认证时触发Basic认证返回401,Spring Boot自动转发到/error,而/error不在API的匹配范围内,被FormLogin规则重定向到登录页。
  4. UserDetails构造不合法:new User(..., null, null)传入空密码和空权限,不符合Spring Security的UserDetails规范,可能引发认证异常。

修复步骤

1. 扩展API的路径匹配范围

修改API配置的securityMatcher,同时匹配/api和/api/**:

http.securityMatcher("/api", "/api/**")
2. 修复LDAP属性注入

修正属性注解的语法错误:

@Value("${ldap.search.ou}")
private String ldapSearchOu;
3. 禁用API的请求缓存,避免重定向

在API的SecurityFilterChain中添加请求缓存禁用配置,防止认证失败后被重定向到登录页:

http.requestCache(request -> request.disable());
4. 修正UserDetails构造逻辑

确保User对象包含合法的密码和权限:

@Override
public UserDetails loadUserByUsername(final String username) throws UsernameNotFoundException {
    final UtilisateurDao utilisateurDao = utilisateurRepository.findByLogin(username);
    if (utilisateurDao == null) {
        throw new UsernameNotFoundException("用户不存在: " + username);
    }
    List<GrantedAuthority
相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 10:41:34