简介

序列化是java中一个非常常用又会被人忽视的功能,我们将对象写入文件需要序列化,同时,对象如果想要在网络上传输也需要进行序列化。

序列化的目的就是保证对象可以正确的传输,那么我们在序列化的过程中需要注意些什么问题呢?

一起来看看吧。

序列化简介

如果一个对象要想实现序列化,只需要实现Serializable接口即可。

奇怪的是Serializable是一个不需要任何实现的接口。如果我们implements Serializable但是不重写任何方法,那么将会使用JDK自带的序列化格式。

但是如果class发送变化,比如增加了字段,那么默认的序列化格式就满足不了我们的需求了,这时候我们需要考虑使用自己的序列化方式。

如果类中的字段不想被序列化,那么可以使用transient关键字。

同样的,static表示的是类变量,也不需要被序列化。

注意serialVersionUID

serialVersionUID 表示的是对象的序列ID,如果我们不指定的话,是JVM自动生成的。在反序列化的过程中,JVM会首先判断serialVersionUID 是否一致,如果不一致,那么JVM会认为这不是同一个对象。

如果我们的实例在后期需要被修改的话,注意一定不要使用默认的serialVersionUID,否则后期class发送变化之后,serialVersionUID也会同样的发生变化,最终导致和之前的序列化版本不兼容。

writeObject和readObject

如果要自己实现序列化,那么可以重写writeObject和readObject两个方法。

注意,这两个方法是private的,并且是non-static的:

private void writeObject(final ObjectOutputStream stream)
throws IOException {
stream.defaultWriteObject();
} private void readObject(final ObjectInputStream stream)
throws IOException, ClassNotFoundException {
stream.defaultReadObject();
}

如果不是private和non-static的,那么JVM就不能够发现这两个方法,就不会使用他们来做自定义序列化。

readResolve和writeReplace

如果class中的字段比较多,而这些字段都可以从其中的某一个字段中自动生成,那么我们其实并不需要序列化所有的字段,我们只把那一个字段序列化就可以了,其他的字段可以从该字段衍生得到。

readResolve和writeReplace就是序列化对象的代理功能。

首先,序列化对象需要实现writeReplace方法,表示替换成真正想要写入的对象:

public class CustUserV3 implements java.io.Serializable{

    private String name;
private String address; private Object writeReplace()
throws java.io.ObjectStreamException
{
log.info("writeReplace {}",this);
return new CustUserV3Proxy(this);
}
}

然后在Proxy对象中,需要实现readResolve方法,用于从系列化过的数据中重构序列化对象。如下所示:

public class CustUserV3Proxy implements java.io.Serializable{

    private String data;

    public CustUserV3Proxy(CustUserV3 custUserV3){
data =custUserV3.getName()+ "," + custUserV3.getAddress();
} private Object readResolve()
throws java.io.ObjectStreamException
{
String[] pieces = data.split(",");
CustUserV3 result = new CustUserV3(pieces[0], pieces[1]);
log.info("readResolve {}",result);
return result;
}
}

我们看下怎么使用:

public void testCusUserV3() throws IOException, ClassNotFoundException {
CustUserV3 custUserA=new CustUserV3("jack","www.flydean.com"); try(FileOutputStream fileOutputStream = new FileOutputStream("target/custUser.ser")){
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(custUserA);
} try(FileInputStream fileInputStream = new FileInputStream("target/custUser.ser")){
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
CustUserV3 custUser1 = (CustUserV3) objectInputStream.readObject();
log.info("{}",custUser1);
}
}

注意,我们写入和读出的都是CustUserV3对象。

不要序列化内部类

所谓内部类就是未显式或隐式声明为静态的嵌套类,为什么我们不要序列化内部类呢?

  • 序列化在非静态上下文中声明的内部类,该内部类包含对封闭类实例的隐式非瞬态引用,从而导致对其关联的外部类实例的序列化。

  • Java编译器对内部类的实现在不同的编译器之间可能有所不同。从而导致不同版本的兼容性问题。

  • 因为Externalizable的对象需要一个无参的构造函数。但是内部类的构造函数是和外部类的实例相关联的,所以它们无法实现Externalizable。

所以下面的做法是正确的:

public class OuterSer implements Serializable {
private int rank;
class InnerSer {
protected String name;
}
}

