无法使用 Spring 个处理程序捕获 LDAP CommunicationException

Can't catch LDAP CommunicationException with Spring handlers

目前我在安全认证过程中使用 ActiveDirectoryLdapAuthenticationProvider 和一个自定义提供程序。假设 CustomAuthenticationProvider 抛出异常,安全流程转到 ldapAuthenticationProvider。 我有一个问题,当没有连接到 AD 服务器时,我得到 CommunicationException,无法处理:

a) @ControllerAdvice@ExceptionHandler

b) AuthenticationFailureHandler

c) AuthenticationEntryPoint

我在默认 spring "error" 的控制台和回调中遇到异常。我假设它是在后台某处完成的,但我对此无能为力并找到原因。我想捕获 CommunicationException 并抛出我的 CustomException(或任何其他)来解析 javascript 客户端上的消息。我该怎么做?

安全配置:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.exceptionHandling()
            .and().authorizeRequests()
            .antMatchers("...").permitAll()
            .anyRequest().authenticated()
            .and().formLogin().loginPage("/login")
            .failureHandler(new CustomFailureHandler())
            .successHandler(new CustomSuccessHandler()).permitAll()
            .and().logout().permitAll()
            .and().exceptionHandling().authenticationEntryPoint(unauthorizedHandler);
}

@Override
protected void configure(AuthenticationManagerBuilder auth) {
    auth
            .authenticationProvider(new CustomAuthenticationProvider())
            .authenticationProvider(ldapAuthenticationProvider());
}

未经授权的处理程序:

@Component
public class UnauthorizedHandler implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request,
                        HttpServletResponse response,
                        AuthenticationException authException) throws IOException {
        String[] url = request.getRequestURL().toString().split(request.getContextPath());
        String redirect = request.getContextPath();
        if (url.length == 2) redirect = request.getContextPath() + "/?callbackUrl=" + url[1].substring(1);
        response.sendRedirect(redirectUrl);
    }
}

CustomFailureHandler:

@Component
public class CustomFailureHandler implements AuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest,
                                        HttpServletResponse httpServletResponse,
                                        AuthenticationException exception) throws IOException {
        String message = "...";

        if (exception instanceof BadCredentialsException) {
            message = "...";
        } else if (exception instanceof CommunicationException) {
            message = "...";
        }

        httpServletResponse.setCharacterEncoding("UTF-8");
        httpServletResponse.setStatus(500);
        httpServletResponse.getWriter().print(message);
        httpServletResponse.getWriter().flush();
        httpServletResponse.getWriter().close();
    }
}

异常:

