简介

序列化是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. 简单渗透测试流程演示(445端口、IPC$、灰鸽子)

    目录 一.实验流程 二.实验过程 2.1 信息收集 2.2 利用过程 2.3 暴力破解系统密码之445 2.4 通过木马留后门 一.实验流程 0.授权(对方同意被渗透测试才是合法的.)1.信息收集  ...

  2. 将SublimeText3打造成简易Java IDE

    简介与优点 使用该教程,你能使你的Sublime Text3可以作为一个精简版的JAVA IDE工具 既可以独立在cmd控制台运行也可以在Sublime自带的控制台运行 运行后不会有乱码 报错后可以在 ...

  3. Java知识系统回顾整理01基础01第一个程序05Eclipse中运行Java程序

    一.打开Java文件 直接打开在 命令行Hello World 中创建的java 文件 HelloWorld.java 二.运行 点击绿色运行按钮,直接运行 在eclipse中,编译过程自动执行了 三 ...

  4. C语言中最常用的标准库函数

    标准头文件包括: <asset.h>      <ctype.h>       <errno.h>       <float.h> <limits ...

  5. P4454 [CQOI2018]破解D-H协议

    链接 这题并不难只是需要把题读懂 - By ShadderLeave 一句话题意 给定两个数 \(p\)和\(g\),有\(t\)组询问,每组询问给出\(A\)和\(B\) 其中 A = \(g^a ...

  6. 【题解】「MCOI-02」Convex Hull 凸包

    题目戳我 \(\text{Solution:}\) \[\sum_{i=1}^n \sum_{j=1}^n \rho(i)\rho(j)\rho(\gcd(i,j)) \] \[=\sum_{d=1} ...

  7. NOI 2012 【迷失游乐园】

    这道题,额,反正我是刚了2天,然后就萎了......(是不是觉得我很菜) 题目描述: 放假了,小Z觉得呆在家里特别无聊,于是决定一个人去游乐园玩. 进入游乐园后,小Z看了看游乐园的地图,发现可以将游乐 ...

  8. maven下载依赖包下载失败

    在家办公,遇到项目的maven包下载不了,刚开始以为是vpn的问题,折腾半天反复确认之后没有发现什么问题. 同时试过阿里巴巴的maven仓库,删除过以来,重新导过包发现都不行. 后来在idea的设置里 ...

  9. Consul 快速入门

    Consul是什么 Consul是一个服务网格(微服务间的 TCP/IP,负责服务之间的网络调用.限流.熔断和监控)解决方案,它是一个一个分布式的,高度可用的系统,而且开发使用都很简便.它提供了一个功 ...

  10. Mybatis中进行批量更新(updateBatch)

    更新多条数据,每条数据都不一样 背景描述:通常如果需要一次更新多条数据有两个方式,(1)在业务代码中循环遍历逐条更新.(2)一次性更新所有数据(更准确的说是一条sql语句来更新所有数据,逐条更新的操作 ...