如果你真的想序列化内部类,那么把内部类置为static吧。

如果类中有自定义变量,那么不要使用默认的序列化

如果是Serializable的序列化,在反序列化的时候是不会执行构造函数的。所以,如果我们在构造函数或者其他的方法中对类中的变量有一定的约束范围的话,反序列化的过程中也必须要加上这些约束,否则就会导致恶意的字段范围。

我们举几个例子:

public class SingletonObject implements Serializable {
private static final SingletonObject INSTANCE = new SingletonObject ();
public static SingletonObject getInstance() {
return INSTANCE;
}
private SingletonObject() {
} public static Object deepCopy(Object obj) {
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
new ObjectOutputStream(bos).writeObject(obj);
ByteArrayInputStream bin =
new ByteArrayInputStream(bos.toByteArray());
return new ObjectInputStream(bin).readObject();
} catch (Exception e) {
throw new IllegalArgumentException(e);
}
} public static void main(String[] args) {
SingletonObject singletonObject= (SingletonObject) deepCopy(SingletonObject.getInstance());
System.out.println(singletonObject == SingletonObject.getInstance());
}
}

上面是一个singleton对象的例子,我们在其中定义了一个deepCopy的方法,通过序列化来对对象进行拷贝,但是拷贝出来的是一个新的对象,尽管我们定义的是singleton对象,最后运行的结果还是false,这就意味着我们的系统生成了一个不一样的对象。

怎么解决这个问题呢?

加上一个readResolve方法就可以了:

    protected final Object readResolve() throws NotSerializableException {
return INSTANCE;
}

在这个readResolve方法中,我们返回了INSTANCE,以确保其是同一个对象。

还有一种情况是类中字段是有范围的。

public class FieldRangeObject implements Serializable {

    private int age;

    public FieldRangeObject(int age){
if(age < 0 || age > 100){
throw new IllegalArgumentException("age范围不对");
}
this.age=age;
}
}

上面的类在反序列化中会有什么问题呢?

因为上面的类在反序列化的过程中,并没有对age字段进行校验,所以,恶意代码可能会生成超出范围的age数据,当反序列化之后就溢出了。

怎么处理呢?

很简单,我们在readObject方法中进行范围的判断即可:

    private  void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException {
ObjectInputStream.GetField fields = s.readFields();
int age = fields.get("age", 0);
if (age > 100 || age < 0) {
throw new InvalidObjectException("age范围不对!");
}
this.age = age;
}

不要在readObject中调用可重写的方法

为什么呢?readObject实际上是反序列化的构造函数,在readObject方法没有结束之前,对象是没有构建完成,或者说是部分构建完成。如果readObject调用了可重写的方法,那么恶意代码就可以在方法的重写中获取到还未完全实例化的对象,可能造成问题。

本文的代码:

learn-java-base-9-to-20/tree/master/security

本文已收录于 http://www.flydean.com/java-security-code-line-serialization/

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!

欢迎关注我的公众号:「程序那些事」,懂技术,更懂你!