org.springframework.ldap.CommunicationException: 10.1.3.13:389; nested exception is javax.naming.CommunicationException: 10.1.3.13:389 [Root exception is java.net.ConnectException: Connection timed out: connect]
    at org.springframework.ldap.support.LdapUtils.convertLdapException(LdapUtils.java:108)
    at org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider.bindAsUser(ActiveDirectoryLdapAuthenticationProvider.java:215)
    at org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider.doAuthentication(ActiveDirectoryLdapAuthenticationProvider.java:146)
    at org.springframework.security.ldap.authentication.AbstractLdapAuthenticationProvider.authenticate(AbstractLdapAuthenticationProvider.java:85)
    at org........authenticate(ActiveDirectoryLdapAuthenticationProvider.java:29)
    at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:175)
    at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:200)
    at org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.attemptAuthentication(UsernamePasswordAuthenticationFilter.java:94)
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:212)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
    at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:116)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
    at org.springframework.security.web.csrf.CsrfFilter.doFilterInternal(CsrfFilter.java:124)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
    at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:74)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
    at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:105)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
    at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:56)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:215)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:178)
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:357)
    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:270)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:200)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:199)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:490)
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139)
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:408)
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:834)
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1417)
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.base/java.lang.Thread.run(Thread.java:835)
Caused by: javax.naming.CommunicationException: 10.1.3.13:389
    at java.naming/com.sun.jndi.ldap.Connection.<init>(Connection.java:237)
    at java.naming/com.sun.jndi.ldap.LdapClient.<init>(LdapClient.java:137)
    at java.naming/com.sun.jndi.ldap.LdapClient.getInstance(LdapClient.java:1610)
    at java.naming/com.sun.jndi.ldap.LdapCtx.connect(LdapCtx.java:2752)
    at java.naming/com.sun.jndi.ldap.LdapCtx.<init>(LdapCtx.java:320)
    at java.naming/com.sun.jndi.ldap.LdapCtxFactory.getLdapCtxFromUrl(LdapCtxFactory.java:225)
    at java.naming/com.sun.jndi.ldap.LdapCtxFactory.getUsingURL(LdapCtxFactory.java:189)
    at java.naming/com.sun.jndi.ldap.LdapCtxFactory.getUsingURLs(LdapCtxFactory.java:243)
    at java.naming/com.sun.jndi.ldap.LdapCtxFactory.getLdapCtxInstance(LdapCtxFactory.java:154)
    at java.naming/com.sun.jndi.ldap.LdapCtxFactory.getInitialContext(LdapCtxFactory.java:84)
    at java.naming/javax.naming.spi.NamingManager.getInitialContext(NamingManager.java:730)
    at java.naming/javax.naming.InitialContext.getDefaultInitCtx(InitialContext.java:305)
    at java.naming/javax.naming.InitialContext.init(InitialContext.java:236)
    at java.naming/javax.naming.ldap.InitialLdapContext.<init>(InitialLdapContext.java:154)
    at org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider$ContextFactory.createContext(ActiveDirectoryLdapAuthenticationProvider.java:426)
    at org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider.bindAsUser(ActiveDirectoryLdapAuthenticationProvider.java:206)
    ... 47 common frames omitted
Caused by: java.net.ConnectException: Connection timed out: connect
    at java.base/java.net.PlainSocketImpl.connect0(Native Method)
    at java.base/java.net.PlainSocketImpl.socketConnect(PlainSocketImpl.java:101)
    at java.base/java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:399)
    at java.base/java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:242)
    at java.base/java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:224)
    at java.base/java.net.SocksSocketImpl.connect(SocksSocketImpl.java:403)
    at java.base/java.net.Socket.connect(Socket.java:591)
    at java.base/java.net.Socket.connect(Socket.java:540)
    at java.base/java.net.Socket.<init>(Socket.java:436)
    at java.base/java.net.Socket.<init>(Socket.java:213)
    at java.naming/com.sun.jndi.ldap.Connection.createSocket(Connection.java:330)
    at java.naming/com.sun.jndi.ldap.Connection.<init>(Connection.java:216)
    ... 62 common frames omitted

CommunicationException 不是 AuthenticationException 的实例,所以行

if (exception instanceof CommunicationException) {

永远不会过去。

捕获此异常的一种简单方法是将 LDAP 身份验证提供程序包装在您自己的提供程序中:

public class MyLdapAuthenticationProvider implements AuthenticationProvider {
    private final ActiveDirectoryLdapAuthenticationProvider delegate;

    public MyLdapAuthenticationProvider(
        ActiveDirectoryLdapAuthenticationProvider delegate) {
        this.delegate = delegate;
    }

    public Authentication authenticate(Authentication authentication) {
        try {
            return this.delegate.authenticate(authentication);
        } catch (CommunicationException e) {
            throw new InternalAuthenticationServiceException(e);
        }
    }
}

然后:

@Override
protected void configure(AuthenticationManagerBuilder auth) {
    auth
        .authenticationProvider(new CustomAuthenticationProvider())
        .authenticationProvider(
            new MyLdapAuthenticationProvider(ldapAuthenticationProvider()));
}

话虽如此,Spring 安全部门有一张公开票,因此 CommunicationException is correctly wrapped in an InternalAuthenticationServiceException。您可能会考虑贡献一个修复程序,这样您就不需要委托身份验证提供程序了。