Java序列化也带来了一些严重的误区,比如:
- 类的结构无法大量改变,除非中断序列化处理,因此即便我们之后已经不需要某些变量了,我们也需要保留它们,仅仅是为了向后兼容。
- 序列化会导致巨大的安全性危机,一个攻击者可以更改流的顺序,继而对系统造成伤害。举个例子,用户角色被序列化了,攻击者可以更改流的值为admin,再执行恶意代码。
序列化代理模式是一种使序列化能达到极高安全性的方式,在这个模式下,一个内部的私有静态类被用作序列化的代理类,该类的设计目的是用于保留主类的状态。这个模式的实现需要合理实现readResolve()和writeReplace()方法。
让我们先来写一个类,实现了序列化代码模式,之后再对其进行分析,以便更好的理解原理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
|
package
com.journaldev.serialization.proxy;
import
java.io.InvalidObjectException;
import
java.io.ObjectInputStream;
import
java.io.Serializable;
public
class
Data
implements
Serializable{
private
static
final
long
serialVersionUID = 2087368867376448459L;
private
String data;
public
Data(String d){
this
.data=d;
}
public
String getData() {
return
data;
}
public
void
setData(String data) {
this
.data = data;
}
@Override
public
String toString(){
return
"Data{data="
+data+
"}"
;
}
//serialization proxy class
private
static
class
DataProxy
implements
Serializable{
private
static
final
long
serialVersionUID = 8333905273185436744L;
private
String dataProxy;
private
static
final
String PREFIX =
"ABC"
;
private
static
final
String SUFFIX =
"DEFG"
;
public
DataProxy(Data d){
//obscuring data for security
this
.dataProxy = PREFIX + d.data + SUFFIX;
}
private
Object readResolve()
throws
InvalidObjectException {
if
(dataProxy.startsWith(PREFIX) && dataProxy.endsWith(SUFFIX)){
return
new
Data(dataProxy.substring(
3
, dataProxy.length() -
4
));
}
else
throw
new
InvalidObjectException(
"data corrupted"
);
}
}
//replacing serialized object to DataProxy object
private
Object writeReplace(){
return
new
DataProxy(
this
);
}
private
void
readObject(ObjectInputStream ois)
throws
InvalidObjectException{
throw
new
InvalidObjectException(
"Proxy is not used, something fishy"
);
}
}
|
- Data和DataProxy类都应该实现序列化接口。
- DataProxy应该能够保留Data对象的状态。
- DataProxy是一个内部的私有静态类,因此其他类无法访问它。
- DataProxy应该有一个单独的构造方法,接收Data作为参数。
- Data类应该提供writeReplace()方法,返回DataProxy实例,这样当Data对象被序列化时,返回的流是属于DataProxy类的,不过DataProxy类在外部是不可见的,所有它不能被直接使用。
- DataProxy应该实现readResolve()方法,返回Data对象,这样当Data类被反序列化时,在内部其实是DataProxy类被反序列化了,之后它的readResolve()方法被调用,我们得到了Data对象。
- 最后,在Data类中实现readObject()方法,抛出InvalidObjectException异常,防止黑客通过伪造Data对象的流并对其进行解析,继而执行攻击。
我们来写一个小测试,检查一下这样的实现是否能工作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
package
com.journaldev.serialization.proxy;
import
java.io.IOException;
import
com.journaldev.serialization.SerializationUtil;
public
class
SerializationProxyTest {
public
static
void
main(String[] args) {
String fileName =
"data.ser"
;
Data data =
new
Data(
"Pankaj"
);
try
{
SerializationUtil.serialize(data, fileName);
}
catch
(IOException e) {
e.printStackTrace();
}
try
{
Data newData = (Data) SerializationUtil.deserialize(fileName);
System.out.println(newData);
}
catch
(ClassNotFoundException | IOException e) {
e.printStackTrace();
}
}
}
|
运行以上测试程序,可以得到以下输出。
1
|
Data{data=Pankaj}
|
如果你打开data.ser文件,可以看到DataProxy对象已经被作为流存入了文件中。
这就是Java序列化的所有内容,看上去很简单但我们应当谨慎地使用它,通常来说,最好不要依赖于默认实现。你可以从上面的链接中下载项目,玩一玩,这能让你学到更多。