單例模式的攻擊之序列化與反序列化

在單例模式這塊捉捅,我們花了幾個(gè)篇幅來(lái)講了里面的道道撤防,使用了幾種方式來(lái)構(gòu)建了看似無(wú)懈可擊的單例。但是真的無(wú)懈可擊嗎棒口?下面幾篇文章寄月,我們來(lái)聊聊對(duì)單例模式的攻擊以及該如何防御這些攻擊。

破壞單例的可能性有哪些

我們知道要破壞單例无牵,則必須創(chuàng)建對(duì)象漾肮,那么我們順著這個(gè)思路走,創(chuàng)建對(duì)象的方式無(wú)非就是new合敦,clone初橘,反序列化验游,以及反射充岛。

  • new
    單例模式的首要條件就是構(gòu)造方法私有化,所以new這種方式去破壞單例的可能性是不存在的(在保障線程安全的情況下)
  • clone
    要調(diào)用clone方法耕蝉,那么必須實(shí)現(xiàn)Cloneable接口崔梗,但是單例模式是不能實(shí)現(xiàn)這個(gè)接口的,因此排除這種可能性
  • 反序列化
    這個(gè)正是本篇文章要討論的問(wèn)題垒在,下面詳細(xì)進(jìn)行分析蒜魄。
  • 反射
    且移步下篇文章單例模式的攻擊之反射

序列化攻擊

為方便測(cè)試,我們寫(xiě)個(gè)最簡(jiǎn)單的餓漢式單例模式代碼场躯,便于以后測(cè)試

/**
 * @Author: ming.wang
 * @Date: 2019/2/20 17:04
 * @Description: 為了演示序列化實(shí)現(xiàn)Serializable 接口
 */
public class HungrySingleton implements Serializable {
    private final static HungrySingleton instance;
    static {
        instance=new HungrySingleton();
    }
    private HungrySingleton() {}
    public static HungrySingleton getInstance(){
        return instance;
    }
}

建立一個(gè)DestroySingletonTest 測(cè)試類谈为,代碼如下,簡(jiǎn)單來(lái)說(shuō)就是采用序列化來(lái)破壞單例

/**
 * @Author: ming.wang
 * @Date: 2019/2/21 16:05
 * @Description: 使用反射或反序列化來(lái)破壞單例
 */
public class DestroySingletonTest {

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        //序列化方式破壞單例   測(cè)試
        serializeDestroyMethod();
    }

    private static void serializeDestroyMethod() throws IOException, ClassNotFoundException {
        HungrySingleton hungrySingleton=null;
        HungrySingleton hungrySingleton_new=null;

        hungrySingleton=HungrySingleton.getInstance();

        ByteArrayOutputStream bos=new ByteArrayOutputStream();
        ObjectOutputStream oos=new ObjectOutputStream(bos);
        oos.writeObject(hungrySingleton);

        ByteArrayInputStream bis=new ByteArrayInputStream(bos.toByteArray());
        ObjectInputStream ois=new ObjectInputStream(bis);
        hungrySingleton_new= (HungrySingleton) ois.readObject();

        System.out.println(hungrySingleton==hungrySingleton_new);
    }
}

我們運(yùn)行程序踢关,結(jié)果打印false伞鲫,顯然單例被破壞了。
那么签舞,我們有什么解決辦法能抵擋這種序列化破壞呢秕脓?
下面我們對(duì)HungrySingleton進(jìn)行小小的修改 ,添加一個(gè)方法readResolve()

public class HungrySingleton implements Serializable {
    private final static HungrySingleton instance;
     ...
     ...
    private Object readResolve()
    {
        return instance;
    }
}

我們重新運(yùn)行程序儒搭,發(fā)現(xiàn)此時(shí)打印的結(jié)果是true吠架。顯然,這個(gè)小小的改動(dòng)幫我們抵御了序列化對(duì)單例的破壞搂鲫。
下面我們就簡(jiǎn)單分析一下傍药,為什么這個(gè)改動(dòng)能夠起到扭轉(zhuǎn)乾坤的作用。
我們進(jìn)入ObjectInputStream.readObject()這個(gè)方法體內(nèi)

....
public final Object readObject()
        throws IOException, ClassNotFoundException
    {
        if (enableOverride) {
            return readObjectOverride();
        }

        // if nested read, passHandle contains handle of enclosing object
        int outerHandle = passHandle;
        try {
            Object obj = readObject0(false);
            handles.markDependency(outerHandle, passHandle);
            ClassNotFoundException ex = handles.lookupException(passHandle);
            if (ex != null) {
                throw ex;
            }
            if (depth == 0) {
                vlist.doCallbacks();
            }
            return obj;
        } finally {
            passHandle = outerHandle;
            if (closed && depth == 0) {
                clear();
            }
        }
    }

.....

繼續(xù)跟進(jìn) Object obj = readObject0(false);這個(gè)方法