java安全编码指南之:序列化Serialization的更多相关文章

  1. java安全编码指南之:基础篇

    目录 简介 java平台本身的安全性 安全第一,不要写聪明的代码 在代码设计之初就考虑安全性 避免重复的代码 限制权限 构建可信边界 封装 写文档 简介 作为一个程序员,只是写出好用的代码是不够的,我 ...

  2. java安全编码指南之:Mutability可变性

    目录 简介 可变对象和不可变对象 创建mutable对象的拷贝 为mutable类创建copy方法 不要相信equals 不要直接暴露可修改的属性 public static fields应该被置位f ...

  3. java安全编码指南之:字符串和编码

    目录 简介 使用变长编码的不完全字符来创建字符串 char不能表示所有的Unicode 注意Locale的使用 文件读写中的编码格式 不要将非字符数据编码为字符串 简介 字符串是我们日常编码过程中使用 ...

  4. java安全编码指南之:输入校验

    目录 简介 在字符串标准化之后进行校验 注意不可信字符串的格式化 小心使用Runtime.exec() 正则表达式的匹配 简介 为了保证java程序的安全,任何外部用户的输入我们都认为是可能有恶意攻击 ...

  5. java安全编码指南之:声明和初始化

    目录 简介 初始化顺序 循环初始化 不要使用java标准库中的类名作为自己的类名 不要在增强的for语句中修改变量值 简介 在java对象和字段的初始化过程中会遇到哪些安全性问题呢?一起来看看吧. 初 ...

  6. java安全编码指南之:Number操作

    目录 简介 Number的范围 区分位运算和算数运算 注意不要使用0作为除数 兼容C++的无符号整数类型 NAN和INFINITY 不要使用float或者double作为循环的计数器 BigDecim ...

  7. java安全编码指南之:堆污染Heap pollution

    目录 简介 产生堆污染的例子 更通用的例子 可变参数 简介 什么是堆污染呢?堆污染是指当参数化类型变量引用的对象不是该参数化类型的对象时而发生的. 我们知道在JDK5中,引入了泛型的概念,我们可以在创 ...

  8. java安全编码指南之:可见性和原子性

    目录 简介 不可变对象的可见性 保证共享变量的复合操作的原子性 保证多个Atomic原子类操作的原子性 保证方法调用链的原子性 读写64bits的值 简介 java类中会定义很多变量,有类变量也有实例 ...

  9. java安全编码指南之:异常处理

    目录 简介 异常简介 不要忽略checked exceptions 不要在异常中暴露敏感信息 在处理捕获的异常时,需要恢复对象的初始状态 不要手动完成finally block 不要捕获NullPoi ...

随机推荐

  1. myisamchk是用来做什么的?MyISAM Static和MyISAM Dynamic有什么区别?

    myisamchk是用来做什么的? 它用来压缩MyISAM[歌1] 表,这减少了磁盘或内存使用. MyISAM Static和MyISAM Dynamic有什么区别? 在MyISAM Static上的 ...

  2. websocket+sockjs+stompjs详解及实例

    最近有项目需求要用到websocket,刚开始以为很简单,但是随着遇到问题,深入了解,才知道websocket并不是想象中的那么简单,这篇文章主要是考虑websocket在客户端的使用. 1.http ...

  3. Dotnet Core IHttpClientFactory深度研究

    今天,我们深度研究一下IHttpClientFactory.   一.前言 最早,我们是在Dotnet Framework中接触到HttpClient. HttpClient给我们提供了与HTTP交互 ...

  4. GUI应用编程初体验

    不同平台的GUI实现原理是一样的. 本实验基于 windos平台. 先捋一捋概念 什么是消息队列(Message Queue)假 设一个场景:系统正在处理WM_PAINT消息,就在这时用户在键盘上敲击 ...

  5. Java 文件 IO 操作

    window 路径分割符: \ 表示 windows 系统文件目录分割符 java 代码在 windows 下写某个文件的话需要下面的方式 D:\\soft\\sdclass.txt  其中一个单斜杠 ...

  6. visual studio 2015 安装MSDN全称Microsoft Developer Network 安装离线的MSDN

    MSDN: 微软向开发人员提供的一套帮助系统,其中包含大量的开发文档,技术文章和示例代码. 这里介绍了vs2015 装离线的MSDN(说明一点是,如果不行,说明你的文件有缺陷,没安装好,之前我用vs2 ...

  7. js 为什么0.1+0.2不等于0.3

    当程序员在使用浮点数进行计算逻辑处理时,不注意,就可能出现问题, 记住,永远不要直接比较俩个浮点的大小 这个属于数字运算中的精度缺失的问题 在0.1 + 0.2这个式子中,0.1和0.2都是近似表示的 ...

  8. JAVA基础 随机点名器案例

    1.1      案例介绍 随机点名器,即在全班同学中随机的找出一名同学,打印这名同学的个人信息. 此案例在我们昨天课程学习中,已经介绍,现在我们要做的是对原有的案例进行升级,使用新的技术来实现. 我 ...

  9. Linux系统安装JDK1.8

    2020最新Linux系统发行版ContOS7演示安装JDK. 为防止操作权限不足,建议切换root用户,当然如果你对Linux命令熟悉,能够自主完成权限更新操作,可以不考虑此推荐. 更多命令学习推荐 ...

  10. 微信聊天记录导出为csv,并生成词云图

    微信聊天记录生成特定图片图云 首先贴上github地址 https://github.com/ghdefe/WechatRecordToWordCloud 来个效果图 提取聊天记录到csv参考教程 h ...