为什么 POST 请求无法使用 Spring Security Kerberos 进行 Spnego/Kerberos 身份验证?

问题描述 投票:0回答:1

我们从 Spring Security Kerberos 文档中获取了 sec-server-win-auth 示例应用程序,并通过

RestController
对其进行了扩展。在此
RestController
中,我们定义了一些
GET
- 和
POST
- 映射来处理相应的请求。
此外,我们正在使用 swagger 来尝试这些请求。

设置 Active Directory 服务器后,我们可以启动应用程序,并在打开 swagger-endpoint

https://myserver.test.local/swagger-ui/index.html
时,系统会提示用户 Windows 登录窗口。身份验证后,swagger UI 将打开,您可以尝试请求。

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-authentication 协议期间,执行了对

/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
。使用调试器我们发现第 68 行和第 69 行中的变量
username
password
是空字符串。因此,网络应用程序认为用户输入了错误的凭据,并响应登录页面以及消息“无效的用户名和密码”。 我们怀疑调用
AbstractLdapAuthenticationProvider
 的原因是因为在 
POST
 上执行了 
/login
 请求,如果在 
/login
 页面上输入用户名和密码后单击登录按钮也会发生这种情况。

看来,在 spnego 协议期间,

/login

 页面上的请求是使用与初始请求相同的
HTTP 方法执行的(我们也使用 DELETE 尝试过)。
我们的问题:

为什么首先要调用
    AbstractLdapAuthenticationProvider
  • ?我们可以禁用它并只使用
    KerberosServiceAuthenticationProvider
    吗?
    为什么我们在 spnego 身份验证过程中会得到 
  • forward
  • ? (
    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 存储库的链接

spring-security active-directory ldap kerberos spnego
1个回答
0
投票
TLDR

:使用 SpnegoEntryPoint() 构造函数而不是

SpnegoEntryPoint(String forwardUrl)

经过更多挖掘,我已经弄清楚了:

在示例中,

SpnegoEntryPoint

使用

SpnegoEntryPoint(String forwardUrl)
构造函数构造,其中
forwardUrl
=
"/login"
。由于初始请求是
POST
请求,并且
SpnegoEntryPoint
转发到
"/login"
(请参阅
SpnegoEntryPoint.java
中的第 105 行),因此 AbstractLdapAuthenticationProvider::authenticate 被调用并抛出异常。
如果使用不带任何参数的 
SpnegoEntryPoint()
构造函数,问题就完全解决了,并且
POST
请求也能工作。
到目前为止,我遇到的唯一缺点是,如果不想使用 Kerberos 对应用程序进行身份验证,而是使用标准 LDAP 密码查询,则他/她必须显式输入 

"/login"

端点,重定向到此端点如果取消,Windows 登录窗口将关闭。

    

© www.soinside.com 2019 - 2024. All rights reserved.