记spring-security升级,引发的redis反序列化不一致问题

发布时间 2023-04-07 13:49:20作者: 程序员CRUD

问题解决参考文章如下:

问题复现

由于一些原因,登录的token由旧版本的微服务存入的redis,另一个新版本的微服务需要取出数据校验
springboot 版本升级 导致spring-security-core升级 redis反序列化版本不一致
依赖版本变化
springboot 2.0.9.RELEASE => 2.6.6.RELEASE
spring-security-core 5.0.12.RELEASE => 5.0.12.RELEASE

报错如下
org.springframework.data.redis.serializer.SerializationException: Cannot deserialize; nested exception is org.springframework.core.serializer.support.SerializationFailedException: Failed to deserialize payload. Is the byte array a result of corresponding serialization for DefaultDeserializer?; nested exception is java.io.InvalidClassException: org.springframework.security.authentication.UsernamePasswordAuthenticationToken; local class incompatible: stream classdesc serialVersionUID = 500, local class serialVersionUID = 560
	at org.springframework.data.redis.serializer.JdkSerializationRedisSerializer.deserialize(JdkSerializationRedisSerializer.java:84)
	at org.springframework.data.redis.core.AbstractOperations.deserializeHashValue(AbstractOperations.java:380)
	at org.springframework.data.redis.core.AbstractOperations.deserializeHashMap(AbstractOperations.java:324)
	at org.springframework.data.redis.core.DefaultHashOperations.entries(DefaultHashOperations.java:309)
	at org.springframework.data.redis.core.DefaultBoundHashOperations.entries(DefaultBoundHashOperations.java:223)
	at org.springframework.session.data.redis.RedisIndexedSessionRepository.getSession(RedisIndexedSessionRepository.java:457)
	at org.springframework.session.data.redis.RedisIndexedSessionRepository.findById(RedisIndexedSessionRepository.java:429)
	at org.springframework.session.data.redis.RedisIndexedSessionRepository.findById(RedisIndexedSessionRepository.java:251)
	at org.springframework.session.web.http.SessionRepositoryFilter$SessionRepositoryRequestWrapper.getRequestedSession(SessionRepositoryFilter.java:356)
	at org.springframework.session.web.http.SessionRepositoryFilter$SessionRepositoryRequestWrapper.getSession(SessionRepositoryFilter.java:290)
	at org.springframework.session.web.http.SessionRepositoryFilter$SessionRepositoryRequestWrapper.getSession(SessionRepositoryFilter.java:193)
	at javax.servlet.http.HttpServletRequestWrapper.getSession(HttpServletRequestWrapper.java:244)
	at org.springframework.security.web.context.HttpSessionSecurityContextRepository.loadContext(HttpSessionSecurityContextRepository.java:118)
	at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:98)
	at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:80)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336)
	at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:55)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336)
	at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:211)
	at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:183)
	at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:354)
	at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:267)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
	at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
	at org.springframework.security.oauth2.client.filter.OAuth2ClientContextFilter.doFilter(OAuth2ClientContextFilter.java:60)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
	at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
	at org.springframework.session.web.http.SessionRepositoryFilter.doFilterInternal(SessionRepositoryFilter.java:142)
	at org.springframework.session.web.http.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:82)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
	at org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter.doFilterInternal(WebMvcMetricsFilter.java:96)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
	at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
	at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:197)
	at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97)
	at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:541)
	at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:135)
	at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
	at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78)
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:360)
	at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:399)
	at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)
	at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:889)
	at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1743)
	at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
	at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)
	at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
	at java.base/java.lang.Thread.run(Thread.java:834)
解决思路

更换redis对session数据反序列化策略
由于security-core的org.springframework.security.core.context里的 SecurityContextImpl在不同版本下serialVersionUID不一致,因此参考第二篇引用文章,通过反射将序列化版本进行修改,使得redis锁使用的JdkSerializationRedisSerializer可以顺利序列化

最终代码

SpringSessionConfig.java



import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.ConfigurableObjectInputStream;
import org.springframework.core.NestedIOException;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import org.springframework.lang.Nullable;

import java.io.ByteArrayInputStream;


@Configuration
public class SpringSessionConfig implements BeanClassLoaderAware {

  private ClassLoader loader;


  @Bean("springSessionDefaultRedisSerializer")
  //@ConditionalOnProperty(prefix = "spring.session", name = "store-type", havingValue = "redis")
  public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
    return new JdkSerializationRedisSerializer(loader){
      @Override
      public Object deserialize(@Nullable byte[] bytes) {
        if (bytes == null || bytes.length == 0) {
          return null;
        } else {

          try {
            // 注意这里使用我们重写的 CompatibleInputStream
            ConfigurableObjectInputStream objectInputStream = new CompatibleInputStream(new ByteArrayInputStream(bytes), loader);
            try {
              return objectInputStream.readObject();
            } catch (ClassNotFoundException var4) {
              throw new NestedIOException("Failed to deserialize object type", var4);
            }finally {
              objectInputStream.close();
            }
          } catch (Exception e) {
            throw new SerializationException("Cannot deserialize", e);
          }
        }
      }
    };
  }
 
  @Override
  public void setBeanClassLoader(ClassLoader classLoader) {
    this.loader = classLoader;
  }
}

CompatibleInputStream.java


import lombok.extern.slf4j.Slf4j;
import org.springframework.core.ConfigurableObjectInputStream;

import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectStreamClass;
import java.lang.reflect.Field;

@Slf4j
public class CompatibleInputStream  extends ConfigurableObjectInputStream {

     public CompatibleInputStream(InputStream in, ClassLoader classLoader) throws IOException {
          super(in, classLoader);
     }

     public CompatibleInputStream(InputStream in, ClassLoader classLoader, boolean acceptProxyClasses) throws IOException {
          super(in, classLoader, acceptProxyClasses);
     }

     @Override
     protected ObjectStreamClass readClassDescriptor() throws IOException, ClassNotFoundException {
          ObjectStreamClass resultClassDescriptor = super.readClassDescriptor();
          Class localClass;
          try {
               localClass = Class.forName(resultClassDescriptor.getName());
          } catch (ClassNotFoundException e) {
               log.error("No local class for " + resultClassDescriptor.getName(), e);
               return resultClassDescriptor;
          }
          ObjectStreamClass localClassDescriptor = ObjectStreamClass.lookup(localClass);
          if (localClassDescriptor != null) {
               final long localSUID = localClassDescriptor.getSerialVersionUID();
               final long streamSUID = resultClassDescriptor.getSerialVersionUID();
               if (streamSUID != localSUID) {
                    log.debug("streamSUID = {} localSUID = {}", streamSUID, localSUID);
                    // 注意 这里是关键 通过反射强制修改 ObjectStreamClass
                    // 的 suid 属性
                    try {
                         Field field = resultClassDescriptor.getClass().getDeclaredField("suid");
                         field.setAccessible(true);
                         field.set(resultClassDescriptor, localSUID);
                    } catch (Exception e) {
                         e.printStackTrace();
                    }
                    // resultClassDescriptor = localClassDescriptor; // Use local class descriptor for deserialization*/
               }
          }
          return resultClassDescriptor;

     }
}