通过 Spring 安全性的 Kerberos 身份验证在 IE11 和 Chrome 但不是 Firefox 中失败

Kerberos Authentication through Spring Security Failing in IE11 and Chrome but not Firefox

简介

我正在使用 Spring Securities Kerberos 身份验证来处理网站登录。我按照说明 here and used the code from here 对用户进行了身份验证。在 Firefox 中,一切都成功,下面的登录页面按预期弹出,我可以使用我的 windows 登录名登录。

但是在IE和Chrome中认证失败。没有显示登录屏幕,而是显示了一个要求输入密码的弹出窗口。当我输入 Windows 用户名和密码时,我会看到下面的屏幕。 尽管在 Chrome 和 IE 上显示 HTTP 错误 500,但 Spring 服务器端未显示任何错误。

研究

我尝试 运行 提供的示例代码 here(在 spring-security-kerberos-samples/sec-server-win-auth 下),但同样的问题仍然存在。然而,在这种情况下,Spring returns 出现以下错误

org.springframework.security.authentication.BadCredentialsException: Kerberos validation not successful
    at org.springframework.security.kerberos.authentication.sun.SunJaasKerberosTicketValidator.validateTicket(SunJaasKerberosTicketValidator.java:71)
    at org.springframework.security.kerberos.authentication.KerberosServiceAuthenticationProvider.authenticate(KerberosServiceAuthenticationProvider.java:64)
    at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:156)
    at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:177)
    at org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter$AuthenticationManagerDelegator.authenticate(WebSecurityConfigurerAdapter.java:436)
    at org.springframework.security.kerberos.web.authentication.SpnegoAuthenticationProcessingFilter.doFilter(SpnegoAuthenticationProcessingFilter.java:145)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:199)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:110)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.csrf.CsrfFilter.doFilterInternal(CsrfFilter.java:85)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:57)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:87)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:50)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:192)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:160)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:85)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:219)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:106)
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:501)
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:142)
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:79)
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:88)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:537)
    at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1085)
    at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:658)
    at org.apache.coyote.http11.Http11NioProtocol$Http11ConnectionHandler.process(Http11NioProtocol.java:222)
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1556)
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.run(NioEndpoint.java:1513)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.lang.Thread.run(Thread.java:745)
Caused by: java.security.PrivilegedActionException: null
    at java.security.AccessController.doPrivileged(Native Method)
    at javax.security.auth.Subject.doAs(Subject.java:422)
    at org.springframework.security.kerberos.authentication.sun.SunJaasKerberosTicketValidator.validateTicket(SunJaasKerberosTicketValidator.java:68)
    ... 45 common frames omitted
Caused by: org.ietf.jgss.GSSException: Defective token detected (Mechanism level: GSSHeader did not find the right tag)
    at sun.security.jgss.GSSHeader.<init>(GSSHeader.java:97)
    at sun.security.jgss.GSSContextImpl.acceptSecContext(GSSContextImpl.java:306)
    at sun.security.jgss.GSSContextImpl.acceptSecContext(GSSContextImpl.java:285)
    at org.springframework.security.kerberos.authentication.sun.SunJaasKerberosTicketValidator$KerberosValidateAction.run(SunJaasKerberosTicketValidator.java:170)
    at org.springframework.security.kerberos.authentication.sun.SunJaasKerberosTicketValidator$KerberosValidateAction.run(SunJaasKerberosTicketValidator.java:1)
    ... 48 common frames omitted

我确定用户名和密码是正确的,但在 IE 和 Chrome 上验证仍然失败,但在 Firefox 上验证成功。

此外,我尝试按照教程 here 进行操作,该教程假定允许在 IE 上进行 Kerberos 身份验证。唯一不同的是没有提示输入密码,而是页面显示500错误。

问题

我有没有办法更改项目的配置以在所有浏览器上工作,或者当前的身份验证方法只适用于 Firefox?

关联文件

