Spring Boot分布式系统实践【基础模块构建3.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);


    }
}

最新发现 2019年8月6日17:59:38

如果复杂对象A里包含List<另一个对象B>属性时,在A序列化之后 B被序列化之后无@type属性,所以反序列化是B也就成了JsonObject了。

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里获取,暂未实现测试)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,470评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,393评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,577评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,176评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,189评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,155评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,041评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,903评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,319评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,539评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,703评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,417评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,013评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,664评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,818评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,711评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,601评论 2 353

推荐阅读更多精彩内容