原文链接:
https://www.cnblogs.com/jian0110/p/10724765.html

前言

  什么是序列化:将对象编码成一个字节流,这样一来就可以在通信中传递对象了。比如在一台虚拟机中被传递到另一台虚拟机中,或者字节流存储到磁盘上。   “关于Java的序列化,无非就是简单的实现Serializable接口”这样的说法只能说明停留在会用的阶段,而我们想要走的更远往往就需要了解更多的东西,比如:为什么要实现序列化?序列化对程序的安全性有啥影响?如何避免多余的序列化?…..   本文主要参考资料《Effective Java》,其中代码除了只作部分说明,不能运行外,剩余代码都是亲自实践过的!



一、序列化代价

虽然实现Serializable很简单,但是为了序列化而付出的长期开销往往是实实在在的。实现Serializable接口而付出的最大代价是,一旦一个类被发布,就大大降低了“改变这个类的实现”的灵活性。
  问:这个灵活性具体是指什么呢?

  即一旦类实现了Serializable接口,并且这个类被广泛地使用,往往必须永远支持这种序列化形式,如果使用默认的序列化形式,那么这种序列化形式将永远地束缚在该类最初的内部表示法上,换句话说,一旦接受了默认的序列化形式,这个类中私有的和包级私有的实例域都变成导出的API的一部分,这显然是不符合的。这也就是实现序列化往往需要考虑到的几个代价,具体请往下看!

1、可能会导致InvalidClassException异常

  如果没有显式声明序列版本UID,对对象的需求进行了改动,那么兼容性将会遭到破坏,在运行时导致InvalidClassException。比如:增加一个不是很重要的工具方法,自动产生的序列版本UID也会发生变化,则会出现序列版本UID不一致的情况。所以最好还是显式的增加序列版本号UID。
  对User JavaBean实现Serializable接口,增加固定的序列版本号

  1. public class User implements Serializable {
  2. /** 显示增加序列版本UUID,自动生成UUID可能会导致InvalidClassException */
  3. private static final long serialVersionUID = 1L;
  4. public User(int id, String name) {
  5. this.id = id;
  6. this.name = name;
  7. }
  8. private int id;
  9. private String name;
  10. public int getId() {
  11. return id;
  12. }
  13. public void setId(int id) {
  14. this.id = id;
  15. }
  16. public String getName() {
  17. return name;
  18. }
  19. public void setName(String name) {
  20. this.name = name;
  21. }
  22. @Override
  23. public String toString() {
  24. return "User{" +
  25. "id=" + id +
  26. ", name='" + name + '\'' +
  27. '}';
  28. }
  29. }

使用ObjectOutputStream与ObjectInputStream流控制序列与反序列

  1. /**
  2. * @author jian
  3. * @date 2019/4/5
  4. * @description 测试序列化
  5. */
  6. public class SeriablizableTest {
  7. public static void main(String[] args) {
  8. User user = new User(1, "lijian");
  9. serializeUser(user);
  10. deserializeUser();
  11. }
  12. /**
  13. * 使用writeObject方法序列化
  14. *
  15. * @param user
  16. */
  17. private static void serializeUser(User user) {
  18. ObjectOutputStream outputStream = null;
  19. try {
  20. // 创建对象输出流, 包装一个其它类型目标输出流,如文件流
  21. outputStream = new ObjectOutputStream(new FileOutputStream("D:\\user.txt"));
  22. // 通过对象输出流的writeObject方法将对象user写入流中
  23. outputStream.writeObject(user);
  24. System.out.println("user序列化成功!");
  25. } catch (IOException e) {
  26. e.printStackTrace();
  27. } finally {
  28. if (outputStream != null) {
  29. try {
  30. outputStream.close();
  31. } catch (IOException e) {
  32. e.printStackTrace();
  33. }
  34. }
  35. }
  36. }
  37. private static void deserializeUser() {
  38. User user = null;
  39. Employee employee = null;
  40. ObjectInputStream inputStream = null;
  41. try {
  42. // 创建对象输出流, 包装一个其它类型目标输出流,如文件流
  43. inputStream = new ObjectInputStream(new FileInputStream("D:\\user.txt"));
  44. // 通过对象输出流的writeObject方法将对象user写入流中
  45. user = (User)inputStream.readObject();
  46. System.out.println("user反序列化成功:" + user);
  47. } catch (ClassNotFoundException e) {
  48. e.printStackTrace();
  49. } catch (IOException e) {
  50. e.printStackTrace();
  51. }
  52. finally {
  53. if (inputStream != null) {
  54. try {
  55. inputStream.close();
  56. } catch (IOException e) {
  57. e.printStackTrace();
  58. }
  59. }
  60. }
  61. }
  62. }