....
  /**
     * Underlying readObject implementation.
     */
    private Object readObject0(boolean unshared) throws IOException {
        boolean oldMode = bin.getBlockDataMode();
        if (oldMode) {
            int remain = bin.currentBlockRemaining();
            if (remain > 0) {
                throw new OptionalDataException(remain);
            } else if (defaultDataEnd) {
                /*
                 * Fix for 4360508: stream is currently at the end of a field
                 * value block written via default serialization; since there
                 * is no terminating TC_ENDBLOCKDATA tag, simulate
                 * end-of-custom-data behavior explicitly.
                 */
                throw new OptionalDataException(true);
            }
            bin.setBlockDataMode(false);
        }

        byte tc;
        while ((tc = bin.peekByte()) == TC_RESET) {
            bin.readByte();
            handleReset();
        }

        depth++;
        totalObjectRefs++;
        try {
            switch (tc) {
                case TC_NULL:
                    return readNull();

                case TC_REFERENCE:
                    return readHandle(unshared);

                case TC_CLASS:
                    return readClass(unshared);

                case TC_CLASSDESC:
                case TC_PROXYCLASSDESC:
                    return readClassDesc(unshared);

                case TC_STRING:
                case TC_LONGSTRING:
                    return checkResolve(readString(unshared));

                case TC_ARRAY:
                    return checkResolve(readArray(unshared));

                case TC_ENUM:
                    return checkResolve(readEnum(unshared));

                case TC_OBJECT:
                    return checkResolve(readOrdinaryObject(unshared));

                case TC_EXCEPTION:
                    IOException ex = readFatalException();
                    throw new WriteAbortedException("writing aborted", ex);

                case TC_BLOCKDATA:
                case TC_BLOCKDATALONG:
                    if (oldMode) {
                        bin.setBlockDataMode(true);
                        bin.peek();             // force header read
                        throw new OptionalDataException(
                            bin.currentBlockRemaining());
                    } else {
                        throw new StreamCorruptedException(
                            "unexpected block data");
                    }

                case TC_ENDBLOCKDATA:
                    if (oldMode) {
                        throw new OptionalDataException(true);
                    } else {
                        throw new StreamCorruptedException(
                            "unexpected end of block data");
                    }

                default:
                    throw new StreamCorruptedException(
                        String.format("invalid type code: %02X", tc));
            }
        } finally {
            depth--;
            bin.setBlockDataMode(oldMode);
        }
    }
....

我們看方法體中的switch 分支,我們會(huì)走 case TC_OBJECT:這個(gè)分支拐辽,那么我們繼續(xù)跟進(jìn)褪秀,在這個(gè)分支下調(diào)用了checkResolve(readOrdinaryObject(unshared));,我們著重看readOrdinaryObject(unshared)這個(gè)方法薛训,跟進(jìn)去瞅瞅

....
 /**
     * Reads and returns "ordinary" (i.e., not a String, Class,
     * ObjectStreamClass, array, or enum constant) object, or null if object's
     * class is unresolvable (in which case a ClassNotFoundException will be
     * associated with object's handle).  Sets passHandle to object's assigned
     * handle.
     */
    private Object readOrdinaryObject(boolean unshared)
        throws IOException
    {
        if (bin.readByte() != TC_OBJECT) {
            throw new InternalError();
        }

        ObjectStreamClass desc = readClassDesc(false);
        desc.checkDeserialize();

        Class<?> cl = desc.forClass();
        if (cl == String.class || cl == Class.class
                || cl == ObjectStreamClass.class) {
            throw new InvalidClassException("invalid class descriptor");
        }

        Object obj;
        try {
            obj = desc.isInstantiable() ? desc.newInstance() : null;
        } catch (Exception ex) {
            throw (IOException) new InvalidClassException(
                desc.forClass().getName(),
                "unable to create instance").initCause(ex);
        }

        passHandle = handles.assign(unshared ? unsharedMarker : obj);
        ClassNotFoundException resolveEx = desc.getResolveException();
        if (resolveEx != null) {
            handles.markException(passHandle, resolveEx);
        }

        if (desc.isExternalizable()) {
            readExternalData((Externalizable) obj, desc);
        } else {
            readSerialData(obj, desc);
        }

        handles.finish(passHandle);

        if (obj != null &&
            handles.lookupException(passHandle) == null &&
            desc.hasReadResolveMethod())
        {
            Object rep = desc.invokeReadResolve(obj);
            if (unshared && rep.getClass().isArray()) {
                rep = cloneArray(rep);
            }
            if (rep != obj) {
                // Filter the replacement object
                if (rep != null) {
                    if (rep.getClass().isArray()) {
                        filterCheck(rep.getClass(), Array.getLength(rep));
                    } else {
                        filterCheck(rep.getClass(), -1);
                    }
                }
                handles.setObject(passHandle, obj = rep);
            }
        }

        return obj;
    }

...