build.gradle

    buildscript {
    ext {
        springBootVersion = '1.4.1.RELEASE'
    }
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'spring-boot'

jar {
    baseName = 'vlgx-portal-app'
    version = '0.0.1-SNAPSHOT'
}
sourceCompatibility = 1.7
targetCompatibility = 1.7

repositories {
    mavenCentral()
}


dependencies {
    compile('org.springframework.boot:spring-boot-starter-jersey')
    compile('org.springframework.boot:spring-boot-starter-security')
    compile('org.springframework.boot:spring-boot-starter-thymeleaf')
    compile('org.springframework.boot:spring-boot-starter-web')
    compile('org.springframework.boot:spring-boot-starter-web-services')
    compile('org.jsoup:jsoup:1.8.1')
    compile ('javax.mail:mail:1.4.7')
    compile 'org.springframework.security.kerberos:spring-security-kerberos-web:1.0.1.RELEASE'
    compile 'org.springframework.security.kerberos:spring-security-kerberos-client:1.0.1.RELEASE'
    compile 'org.springframework.security:spring-security-ldap:4.2.3.RELEASE'
    //runtime('org.postgresql:postgresql')
    testCompile('org.springframework.boot:spring-boot-starter-test')
}

WebSecurityConfig.java

package com.valogix.portal.configuration;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.FileSystemResource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
import org.springframework.security.kerberos.authentication.KerberosServiceAuthenticationProvider;
import org.springframework.security.kerberos.authentication.sun.SunJaasKerberosTicketValidator;
import org.springframework.security.kerberos.client.config.SunJaasKrb5LoginConfig;
import org.springframework.security.kerberos.client.ldap.KerberosLdapContextSource;
import org.springframework.security.kerberos.web.authentication.SpnegoAuthenticationProcessingFilter;
import org.springframework.security.kerberos.web.authentication.SpnegoEntryPoint;
import org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider;
import org.springframework.security.ldap.search.FilterBasedLdapUserSearch;
import org.springframework.security.ldap.userdetails.LdapUserDetailsMapper;
import org.springframework.security.ldap.userdetails.LdapUserDetailsService;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;

@Configuration
@EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Value("${app.ad-domain}")
    private String adDomain;

    @Value("${app.ad-server}")
    private String adServer;

    @Value("${app.service-principal}")
    private String servicePrincipal;

    @Value("${app.keytab-location}")
    private String keytabLocation;

    @Value("${app.ldap-search-base}")
    private String ldapSearchBase;

    @Value("${app.ldap-search-filter}")
    private String ldapSearchFilter;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .exceptionHandling()
                .authenticationEntryPoint(spnegoEntryPoint())
                .and()
            .authorizeRequests()
                .antMatchers("/login").permitAll()
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .loginPage("/login").permitAll()
                .and()
            .logout()
                .permitAll()
                .and()
            .addFilterBefore(
                    spnegoAuthenticationProcessingFilter(authenticationManagerBean()),
                    BasicAuthenticationFilter.class);
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
            .authenticationProvider(activeDirectoryLdapAuthenticationProvider())
            .authenticationProvider(kerberosServiceAuthenticationProvider());
    }

    @Bean
    public ActiveDirectoryLdapAuthenticationProvider activeDirectoryLdapAuthenticationProvider() {
        return new ActiveDirectoryLdapAuthenticationProvider(adDomain, adServer);
    }

    @Bean
    public SpnegoEntryPoint spnegoEntryPoint() {
        return new SpnegoEntryPoint("/login");
    }

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

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

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

    @Bean
    public KerberosLdapContextSource kerberosLdapContextSource() {
        KerberosLdapContextSource contextSource = new KerberosLdapContextSource(adServer);
        contextSource.setLoginConfig(loginConfig());
        return contextSource;
    }

    @Bean
    public SunJaasKrb5LoginConfig loginConfig() {
        SunJaasKrb5LoginConfig loginConfig = new SunJaasKrb5LoginConfig();
        loginConfig.setKeyTabLocation(new FileSystemResource(keytabLocation));
        loginConfig.setServicePrincipal(servicePrincipal);
        loginConfig.setDebug(true);
        loginConfig.setIsInitiator(true);
        return loginConfig;
    }

    @Bean
    public LdapUserDetailsService ldapUserDetailsService() {
        FilterBasedLdapUserSearch userSearch =
                new FilterBasedLdapUserSearch(ldapSearchBase, ldapSearchFilter, kerberosLdapContextSource());
        LdapUserDetailsService service = new LdapUserDetailsService(userSearch);
        service.setUserDetailsMapper(new LdapUserDetailsMapper());
        return service;
    }

}

application.properties

server.port = 8096
customerServiceEmail = "example@gmail.com"
errorLogDirectory = "error_log_path"

app.ad-domain: Domain
app.ad-server: ad_server
app.service-principal: HTTP/path_or_something
app.keytab-location: /tmp/tomcat.keytab
app.ldap-search-base: dc=example,dc=org
app.ldap-search-filter: "(| (userPrincipalName={0}) (sAMAccountName={0}))"

如果有什么忘记的请告诉我,谢谢你的时间。

