我们从 Spring Security Kerberos 文档中获取了 sec-server-win-auth 示例应用程序,并通过
RestController
对其进行了扩展。在此 RestController
中,我们定义了一些 GET
- 和 POST
- 映射来处理相应的请求。设置 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 存储库的链接
:使用 SpnegoEntryPoint()
构造函数而不是
SpnegoEntryPoint(String forwardUrl)
。
经过更多挖掘,我已经弄清楚了:在示例中,
SpnegoEntryPoint
使用
SpnegoEntryPoint(String forwardUrl)
构造函数构造,其中 forwardUrl
= "/login"
。由于初始请求是 POST
请求,并且 SpnegoEntryPoint
转发到 "/login"
(请参阅 SpnegoEntryPoint.java中的第 105 行),因此
AbstractLdapAuthenticationProvider::authenticate
被调用并抛出异常。如果使用不带任何参数的
SpnegoEntryPoint()
POST
请求也能工作。到目前为止,我遇到的唯一缺点是,如果不想使用 Kerberos 对应用程序进行身份验证,而是使用标准 LDAP 密码查询,则他/她必须显式输入 "/login"
端点,重定向到此端点如果取消,Windows 登录窗口将关闭。