输出结果:先看user.txt文件中二进制文件流(因为txt打不开二进制流,所以是乱码)
如何正确使用Java序列化? - 图1

之后再看控制台中,反序列化输出的User{id=1, name=’lijian’},说明整个过程序列化成功!
如何正确使用Java序列化? - 图2

之后去掉固定的序列版本号UID,让其自动生成,同时增加age属性(或者手动修改UID为2L)

  1. private static final long serialVersionUID = 2L;
  2. 只进行反序列化将会报错: java.io.InvalidClassException
  3. public static void main(String[] args) {
  4. User user = new User(1, "lijian");
  5. // serializeUser(user);
  6. deserializeUser();
  7. }

如何正确使用Java序列化? - 图3

2、增加了出现Bug和安全漏洞的可能性

  序列化机制是一种语言之外的对象创建机制,反序列化机制都是一个“隐藏的构造器”,具备与其他构造器相同的特点,正式因为反序列化中没有显式构造器,所以很容易就会忽略:不允许攻击者访问正在构造过程中的对象内部信息。换句话说,序列化后的字节流可以被截取进行伪造,之后利用readObject方法反序列会不符合要求甚至不安全的实例。
    如何正确使用Java序列化? - 图4

3、随着类发行新的版本,测试负担也会增加。

  一个可序列化的类被修订时,需要检查是否“在新版本中序列化一个实例,可以在旧版本中反序列化”,如果一个实现序列化的类有很多的子类或者是被修改时,就不得不加以测试。

二、序列化的缺陷

1、序列化是保存对象的状态,也就是不会关心static静态域,静态域不会被序列化。如User中count静态域。

  1. public class User implements Serializable {
  2. private static final long serialVersionUID = 1L;
  3. private static int count = 1;
  4. public User(int id, String name) {
  5. // 约束条件name不能为null
  6. if (name == null || StringUtils.isEmpty(name)) {
  7. throw new NullPointerException("name is null");
  8. }
  9. this.id = id;
  10. this.name = name;
  11. }
  12. public User(){};
  13. private int id;
  14. private String name;
  15. public int getId() {
  16. return id;
  17. }
  18. public void setId(int id) {
  19. this.id = id;
  20. }
  21. public String getName() {
  22. return name;
  23. }
  24. public void setName(String name) {
  25. this.name = name;
  26. }
  27. public int getCount() {
  28. return count;
  29. }
  30. public void setCount(int count) {
  31. User.count = count;
  32. }
  33. @Override
  34. public String toString() {
  35. return "User{" +
  36. "id=" + id +
  37. ", name='" + name + '\'' +
  38. ", count=" + count +
  39. '}';
  40. }
  41. private void readObject(ObjectInputStream inputStream)
  42. throws IOException, ClassNotFoundException {
  43. inputStream.defaultReadObject();
  44. // 约束条件name不能为null
  45. if (name == null || StringUtils.isEmpty(name)) {
  46. throw new NullPointerException("name is null");
  47. }
  48. }
  49. }

赋值count为20:

  1. public static void main(String[] args) {
  2. User user = new User();
  3. user.setName("Lijian");
  4. user.setId(1);
  5. user.setCount(20);
  6. serializeUser(user);
  7. deserializeUser();
  8. }

