序列化——对于实例控制,枚举类型优先于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-创新互联
本文网址:http://myzitong.com/article/dochpc.html