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

Spring Security Kerberos中POST请求Spnego认证失败问题排查

问题:Spring Security Kerberos POST请求认证失败返回登录页

背景

基于Spring Security Kerberos的sec-server-win-auth示例扩展开发,新增了包含GET/POST映射的RestController,使用Swagger测试接口。搭建Active Directory服务器后,访问Swagger端点会弹出Windows登录窗口,认证成功可进入Swagger UI,但执行POST请求时,响应体返回带“Invalid username and password.”提示的登录页HTML,GET请求则正常工作。

错误响应示例

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
    <head>
        <title>Spring Security Kerberos Example</title>
        <link rel="icon" href="data:,">
    </head>
    <body>
        <div>
            Invalid username and password.
        </div>
        
        <form action="/login" method="post">
            <div><label> User Name : <input type="text" name="username"/> </label></div>
            <div><label> Password: <input type="password" name="password"/> </label></div>
            <div><input type="submit" value="Sign In"/></div>
        </form>
    </body>
</html>

关键日志

2024-05-08 11:30:13,508 [DEBUG|org.springframework.security.web.FilterChainProxy|FilterChainProxy] Securing POST /config
2024-05-08 11:30:13,509 [DEBUG|org.springframework.security.web.authentication.AnonymousAuthenticationFilter|AnonymousAuthenticationFilter] Set SecurityContextHolder to anonymous SecurityContext
2024-05-08 11:30:13,509 [DEBUG|org.springframework.security.web.savedrequest.HttpSessionRequestCache|HttpSessionRequestCache] Saved request https://myserver.test.local/config?continue to session
2024-05-08 11:30:13,510 [DEBUG|org.springframework.security.kerberos.web.authentication.SpnegoEntryPoint|SpnegoEntryPoint] Add header WWW-Authenticate:Negotiate to https://myserver.test.local/config, forward: /login
2024-05-08 11:30:13,515 [DEBUG|org.springframework.security.web.FilterChainProxy|FilterChainProxy] Securing POST /login
2024-05-08 11:30:13,517 [DEBUG|org.springframework.security.web.DefaultRedirectStrategy|DefaultRedirectStrategy] Redirecting to /login?error
2024-05-08 11:30:13,525 [DEBUG|org.springframework.security.web.FilterChainProxy|FilterChainProxy] Securing GET /login?error
2024-05-08 11:30:13,527 [DEBUG|org.springframework.security.web.FilterChainProxy|FilterChainProxy] Secured GET /login?error
2024-05-08 11:30:13,528 [DEBUG|org.springframework.web.servlet.DispatcherServlet|LogFormatUtils] GET "/login?error", parameters={masked}
2024-05-08 11:30:13,528 [DEBUG|org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping|AbstractHandlerMapping] Mapped to org.example.MainController#login()
2024-05-08 11:30:13,532 [DEBUG|org.springframework.web.servlet.DispatcherServlet|FrameworkServlet] Completed 200 OK
2024-05-08 11:30:13,532 [DEBUG|org.springframework.security.web.authentication.AnonymousAuthenticationFilter|AnonymousAuthenticationFilter] Set SecurityContextHolder to anonymous SecurityContext

调试发现AbstractLdapAuthenticationProvider::authenticate方法被调用并抛出BadCredentialsException,其中username和password变量为空字符串。推测是Spnego认证过程中发起了POST /login请求,与登录页点击登录按钮的行为一致,且请求方法始终与初始请求相同(测试DELETE请求也出现同样问题)。