序列化-反序列化

  1. /**
  2. * 使用writeObject方法序列化
  3. *
  4. * @param user
  5. */
  6. private static void serializeUser(User user) {
  7. ObjectOutputStream outputStream = null;
  8. try {
  9. // 创建对象输出流, 包装一个其它类型目标输出流,如文件流
  10. outputStream = new ObjectOutputStream(new FileOutputStream("D:\\user.txt"));
  11. // 通过对象输出流的writeObject方法将对象user写入流中
  12. outputStream.writeObject(user);
  13. System.out.println("user序列化成功!");
  14. } catch (IOException e) {
  15. e.printStackTrace();
  16. } finally {
  17. if (outputStream != null) {
  18. try {
  19. outputStream.close();
  20. } catch (IOException e) {
  21. e.printStackTrace();
  22. }
  23. }
  24. }
  25. }
  26. private static void deserializeUser() {
  27. User user = null;
  28. ObjectInputStream inputStream = null;
  29. try {
  30. // 创建对象输出流, 包装一个其它类型目标输出流,如文件流
  31. inputStream = new ObjectInputStream(new FileInputStream("D:\\user.txt"));
  32. // 通过对象输出流的writeObject方法将对象user写入流中
  33. user = (User)inputStream.readObject();
  34. // User静态变量初始化为0,不会被反序列化
  35. System.out.println("user反序列化成功!");
  36. System.out.println("id:" + user.getId());
  37. System.out.println("name:" + user.getName());
  38. System.out.println("count:" + user.getCount());
  39. } catch (ClassNotFoundException e) {
  40. e.printStackTrace();
  41. } catch (IOException e) {
  42. e.printStackTrace();
  43. }
  44. finally {
  45. if (inputStream != null) {
  46. try {
  47. inputStream.close();
  48. } catch (IOException e) {
  49. e.printStackTrace();
  50. }
  51. }
  52. }
  53. }

控制它输出:count明明被赋值为20,但是反序列化后输出为0,说明static是不会参数序列化的,跟transient类似。最终在反序列化过程中会被初始化为默认值(基本数据类型为0,对象引用为null,boolean为false)
如何正确使用Java序列化? - 图5

2、在序列化对象时,如果该对象中有引用对象域名,那么也要要求该引用对象是可实例化的。如序列化User实例,其中引用了Employee实例,那么也需要对Employee进行可序列化操作,否则会报错: java.io.NotSerializableException
User增加对Employee引用:
/* 对外引用其它对象,如果序列化该实例,则该对象实例也必须能实例化(implement Serializable) /
public Employee employee = new Employee(1, “Java programmer”);
 Employee不实现序列化:

  1. public class Employee{
  2. private int code;
  3. private String position;
  4. public int getCode() {
  5. return code;
  6. }
  7. public void setCode(int code) {
  8. this.code = code;
  9. }
  10. public String getPosition() {
  11. return position;
  12. }
  13. public void setPosition(String position) {
  14. this.position = position;
  15. }
  16. public Employee(int code, String position) {
  17. this.code = code;
  18. this.position = position;
  19. }
  20. @Override
  21. public String toString() {
  22. return "Employee{" +
  23. "code=" + code +
  24. ", position='" + position + '\'' +
  25. '}';
  26. }
  27. }

测试类:

  1. /**
  2. * @author jian
  3. * @date 2019/4/5
  4. * @description 测试序列化
  5. */
  6. public class SeriablizableTest {
  7. public static void main(String[] args) {
  8. User user = new User(1, "lijian");
  9. serializeUser(user);
  10. deserializeUser();
  11. }
  12. /**
  13. * 使用writeObject方法序列化
  14. *
  15. * @param user
  16. */
  17. private static void serializeUser(User user) {
  18. ObjectOutputStream outputStream = null;
  19. try {
  20. // 创建对象输出流, 包装一个其它类型目标输出流,如文件流
  21. outputStream = new ObjectOutputStream(new FileOutputStream("D:\\user.txt"));
  22. // 通过对象输出流的writeObject方法将对象user写入流中
  23. outputStream.writeObject(user);
  24. System.out.println("user序列化成功!");
  25. } catch (NotSerializableException e) {
  26. System.out.println("user引用employee对象域序列化失败");
  27. } catch (IOException e) {
  28. e.printStackTrace();
  29. } finally {
  30. if (outputStream != null) {
  31. try {
  32. outputStream.close();
  33. } catch (IOException e) {
  34. e.printStackTrace();
  35. }
  36. }
  37. }
  38. }
  39. private static void deserializeUser() {
  40. User user = null;
  41. Employee employee = null;
  42. int id = 0;
  43. ObjectInputStream inputStream = null;
  44. try {
  45. // 创建对象输出流, 包装一个其它类型目标输出流,如文件流
  46. inputStream = new ObjectInputStream(new FileInputStream("D:\\user.txt"));
  47. // 通过对象输出流的writeObject方法将对象user写入流中
  48. user = (User)inputStream.readObject();
  49. System.out.println("user引用employee对象域反序列化成功");
  50. System.out.println("user反序列化成功:" + user);
  51. } catch (WriteAbortedException e) {
  52. System.out.println("user引用employee对象域反序列化失败");
  53. } catch (ClassNotFoundException e) {
  54. e.printStackTrace();
  55. } catch (IOException e) {
  56. e.printStackTrace();
  57. }
  58. finally {
  59. if (inputStream != null) {
  60. try {
  61. inputStream.close();
  62. } catch (IOException e) {
  63. e.printStackTrace();
  64. }
  65. }
  66. }
  67. }
  68. }

