We have a Spring Boot application using a Hazecast-backed Spring Session. The application authenicates with Active Directory using Spring Security. If a user attempts to log in with invalid credentials, a serialization error is thrown:
com.hazelcast.nio.serialization.HazelcastSerializationException: java.io.NotSerializableException: com.sun.jndi.ldap.LdapCtx
at com.hazelcast.nio.serialization.SerializationServiceImpl.handleException(SerializationServiceImpl.java:380)
at com.hazelcast.nio.serialization.SerializationServiceImpl.toData(SerializationServiceImpl.java:235)
at com.hazelcast.nio.serialization.SerializationServiceImpl.toData(SerializationServiceImpl.java:207)
at com.hazelcast.map.impl.MapServiceContextImpl.toData(MapServiceContextImpl.java:338)
at com.hazelcast.map.impl.proxy.MapProxySupport.toData(MapProxySupport.java:1160)
at com.hazelcast.map.impl.proxy.MapProxyImpl.put(MapProxyImpl.java:96)
at org.springframework.session.hazelcast.config.annotation.web.http.HazelcastHttpSessionConfiguration$ExpiringSessionMap.put(HazelcastHttpSessionConfiguration.java:112)
at org.springframework.session.hazelcast.config.annotation.web.http.HazelcastHttpSessionConfiguration$ExpiringSessionMap.put(HazelcastHttpSessionConfiguration.java:102)
at org.springframework.session.MapSessionRepository.save(MapSessionRepository.java:72)
at org.springframework.session.MapSessionRepository.save(MapSessionRepository.java:36)
at org.springframework.session.web.http.SessionRepositoryFilter$SessionRepositoryRequestWrapper.commitSession(SessionRepositoryFilter.java:194)
at org.springframework.session.web.http.SessionRepositoryFilter$SessionRepositoryRequestWrapper.access$100(SessionRepositoryFilter.java:170)
at org.springframework.session.web.http.SessionRepositoryFilter.doFilterInternal(SessionRepositoryFilter.java:128)
at org.springframework.session.web.http.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:65)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:240)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207)
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:121)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:240)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207)
at org.springframework.boot.actuate.autoconfigure.MetricsFilter.doFilterInternal(MetricsFilter.java:103)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:240)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:212)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:106)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:502)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:141)
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:522)
at org.apache.coyote.ajp.AbstractAjpProcessor.process(AbstractAjpProcessor.java:868)
at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:672)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1502)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.run(NioEndpoint.java:1458)
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)
This appears to be the identical to another issue (Spring Boot with Session/Redis Serialization Error with Bad Active Directory Ldap Credentials) with Redis, however there doesn't appear to be a similar mechanism to control serialization in the Hazelcast session mapping that there is for Redis in Spring Session.
We've come up with a workaround (below), but it seems less than ideal as HazelcastHttpSessionConfiguration
doesn't really seem to lend itself to extension, so it seems like there should be a cleaner way that we aren't seeing.
We are extending the HazelcastHttpSessionConfiguration
to get at the ExpiringSessionMap
to remove the LdapCtx
before serialization is attempted. This doesn't seem ideal as the HazelcastHttpSessionConfiguration
doesn't really lend it self to extension, requiring duplication of code.
Is there a better solution that we're missing?
@Configuration
public class CustomHazelcastHttpSessionMapConfiguration extends HazelcastHttpSessionConfiguration{
private String sessionMapName = "spring:session:sessions";
private int maxInactiveIntervalInSeconds = 1800;
@Bean
public SessionRepository<ExpiringSession> sessionRepository(
HazelcastInstance hazelcastInstance, SessionEntryListener sessionListener) {
super.sessionRepository(hazelcastInstance, sessionListener);
MapSessionRepository sessionRepository = new MapSessionRepository(
new CustomExpiringSessionMap(hazelcastInstance.getMap(this.sessionMapName)));
sessionRepository
.setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);
return sessionRepository;
}
@Override
public void setSessionMapName(String sessionMapName) {
this.sessionMapName = sessionMapName;
super.setSessionMapName(sessionMapName);
}
@Override
public void setMaxInactiveIntervalInSeconds(int maxInactiveIntervalInSeconds) {
this.maxInactiveIntervalInSeconds = maxInactiveIntervalInSeconds;
super.setMaxInactiveIntervalInSeconds(maxInactiveIntervalInSeconds);
}
static class CustomExpiringSessionMap implements Map<String, ExpiringSession> {
private IMap<String, ExpiringSession> delegate;
CustomExpiringSessionMap(IMap<String, ExpiringSession> delegate) {
this.delegate = delegate;
}
public ExpiringSession put(String key, ExpiringSession value) {
if (value == null) {
return this.delegate.put(key, value);
}
for (String attrName : value.getAttributeNames()) {
Object attrVal = value.getAttribute(attrName);
// Don't serialize LdapCtx in a BadCredentialsException
if (attrVal instanceof BadCredentialsException &&
((BadCredentialsException) attrVal).getCause() != null &&
((BadCredentialsException) attrVal).getCause() instanceof ActiveDirectoryAuthenticationException &&
((BadCredentialsException) attrVal).getCause().getCause() != null &&
((BadCredentialsException) attrVal).getCause().getCause() instanceof javax.naming.AuthenticationException) {
((javax.naming.AuthenticationException) ((BadCredentialsException) attrVal).getCause().getCause()).setResolvedObj(null);
}
}
return this.delegate.put(key, value, value.getMaxInactiveIntervalInSeconds(),
TimeUnit.SECONDS);
}
/*... copy and paste of the rest of ExpiringSessionMap */
}
}
You should configure a custom serialization for object(s) you're having issues with.
This way you would address your problem in Hazelcast configuration without extending/duplicating Spring Session's Hazelcast configuration.