• Spring Boot分布式系统实践【扩展1】shiro+redis实现session共享、simplesession反序列化失败的问题定位及反思改进


    前言

    调试之前请先关闭Favicon配置

    spring:
        favicon:
          enabled: false
    

    不然会发现有2个请求(如果用nginx+ 浏览器调试的话)
    Image.png

    序列化工具类【fastjson版本1.2.37】

    
        public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
    
    
        private Class<T> clazz;
    
    
        public FastJson2JsonRedisSerializer(Class<T> clazz) {
            super();
            this.clazz = clazz;
        }
    
    
        @Override
        public byte[] serialize(T t) throws SerializationException {
            if (t == null) {
                return new byte[0];
            }
            return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
        }
    
    
        @Override
        public T deserialize(byte[] bytes) throws SerializationException {
            if (bytes == null || bytes.length <= 0) {
                return null;
            }
            String str = new String(bytes, DEFAULT_CHARSET);
    
    
            return (T) JSON.parseObject(str, clazz);
    
    
        }
    }
    
    

    org.apache.shiro.session.mgt.SimpleSession存储到redis中会发现已经丢失了所有属性

    Image [1].png

    查看SimpleSession源码:

    public class SimpleSession implements ValidatingSession, Serializable {
    
        private transient Serializable id;
        private transient Date startTimestamp;
        private transient Date stopTimestamp;
        private transient Date lastAccessTime;
        private transient long timeout;
        private transient boolean expired;
        private transient String host;
        private transient Map<Object, Object> attributes;
    /* Serializes this object to the specified output stream for JDK Serialization.
    *
    * @param out output stream used for Object serialization.
    * @throws IOException if any of this object's fields cannot be written to the stream.
    * @since 1.0
    */
    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();
        short alteredFieldsBitMask = getAlteredFieldsBitMask();
        out.writeShort(alteredFieldsBitMask);
        if (id != null) {
            out.writeObject(id);
        }
        if (startTimestamp != null) {
            out.writeObject(startTimestamp);
        }
        if (stopTimestamp != null) {
            out.writeObject(stopTimestamp);
        }
        if (lastAccessTime != null) {
            out.writeObject(lastAccessTime);
        }
        if (timeout != 0l) {
            out.writeLong(timeout);
        }
        if (expired) {
            out.writeBoolean(expired);
        }
        if (host != null) {
            out.writeUTF(host);
        }
        if (!CollectionUtils.isEmpty(attributes)) {
            out.writeObject(attributes);
        }
    }
    
    
    /*
    * Reconstitutes this object based on the specified InputStream for JDK Serialization.
    *
    * @param in the input stream to use for reading data to populate this object.
    * @throws IOException            if the input stream cannot be used.
    * @throws ClassNotFoundException if a required class needed for instantiation is not available in the present JVM
    * @since 1.0
    */
    @SuppressWarnings({"unchecked"})
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
    
    
    

    发现transient修饰,所以Fastjson不会对这些transient属性进行持久化,所以有了方案二,重写可以json序列化的对象
    同时发现有writeObject()方法写着“ Serializes this object to the specified output stream for JDK Serialization.”,
    所以有了方案一,修改序列化工具( 默认使用JdkSerializationRedisSerializer,这个序列化模式会将value序列化成字节码)
    问题我们就好对症下药了

    方案一:

    修改序列化工具类 (这个方式其实有问题

    public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T> {
        private Class<T> clazz;
        public FastJson2JsonRedisSerializer(Class<T> clazz) {
            super();
            this.clazz = clazz;
        }
        @Override
        public byte[] serialize(T t) {
            return ObjectUtils.serialize(t);
        }
        @Override
        public T deserialize(byte[] bytes) {
            return (T) ObjectUtils.unserialize(bytes);
        }
    }
    
    
    

    ObjectUtils的方法如下:

    /**
    * 序列化对象
    * @param object
    * @return
    */
    public static byte[] serialize(Object object) {
       ObjectOutputStream oos = null;
       ByteArrayOutputStream baos = null;
       try {
          if (object != null){
             baos = new ByteArrayOutputStream();
             oos = new ObjectOutputStream(baos);
             oos.writeObject(object);
             return baos.toByteArray();
          }
       } catch (Exception e) {
          e.printStackTrace();
       }
       return null;
    }
    
    
    /**
    * 反序列化对象
    * @param bytes
    * @return
    */
    public static Object unserialize(byte[] bytes) {
       ByteArrayInputStream bais = null;
       try {
          if (bytes != null && bytes.length > 0){
             bais = new ByteArrayInputStream(bytes);
             ObjectInputStream ois = new ObjectInputStream(bais);
             return ois.readObject();
          }
       } catch (Exception e) {
          e.printStackTrace();
       }
       return null;
    }
    
    
    
    

    此方案会严重依赖对象class,如果反序列化时class对象不存在则会报错 修改为: JdkSerializationRedisSerializer

    Image [2].png

    方案二:

    继承SimpleSession并重写
    让相关的字段可以被序列化(不被transient修饰)
    重写之后一定要重写SessionManager里的方法

    @Override
    protected Session newSessionInstance(SessionContext context) {
    SimpleSession session = new MyRedisSession(context.getHost());
    // session.setId(IdGen.uuid());
    session.setTimeout(SessionUtils.SESSION_TIME);
    return session;
    }
    
    

    由方案二引发的另一个问题就是:

    在微服务开发过程中,为了使用方便经常会将频繁访问的信息如用户、权限等放置到SESSION中,便于服务访问,而且,微服务间为了共享SESSION,通常会使用Redis共享存储。但是这样就会有一个问题,在封装Request对象时会将当前SESSION中所有属性对象反序列化,反序列化都成功以后,将SESSION对象生成。如果有一个微服务将本地的自定义Bean对象放置到SESSION中,则其他微服务都将出现反序列化失败,请求异常,服务将不能够使用了,这是一个灾难性问题。

    以下是为了解决下面问题提出来的一种思路。

    反序列化失败在于Attribute中添加了复杂对象,由此推出以下解决方案:

    1. 将复杂对象的(即非基本类型的)Key进行toString转换(转换之后再MD5缩减字符串,或者用类名代替)
    2. 将复杂对象的(即非基本类型的)Value进行JSON化(不使用不转换的懒加载模式)

    注意: 日期对象的处理(单独处理)

      /**
         * 通过类型转换,将String反序列化成对象
         * @param key
         * @param value
         * @return
         */
        public Object getObjectValue(String key,String value){
            if(key == null || value == null){
               return null;
            }
            String clz = key.replace(FLAG_STR,"");
            try {
               Class aClass = Class.forName(clz);
               if(aClass.equals(Date.class)){
                   return DateUtils.parseDate(value);
               }
              return   JSONObject.parseObject(value,aClass);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
    //        如果反序列化失败就进行json化处理
            return JSONObject.parseObject(value);
        }
    
    
    

    经过如此处理可以在所有系统里共享缓存
    唯一缺点就是太复杂了,可能引起其他系统的修改导致反序列化失败(这个下次再讨论或者实验,因为有这么复杂的功夫,就可以考虑用JWT)

    还有一种方案是将复杂对象放到redis中去,实行懒加载机制(不用的复杂对象,不从redis里获取,暂未实现测试)

  • 相关阅读:
    [转] packagelock.json
    前端框架和技术
    typescript
    微信小程序登陆流程
    Introduction to my galaxy engine 4: Test on local light model
    Introduction to my galaxy engine 3: Local light model
    Introduction to my galaxy engine 5: Differed Lighting
    Introduction to my galaxy engine 2: Depth of field
    自己整理的一些国外免费3D模型网站,以后还会陆续添加
    Introduction to my galaxy engine 6: Differed Lighting 2
  • 原文地址:https://www.cnblogs.com/Halburt/p/10552582.html
Copyright © 2020-2023  润新知