控制台输出结果:
如何正确使用Java序列化? - 图6
要解决这样的问题,要么将 Employee implement Serializable ,要么对Employee对象实例transient修饰: public transient Employee employee = new Employee(1, “Java programmer”); 。但是需要注意的是序列化过程会对transient修饰的域初始化为默认值(对象引用为null,基本数据类型为0,boolean为false),所以执行以上代码会出现 java.lang.NullPointerException

3、默认序列化的过程可能消耗大量内存空间和时间,甚至可能会引起栈溢出:因为第二条的原因,如果一个类中大量存在引用对象域,并且都需要实现序列化,那么整个序列化过程可能会很消耗时间,在通信传输过程中更是如此,同时序列化后的字节流需要足够大的内存。

三、提高序列化的安全性

1、编写readObject提供安全性与约束性

  即使确定了默认的序列化形式是合适的,通常还必须提供一个readObject方法以保证约束关系和安全性。readObject方法相当于另一个共有构造器(可以认为是用“字节流作为唯一参数”的构造器)跟其它构造器一样,它也要求同样的所有主要事项:构造器必须检查参数的有效性,必要时对参数进行保护性拷贝等。readObject如果没有做到,那么对于攻击者来说违反这个类的约束条件相对就比较简单了,如果对一个人工仿造的字节流(人工修改从实例序列后的字节流)时,readObject产生的对象会违反所属类的约束条件。
  1)为了解决这个问题,User中需要提供了readObject方法,该方法首先调用defalutReadObject,然后检查被反序列化之后的对象的有效性,如果有效性检查失败,readObject方法就会抛出InvalidObjectException异常,使反序列过程不能成功。

  1. private void readObject(ObjectInputStream inputStream)
  2. throws IOException, ClassNotFoundException {
  3. inputStream.defaultReadObject();
  4. }

  User中的构造器中已对参数name约束为不能为null

  1. public User(int id, String name) {
  2. // 约束条件name不能为null或空
  3. if (name == null || StringUtils.isEmpty(name)) {
  4. throw new NullPointerException("name is null or empty");
  5. }
  6. this.id = id;
  7. this.name = name;
  8. }

2)那么readObject中也应该对其name进行约束,否则人工伪造的字节流很容易通过readObject构造出没有任何约束的对象实例,造成安全隐患。

  1. private void readObject(ObjectInputStream inputStream)
  2. throws IOException, ClassNotFoundException {
  3. inputStream.defaultReadObject();
  4. // 约束条件name不能为null或空
  5. if (name == null || StringUtils.isEmpty(name)) {
  6. throw new NullPointerException("name is null or empty");
  7. }
  8. }

  尽管以上两种修正已经有效地避免攻击者创建无效的User实例,但是还有一种情况通过伪造字节流可以创建可变的User实例:比如User中增加Date对象引用birthday私有域,然后通过附加伪造字节流指向该birthday引用,攻击者从ObjectInputStream中读取User实例,然后读取附加后面的恶意Date引用,通过该Date引用就可以能够访问User对象内部私有Date域所引用的对象,从而改变User实例。
