Мы взяли пример приложения 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)Это наш 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.





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 отключено.