序列化——对于实例控制,枚举类型优先于readResolve-创新互联

普通单例模式的漏洞

第3条讲述了 Singleton 模式,并且给出了以下这个 Singleton 类的示例。

成都创新互联专注于企业全网营销推广、网站重做改版、兰山网站定制设计、自适应品牌网站建设、H5开发商城系统网站开发、集团公司官网建设、成都外贸网站建设、高端网站制作、响应式网页设计等建站业务,价格优惠性价比高,为兰山等各大城市提供网站开发制作服务。

这个类限制了对其构造器的访问,确保永远只创建一个实例:

public class Singer{
    public static final Singer INSTANCE = new Singer();
    private Singer(){}

    public static Singer getInstance(){
        return INSTANCE;
    }
}

但如果在这个类实现序列化,这种方式就不能保证安全了。序列化可以轻松突破这种机制,甚至,这种错误是在无意间的,并不是蓄意破坏。之前曾说过,序列化是一种独立于构造器的创建对象的机制,或者你可以变相地认为序列化是一种以 byte[] 为参数的隐形构造器。

演示:

public class Singer implements Serializable {
    public static final Singer INSTANCE = new Singer();
    private Singer(){}

    public static Singer getInstance(){
        return INSTANCE;
    }

    private Object readResolve(){
        return INSTANCE;
    }
}
public class Client {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Singer instance = Singer.getInstance();
        //序列化
        File file = new File("C:\\Users\\admin\\Desktop\\file_upload\\1.txt");
        ser(instance,file);

        //反序列化
        byte[] bytes = readBytes(file);
        Singer instance2 = deSer(bytes);

        //比较两个对象是否相同
        System.out.println(instance == instance2);
    }

    static void ser(Singer s,File file) throws IOException {
        FileOutputStream fos = new FileOutputStream(file);
        ObjectOutputStream os = new ObjectOutputStream(fos);
        os.writeObject(s);
        os.close();
    }

    static Singer deSer(byte[] bytes) throws IOException, ClassNotFoundException {
        ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bytes));
        Singer p = (Singer) in.readObject();
        in.close();
        return p;
    }

    static byte[] readBytes(File file) throws IOException {
        FileInputStream in = null;
        try {
            in =new FileInputStream(file);
            //当文件没有结束时,每次读取一个字节显示
            byte[] data=new byte[in.available()];
            in.read(data);
            in.close();
            return data;
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            in.close(); //关闭流
        }
        return null;
    }
}

false

可以看出,反序列化的对象和单例获得的对象并不相同,从而导致单例模式失效


使用 readResole 方法

readResole 特性允许你用 readObject 创建的实例代替另一个实例。对于一个正在被反序列化的对象,如果它的类定义了一个 readResolve 方法,并且具备正确的声明,那么在反序列化之后,新建对象上的 readResolve 方法就会被调用。然后,该方法返回的对象应用将被返回,取代新建的对象。在这个特性的绝大多数用法中,指向新建对象的引用不需要再被保留,因此立即成为垃圾回收的对象。

比如,使用以下的方法保证单例

private Object readResolve(){
    return INSTANCE;
}

该方法忽略了被反序列化的对象,只返回该类初始化时创建的那个特殊的实例。因此,Singer 实例的序列化形式并不需要包含任何实际的数据;所有的实例域都应该声明为 transient。


readResole 方法的不足 

事实上,如果依赖 readResolve 进行实例控制,带有对象引用类型的所有实例域则都必须声明为 transient。否则,攻击者依然可以想办法在 readResolve 运行之前,获取反序列化的对象引用,得到一个单例之外的“副本”,类似于上一条中提到的 MutablePeriod攻击。

这种攻击有点复杂,但背后的思想却很简单。如果 Singleton 包含一个非 transient 对象引用域,这个域的内容就可以在 Singleton 的 readResolve 方法运行之前被反序列化。当对象引用域的内容被反序列化时,它就允许一个精心制作的流“盗用”指向最初被反序列化的 Singleton 引用。

实例演示:

import java.io.Serializable;
import java.util.Arrays;

public class Elvis implements Serializable {
    public static final Elvis INSTANCE = new Elvis();
    private Elvis(){}
    private String[] favoriteSongs = {"红颜如霜","发如雪"};
    public void printFavorites(){
        System.out.println(Arrays.toString(favoriteSongs));
    }

    private Object readResolve(){
        return INSTANCE;
    }

}
import java.io.Serializable;

public class ElvisStealer implements Serializable {

    static Elvis impersonator;
    private Elvis payload;

    private Object readResolve(){
        impersonator = payload;
        return new String[]{"发如霜"};
    }
    private static final long serialVersionUID = 0;

}
import lombok.SneakyThrows;
import java.io.ByteArrayInputStream;
import java.io.ObjectInputStream;

public class ElvisImpersonator {
    private static final byte[] bytes = new byte[]{
            (byte) 0xac, (byte) 0xed,0x00,0x05,0x73,0x72,0x00,0x05,
            0x45,0x6c,0x76,0x69,0x73, (byte) 0x84, (byte) 0xe6,
            (byte) 0x93,0x33, (byte) 0xc3, (byte) 0xf4, (byte) 0x8b,
            0x32,0x02,0x00,0x01,0x4c,0x00,0x0d,0x66,0x61,0x76,
            0x6f,0x72,0x69,0x74,0x65,0x53,0x6f,0x6e,0x67,0x73,
            0x74,0x00,0x12,0x4c,0x6a,0x61,0x76,0x61,0x2f,0x6c,
            0x61,0x6e,0x67,0x2f,0x4f,0x62,0x6a,0x65,0x63,0x74,
            0x3b,0x78,0x70,0x73,0x72,0x00,0x0c,0x45,0x6c,0x76,
            0x69,0x73,0x53,0x74,0x65,0x61,0x6c,0x65,0x72,0x00,
            0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x01,
            0x4c,0x00,0x07,0x70,0x61,0x79,0x6c,0x6f,0x61,0x64,
            0x74,0x00,0x07,0x4c,0x45,0x6c,0x76,0x69,0x73,0x3b,
            0x78,0x70,0x71,0x00,0x7e,0x00,0x02
    };

    public static void main(String[] args) {
        Elvis elvis = deser(bytes);
        Elvis impersonator = ElvisStealer.impersonator;
        elvis.printFavorites();
        impersonator.printFavorites();
    }

    @SneakyThrows
    static Elvis deser(byte[] bytes) {
        ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bytes));
        Elvis p = (Elvis) in.readObject();
        in.close();
        return p;
    }
}

通过将 favorites 声明为 transient 可以解决这个问题。但如果确实要避免出现这种错误,最好的还是使用 枚举来解决这种问题。

正如之前所提到过的:自从 jdk1.5之后,单例的最佳实现方式就是枚举。当然,readResolve 并非完全过时,有些情况可能不适合使用枚举,这时候依然需要这种方案。但一定要注意,属性设置为transient 或者 基本类型

你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧

本文题目:序列化——对于实例控制,枚举类型优先于readResolve-创新互联
标题路径:https://www.cdcxhl.com/article32/dochpc.html

成都网站建设公司_创新互联,为您提供动态网站小程序开发移动网站建设外贸网站建设域名注册网站营销

广告

声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联

成都定制网站网页设计