如何正确使用Java序列化? - 图7

代码如下:

  1. public class MutableUser {
  2. public User user;
  3. public Date birthday;
  4. public MutableUser(){
  5. try {
  6. ByteArrayOutputStream bos = new ByteArrayOutputStream();
  7. ObjectOutputStream out = new ObjectOutputStream(bos);
  8. // 字节流有效的User实例开头,然后附加额外的引用
  9. out.writeObject(new User(new Date()));
  10. // 假设这是恶意的二进制,即附加恶意对象引用Date
  11. byte[] ref = {0x71, 0, 0x7e, 0 ,5};
  12. bos.write(ref);
  13. // 攻击者从ObjectInputStream中读取User实例,然后读取附加在后面的“恶意编制对象引用Date”
  14. ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
  15. user = (User) in.readObject();
  16. birthday = (Date) in.readObject();
  17. } catch (Exception e) {
  18. }
  19. }
  20. public static void main(String[] args) {
  21. MutableUser mutableUser = new MutableUser();
  22. User user = mutableUser.user;
  23. Date birthday = mutableUser.birthday;
  24. // 攻击者修改User内部birthday私有域,年份更改为2018
  25. birthday.setTime(2018);
  26. System.out.println(user);
  27. }
  28. }

注:以上代码运行不了,只会加以解释说明而已,具体可以查看《Effective Java》中的代码举例
为了解决此问题,提出第三个安全措施
 3)当一个对象被反序列化时,客户端不应该拥有对象的引用,如果哪个域包含了这样的对象引用,如果包含了私有的域(组件),就必须要保护性拷贝(非final域):当User对象在客户端MutableUser反序列化时,客户端拥有 了不该拥有的User私有域Date引用birthday,所以应该在readObject对birthday进行拷贝:

  1. private void readObject(ObjectInputStream inputStream)
  2. throws IOException, ClassNotFoundException {
  3. inputStream.defaultReadObject();
  4. // 保护性拷贝birthday
  5. birthday = new Date(birthday.getTime());
  6. // 约束条件name不能为null
  7. if (name == null || StringUtils.isEmpty(name)) {
  8. throw new NullPointerException("name is null");
  9. }
  10. }

总结:

  1)使用readObject其实就跟正常无参数的构造器一样,该满足的约束需要满足,同时必要时进行保护性拷贝。
  2)反序列化过程最终会调用readObject方法,如下是一个异常栈的调用关系(代码中故意让readObject方法抛异常):deserialize——>ObjectInputStream.readObject——->ObjectInputStream.readObject0——->……User.readObject
  如何正确使用Java序列化? - 图8

2、使用readResolve增强单例

  但是如果Sinleton类实现了序列化,那么它不再是一个Singleton,无论该类使用了默认的序列化形式,还是自定义的序列化形式,还是是否提供显式的readObject方法都没关系。任何一个readObject方法,不管是显式还是默认的,它都会返回一个新建的实例,这个新建的实例不同于该类初始化时创建的实例。
  简单的Singleton:

  1. public class Singleton {
  2. private static Singleton INSTANCE= new Singleton();
  3. private Singleton(){};
  4.  .....
  5. }

  readResolve特性允许使用readObject创建实例代替另一个实例,如果一个类定义了readResolve方法,并且具备正确的声明,那么在反序列化的之后,新建的readResolve方法就会被调用,然后返回的对象引用将被返回,取代新建的对象。

  1. public class Singleton implements Serializable {
  2. private static Singleton INSTANCE= new Singleton();
  3. private Singleton(){};
  4. private Object readResolve(){
  5. return INSTANCE;
  6. }
  7. }