Почему запросы POST не работают с использованием аутентификации Spnego/Kerberos с помощью Spring Security Kerberos?

Мы взяли пример приложения sec-server-win-auth из документации Spring Security Kerberos и расширили его с помощью RestController. В этом RestController мы определили некоторые GET- и POST-отображения для обработки соответствующих запросов.
Кроме того, мы используем swagger для проверки запросов.

После настройки сервера Active Directory мы можем запустить приложение, и после открытия swagger-endpoint https://myserver.test.local/swagger-ui/index.html пользователю будет предложено открыть окно входа в Windows. После аутентификации открывается пользовательский интерфейс Swagger, и можно опробовать запросы.

Запросы GET работают нормально, но после выполнения запроса POST тело ответа содержит HTML-код со страницы входа вместе с сообщением «Неверное имя пользователя и пароль.»:

<!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>

Тело реакции

Покопавшись и отладив, мы обнаружили, что во время протокола spnego-аутентификации выполняется запрос /login. Это не проблема, если кто-то выполняет запрос GET, например, /config, но если запрос POST на /config выполняется, происходит следующее

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в строках 68 и 69 являются пустыми строками. Таким образом, веб-приложение считает, что пользователь ввел неверные учетные данные, и отвечает на странице входа вместе с сообщением «Неверное имя пользователя и пароль».
Мы подозреваем, что причина вызова AbstractLdapAuthenticationProvider заключается в том, что был выполнен запрос POST на /login, что также происходит, если нажать кнопку входа в систему после ввода имени пользователя и пароля на странице /login.

Похоже, что во время spnego-протокола запрос на странице /login выполняется с использованием того же HTTP-метода, что и первоначальный запрос (мы также пробовали это с DELETE).

Наш вопрос(ы):

  • Почему AbstractLdapAuthenticationProvider вообще называется? Можем ли мы отключить его и использовать только KerberosServiceAuthenticationProvider?
  • Почему мы получаем forward во время аутентификации Spnego? (Add header WWW-Authenticate:Negotiate to https://myserver.test.local/config, forward: /login)
  • Почему HTTP-метод всегда тот же, что и при первоначальном запросе?

Это наш WebSecurityConfig (измененный из образца):

/* 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;
    }

}

Спасибо!

Обновлено: добавлены ссылки на образец Spring и репозиторий AuthenticatinoProvider на GitHub.

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
0
72
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

TLDR: используйте конструктор SpnegoEntryPoint() вместо SpnegoEntryPoint(String forwardUrl).


Еще немного покопавшись, я понял:

В примере SpnegoEntryPoint создается с помощью конструкции SpnegoEntryPoint(String forwardUrl), где forwardUrl = "/login". Поскольку первоначальный запрос представляет собой запрос POST, а SpnegoEntryPoint пересылается на "/login" (см. строку 105 в SpnegoEntryPoint.java), AbstractLdapAuthenticationProvider::authenticate вызывается и выдает исключение.
Если использовать конструктор SpnegoEntryPoint() без каких-либо аргументов, проблема полностью решена, и запросы POST также работают.

Единственный недостаток, с которым я столкнулся до сих пор, заключается в том, что если кто-то не хочет использовать Kerberos для аутентификации в приложении, а использует стандартный запрос пароля LDAP, ему/ей приходится явно вводить конечную точку "/login", при отмене происходит перенаправление на эту конечную точку. окно входа в Windows отключено.

Другие вопросы по теме