我們抓重點(diǎn)媒吗,看下這句代碼 obj = desc.isInstantiable() ? desc.newInstance() : null;,我們跟進(jìn) desc.isInstantiable()這個(gè)方法,看方法注釋乙埃,簡(jiǎn)單來(lái)說(shuō)一個(gè)類是serializable/externalizable的實(shí)例則返回true闸英。

 /**
     * Returns true if represented class is serializable/externalizable and can
     * be instantiated by the serialization runtime--i.e., if it is
     * externalizable and defines a public no-arg constructor, or if it is
     * non-externalizable and its first non-serializable superclass defines an
     * accessible no-arg constructor.  Otherwise, returns false.
     */
    boolean isInstantiable() {
        requireInitialized();
        return (cons != null);
    }

為true的話,那么obj=desc.newInstance()介袜,通過(guò)查看我們知道這個(gè)最終是調(diào)用的反射機(jī)制生成的新的實(shí)例甫何。截止到此時(shí),我們可以解釋在未做改動(dòng)之前遇伞,生成了新的實(shí)例辙喂,單例被破壞的真正原因了。
我們接著分析鸠珠,既然改動(dòng)之后巍耗,成功抵御了單例的破壞,那么后面肯定有相應(yīng)的代碼實(shí)現(xiàn)渐排。我們繼續(xù)看readOrdinaryObject這個(gè)方法炬太。往后看,我們發(fā)現(xiàn)了這樣一句代碼驯耻,

...
if (obj != null &&
            handles.lookupException(passHandle) == null &&
            desc.hasReadResolveMethod())
        {
            Object rep = desc.invokeReadResolve(obj);
...
        }
....

接著看desc.hasReadResolveMethod()這個(gè)方法,簡(jiǎn)單來(lái)說(shuō)亲族,就是該類是serializable or externalizable的實(shí)例,并且定義了符合要求的readResolve 方法可缚,則返回true

...
 /**
     * Returns true if represented class is serializable or externalizable and
     * defines a conformant readResolve method.  Otherwise, returns false.
     */
    boolean hasReadResolveMethod() {
        requireInitialized();
        return (readResolveMethod != null);
    }
...

什么是符合要求的readResolve 方法呢霎迫?我們搜索readResolve 會(huì)發(fā)現(xiàn)readResolveMethod = getInheritableMethod( cl, "readResolve", null, Object.class);,符合要求的方法是方法名是readResolve,返回值是Object帘靡。
顯然定義了符合要求的方法之后知给,再執(zhí)行Object rep = desc.invokeReadResolve(obj);反射調(diào)用該方法。具體的就是

 private Object readResolve()
    {
        return instance;
    }

這樣我們就得到了原來(lái)的實(shí)例而不是新的實(shí)例2饽炼鞠!

結(jié)論

對(duì)于序列化破壞單例,我們的解決方案就是轰胁,在單例代碼中谒主,增加以下代碼

private Object readResolve()
    {
        return instance;
    }
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市赃阀,隨后出現(xiàn)的幾起案子霎肯,更是在濱河造成了極大的恐慌擎颖,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,651評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件观游,死亡現(xiàn)場(chǎng)離奇詭異搂捧,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)懂缕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,468評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)允跑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人搪柑,你說(shuō)我怎么就攤上這事聋丝。” “怎么了工碾?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,931評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵弱睦,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我渊额,道長(zhǎng)况木,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,218評(píng)論 1 292
  • 正文 為了忘掉前任旬迹,我火速辦了婚禮火惊,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘舱权。我一直安慰自己矗晃,他們只是感情好仑嗅,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,234評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布宴倍。 她就那樣靜靜地躺著,像睡著了一般仓技。 火紅的嫁衣襯著肌膚如雪鸵贬。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,198評(píng)論 1 299
  • 那天脖捻,我揣著相機(jī)與錄音阔逼,去河邊找鬼。 笑死地沮,一個(gè)胖子當(dāng)著我的面吹牛嗜浮,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播摩疑,決...
    沈念sama閱讀 40,084評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼危融,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了雷袋?” 一聲冷哼從身側(cè)響起吉殃,我...
    開(kāi)封第一講書(shū)人閱讀 38,926評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后蛋勺,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體瓦灶,經(jīng)...
    沈念sama閱讀 45,341評(píng)論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,563評(píng)論 2 333
  • 正文 我和宋清朗相戀三年抱完,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了贼陶。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,731評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡巧娱,死狀恐怖每界,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情家卖,我是刑警寧澤眨层,帶...
    沈念sama閱讀 35,430評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站上荡,受9級(jí)特大地震影響趴樱,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜酪捡,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,036評(píng)論 3 326
  • 文/蒙蒙 一叁征、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧逛薇,春花似錦捺疼、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,676評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至呢袱,卻和暖如春官扣,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背羞福。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,829評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工惕蹄, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人治专。 一個(gè)月前我還...
    沈念sama閱讀 47,743評(píng)論 2 368
  • 正文 我出身青樓卖陵,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親张峰。 傳聞我的和親對(duì)象是個(gè)殘疾皇子泪蔫,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,629評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容