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里獲取,暫未實現測試)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM