简介

序列化是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. 初探JVM

    JVM探究 请你谈谈你对JVM的理解?java8虚拟机和之前的变化更新? 什么是OOM,什么是栈溢出StackOverFlowError?怎么分析? JVM的常用调优参数? 内存快照如何抓取,怎么分析 ...

  2. Centos-系统任务队列信息-uptime

    uptime 显示系统的当前时间.系统从启动到当前运行时间.当前总共在线用户.系统1.5.15分钟负载情况

  3. Hbuilder MUI 下拉选择与时间选择器

    一. Hbuilder 下拉选择 <link rel="stylesheet" href="../../../assets/mui/css/mui.picker.m ...

  4. Pyinstaller打包通用流程

    Pyinstaller打包通用流程 前言 什么是Pyinstaller Pyinstaller是用于打包python项目的一个工具, 可以将项目代码打包成可执行文件, 在其他机器上使用. 通俗的说, ...

  5. 085 01 Android 零基础入门 02 Java面向对象 01 Java面向对象基础 02 构造方法介绍 04 构造方法调用

    085 01 Android 零基础入门 02 Java面向对象 01 Java面向对象基础 02 构造方法介绍 04 构造方法调用 本文知识点:构造方法调用 说明:因为时间紧张,本人写博客过程中只是 ...

  6. JavaScript倒计时效果

    实现思路: 输入的时间减去现在的时间就是剩余的时间,但是不能拿着时分秒相减,比如05分减去25分,结果会是负的. 可以用时间戳来做,用户输入时间总的毫秒数减去现在时间的总的毫秒数,得到的就是剩余时间的 ...

  7. centos 7 安装docker 常用指令

    什么是docker l  使用最广泛的开源容器引擎 l  一种操作系统级的虚拟化技术 l  依赖于Linux内核特性:Namespace和Cgroups l  一个简单的应用程序打包工具 docker ...

  8. 实现Excel文件的上传和解析

    前言 本文思维导图 一.需求描述 实现一个页面上传excel的功能,并对excel中的内容做解析,最后存储在数据库中. 二.代码实现 需求实现思路: 先对上传的文件做校验和解析,这里我们通过Excel ...

  9. javascript in IE

    前提:一个页面导入若干个js文件 问题: 1.如果其中一个文件出问题可能会导致下面的文件导入失败,如果导入很多外部js库文件,导致错误不好排查,可以调整好js的加载顺序,以免影响页面功能 2.IE获取 ...

  10. 营口6378.7939(薇)xiaojie:营口哪里有xiaomei

    营口哪里有小姐服务大保健[微信:6378.7939倩儿小妹[营口叫小姐服务√o服务微信:6378.7939倩儿小妹[营口叫小姐服务][十微信:6378.7939倩儿小妹][营口叫小姐包夜服务][十微信 ...