java安全编码指南之:序列化Serialization

简介: java安全编码指南之:序列化Serialization

目录



简介



序列化是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

相关文章
|
4月前
|
Java
Java开发实现图片URL地址检验,如何编码?
【10月更文挑战第14天】Java开发实现图片URL地址检验,如何编码?
139 4
|
4月前
|
Java
Java实现随机生成某个省某个市的身份证号?如何编码?
【10月更文挑战第18天】Java实现随机生成某个省某个市的身份证号?如何编码?
307 5
|
4月前
|
Java
Java开发实现图片地址检验,如果无法找到资源则使用默认图片,如何编码?
【10月更文挑战第14天】Java开发实现图片地址检验,如果无法找到资源则使用默认图片,如何编码?
101 2
|
1月前
|
自然语言处理 Java
Java中的字符集编码入门-增补字符(转载)
本文探讨Java对Unicode的支持及其发展历程。文章详细解析了Unicode字符集的结构,包括基本多语言面(BMP)和增补字符的表示方法,以及UTF-16编码中surrogate pair的使用。同时介绍了代码点和代码单元的概念,并解释了UTF-8的编码规则及其兼容性。
112 60
|
15天前
|
人工智能 监控 安全
Java智慧工地(源码):数字化管理提升施工安全与质量
随着科技的发展,智慧工地已成为建筑行业转型升级的重要手段。依托智能感知设备和云物互联技术,智慧工地为工程管理带来了革命性的变革,实现了项目管理的简单化、远程化和智能化。
35 4
|
3月前
|
SQL 安全 Java
Java 异常处理:筑牢程序稳定性的 “安全网”
本文深入探讨Java异常处理,涵盖异常的基础分类、处理机制及最佳实践。从`Error`与`Exception`的区分,到`try-catch-finally`和`throws`的运用,再到自定义异常的设计,全面解析如何有效管理程序中的异常情况,提升代码的健壮性和可维护性。通过实例代码,帮助开发者掌握异常处理技巧,确保程序稳定运行。
77 2
|
3月前
|
SQL 安全 Java
安全问题已经成为软件开发中不可忽视的重要议题。对于使用Java语言开发的应用程序来说,安全性更是至关重要
在当今网络环境下,Java应用的安全性至关重要。本文深入探讨了Java安全编程的最佳实践,包括代码审查、输入验证、输出编码、访问控制和加密技术等,帮助开发者构建安全可靠的应用。通过掌握相关技术和工具,开发者可以有效防范安全威胁,确保应用的安全性。
76 4
|
4月前
|
存储 缓存 Java
java基础:IO流 理论与代码示例(详解、idea设置统一utf-8编码问题)
这篇文章详细介绍了Java中的IO流,包括字符与字节的概念、编码格式、File类的使用、IO流的分类和原理,以及通过代码示例展示了各种流的应用,如节点流、处理流、缓存流、转换流、对象流和随机访问文件流。同时,还探讨了IDEA中设置项目编码格式的方法,以及如何处理序列化和反序列化问题。
141 1
java基础:IO流 理论与代码示例(详解、idea设置统一utf-8编码问题)
|
3月前
|
存储 安全 Java
🌟Java零基础-反序列化:从入门到精通
【10月更文挑战第21天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
126 5
|
3月前
|
存储 安全 Java
Java编程中的对象序列化与反序列化
【10月更文挑战第22天】在Java的世界里,对象序列化和反序列化是数据持久化和网络传输的关键技术。本文将带你了解如何在Java中实现对象的序列化与反序列化,并探讨其背后的原理。通过实际代码示例,我们将一步步展示如何将复杂数据结构转换为字节流,以及如何将这些字节流还原为Java对象。文章还将讨论在使用序列化时应注意的安全性问题,以确保你的应用程序既高效又安全。

热门文章

最新文章