疑问

  • 为何AbstractLdapAuthenticationProvider会被调用?能否禁用它,仅使用KerberosServiceAuthenticationProvider?
  • 为何Spnego认证过程中会出现forward?(日志:Add header WWW-Authenticate:Negotiate to https://myserver.test.local/config, forward: /login)
  • 为何请求HTTP方法始终与初始请求相同?

配置代码

/* imports omitted */
@Configuration
@EnableWebSecurity
public class WebSecurityConfig {

    @Autowired
    private SpringConfig config;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        KerberosServiceAuthenticationProvider kerberosServiceAuthenticationProvider = kerberosServiceAuthenticationProvider();
        ActiveDirectoryLdapAuthenticationProvider activeDirectoryLdapAuthenticationProvider = activeDirectoryLdapAuthenticationProvider();
        ProviderManager providerManager = new ProviderManager(kerberosServiceAuthenticationProvider, activeDirectoryLdapAuthenticationProvider);

        http
            .authorizeHttpRequests(authz -> authz
                .anyRequest()
                .authenticated()
            )
            .exceptionHandling(exceptionHandling -> exceptionHandling
                .authenticationEntryPoint(spnegoEntryPoint())
            )
            .formLogin(formLogin -> formLogin
                .loginPage(config.getActiveDirectoryLoginSerlvet())
                .permitAll()
            )
            .logout(logout -> logout
                .permitAll()
            )
            .authenticationProvider(activeDirectoryLdapAuthenticationProvider)
            .authenticationProvider(kerberosServiceAuthenticationProvider)
            .addFilterBefore(spnegoAuthenticationProcessingFilter(providerManager), BasicAuthenticationFilter.class)
            .csrf(csrf -> csrf
                .disable()
            );

        return http.build();
    }

    @Bean
    public ActiveDirectoryLdapAuthenticationProvider activeDirectoryLdapAuthenticationProvider() {
        return new ActiveDirectoryLdapAuthenticationProvider(config.getActiveDirectoryDomain(), config.getActiveDirectoryServer());
    }

    @Bean
    public SpnegoEntryPoint spnegoEntryPoint() {
        return new SpnegoEntryPoint(config.getActiveDirectoryLoginSerlvet());
    }

    // @Bean
    public SpnegoAuthenticationProcessingFilter spnegoAuthenticationProcessingFilter(AuthenticationManager authenticationManager) {
        SpnegoAuthenticationProcessingFilter filter = new SpnegoAuthenticationProcessingFilter();
        filter.setAuthenticationManager(authenticationManager);
        return filter;
    }

    public KerberosServiceAuthenticationProvider kerberosServiceAuthenticationProvider() throws Exception {
        KerberosServiceAuthenticationProvider provider = new KerberosServiceAuthenticationProvider();
        provider.setTicketValidator(sunJaasKerberosTicketValidator());
        provider.setUserDetailsService(ldapUserDetailsService());
        return provider;
    }

    @Bean
    public SunJaasKerberosTicketValidator sunJaasKerberosTicketValidator() {
        SunJaasKerberosTicketValidator ticketValidator = new SunJaasKerberosTicketValidator();
        ticketValidator.setServicePrincipal(config.getActiveDirectoryServicePrincipal());
        ticketValidator.setKeyTabLocation(new FileSystemResource(config.getActiveDirectoryKeytabLocation()));
        ticketValidator.setDebug(true);
        return ticketValidator;
    }

    @Bean
    public KerberosLdapContextSource kerberosLdapContextSource() throws Exception {
        KerberosLdapContextSource contextSource = new KerberosLdapContextSource(config.getActiveDirectoryServer());
        contextSource.setLoginConfig(loginConfig());
        return contextSource;
    }

    public SunJaasKrb5LoginConfig loginConfig() throws Exception {
        SunJaasKrb5LoginConfig loginConfig = new SunJaasKrb5LoginConfig();
        loginConfig.setKeyTabLocation(new FileSystemResource(config.getActiveDirectoryKeytabLocation()));
        loginConfig.setServicePrincipal(config.getActiveDirectoryServicePrincipal());
        loginConfig.setDebug(true);
        loginConfig.setIsInitiator(true);
        loginConfig.afterPropertiesSet();
        return loginConfig;
    }

    @Bean
    public LdapUserDetailsService ldapUserDetailsService() throws Exception {
        FilterBasedLdapUserSearch userSearch = new FilterBasedLdapUserSearch(config.getActiveDirectoryLdapSearchBase(), config.getActiveDirectoryLdapSearchFilter(), kerberosLdapContextSource());
        LdapUserDetailsService service = new LdapUserDetailsService(userSearch, new ActiveDirectoryLdapAuthoritiesPopulator());
        service.setUserDetailsMapper(new LdapUserDetailsMapper());
        return service;
    }

}

内容的提问来源于stack exchange,提问作者Benjamin

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 13:44:55