更新

Chrome 只要我退出弹出 window.

就会工作

看起来像是配置问题(使用 SPN... 而不是 Java 代码)。我想 Kerberos 根本不起作用,但 Firefox 使用 NTLM,这就是它起作用的原因。 Chrome 通常使用 IE 的配置。 IE 可能比 Firefox 更严格,不允许使用错误票证进行身份验证:

GSSException: Defective token detected (Mechanism level: GSSHeader did not find the right tag)

您可以使用 Fiddler 查看请求 - 这里是 some instruction

仔细检查您的服务帐户和 SPN:

setspn.exe -L accountname

如果是 HTTP 协议,SPN 的形式应该是 HTTP/machineName.your.domain.com

确保您使用地址 http://machineName.your.domain.com 访问您的应用程序。当然你可以在URL之后添加特定端口(例如8080)或特定路径。但使用与 SPN 中相同的 URL - 不要使用 IP 地址。

我还建议有选择地遵循此指南:SPNEGO SSO using Kerberos

Here 也是微软关于 SPN 构建的一些老帖子。

查找其他帐户中是否有重复的 SPN:

setspn.exe -Q <SPN>

SetSPN command sytax.

this question.

中所述,SPN 重复会导致 NTLM 回退

最后搜索有关令牌缺陷问题的更多信息 - here are some good answers

我费了好大劲才解决了这个问题。它在

http
    .exceptionHandling()
    .authenticationEntryPoint(spnegoEntryPoint())
    .and()
        .authorizeRequests()
            .antMatchers("/login").permitAll()
            .anyRequest().authenticated()
            .and()
        .formLogin()
            .loginPage("/login").permitAll()
            .and()
        .logout()
            .permitAll()
            .and()
        .addFilterBefore(
                spnegoAuthenticationProcessingFilter(authenticationManagerBean()),
                BasicAuthenticationFilter.class);

exceptionHandling() 导致在用户尝试连接到他们未通过身份验证无法查看的页面时显示一个弹出窗口,要求输入用户名和密码。弹出窗口似乎没有正确配置以验证用户身份。我删除了这段代码

.exceptionHandling()
.authenticationEntryPoint(spnegoEntryPoint())
.and()

因为我已经有一个自定义登录页面,用户无论如何都会被重定向到。不再出现错误,用户可以在 IE、Chrome 和 Firefox 上登录。

对于处理此问题的任何人 - 是的,如果您像 skagra_dragneel 所建议的那样省略 spnegoEntryPoint,它将正常工作。但是如果你想在 Windows 域中的机器上单点登录,这不是一个选项,因为你需要入口点来发送协商 header。

到目前为止,由于过去几天我一直在研究这个 SSO/Kerberos/Spring 安全问题,所以我看到了这种行为:

  • 在 SSO 场景中,在 Chrome 和 Firefox
  • 中一切正常
  • 在 Windows 域外的机器上,Firefox 工作正常。它显示登录表单。但是 Chrome 不起作用 - 它显示浏览器登录 window。如果你选择 Storno,它工作正常,它接受重定向到登录页面。但是,如果您填写凭据并点击确定,我将向服务器发送一些废话作为 kerberos 票证。此票证的验证将失败,因此 SpnegoAuthenticationProcessingFilter 将响应 500:
try {
  authentication = authenticationManager.authenticate(authenticationRequest);
} catch (AuthenticationException e) {
  // That shouldn't happen, as it is most likely a wrong
  // configuration on the server side
  logger.warn("Negotiate Header was invalid: " + header, e);
  SecurityContextHolder.clearContext();
  if (failureHandler != null) {
    failureHandler.onAuthenticationFailure(request, response, e);
  } else {                  
    response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
    response.flushBuffer();
  }
  return;
}

到目前为止,我发现的唯一解决方案是将错误处理程序注册到 Spnego 过滤器,如果 kerberos 票证错误,则重定向到登录页面:

@Bean
public SpnegoAuthenticationProcessingFilter spnegoAuthenticationProcessingFilter( AuthenticationManager authenticationManager) {
  SpnegoAuthenticationProcessingFilter filter = new SpnegoAuthenticationProcessingFilter();
  filter.setAuthenticationManager(authenticationManager);
  filter.setFailureHandler(new AuthenticationFailureHandler() {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
      AuthenticationException exception) throws IOException, ServletException {

      //redirection in case of wrong ticket
      response.sendRedirect("login");
    }
  });
  return filter;
}

这将适用于两种情况 - SSO 以及 Windows 域外。