怎么做才能让Java 序列化机制 更安全 ? Security principles we follow to make Java Serialization safe
怎样做才能让Java 序列化机制 更安全 ? Security principles we follow to make Java Serialization safe.
输出结果如下
这说明_salary 变量的值没有被序列化。
如果你想看到验证失败的结果,你可以把SerializationTest类中的代码
改为
输出结果如下

概述
Java 序列化 serialization,大家应该都不陌生。其主要职责就是将一个对象的状态转化为一个字节序列,以方便对象的持久化或网络传输。反序列化的过程正好相反。开发人员所要做的只是实现Serializable接口,然后调用ObjectOutputStream/ObjectInputStream的WriteObject/ReadObject方法即可,其他的工作 JVM 会自动帮你做了。
那通过实现Serializable 接口所获取的序列化能力是否有安全隐患?由于这些字节序列已经脱离了Java的安全体系存在于磁盘或网络上,我们能否对序列化后的字节序列进行查看和修改,甚至于注入恶意病毒呢? Java 反序列化机制是否又会对建立的对象进行验证以确保它的安全性、准确性呢? 如果你想到这些问题,那恐怕答案会让你失望了。Java序列化后的字节序列基本都是明文存在的,而且字节序列的组成有很明确的文档进行说明,你可以试着用一些十六进制的文本编辑工具,如Hexeditor 查看一下对象序列化后的内容,你都能看到很多私有变量的实际赋值。关于字节序列的说明,可参考对象序列化流协议
,这里就不多说了。这篇文章的重点是说一些Java提供的安全机制,通过这些机制,我们能够提升序列化/反序列化的安全指数。
读这篇文章前,最好能了解一些Java序列化的基本知识。
Transient
这个关键字的用途,大家应该都不陌生。它用来指定可序列化对象中,哪个变量不被序列化。如果你的对象中存放了一些敏感信息,不想让别人看到的话。那么就把存放这个敏感信息的变量声明为Transient. 如下代码例子所示,Employee类中有一个私有变量_salary,我们在序列化时,想忽略这个敏感信息,那将它定义为transient即可。
import java.io.Serializable; public class Employee implements Serializable { private static final long serialVersionUID = -7331553489509930824L; private String _name; private transient double _salary; public Employee(String name,double salary) { this._name = name; this._salary = salary; } public String toString(){ return "Employee Name: " + this._name + " with salary " + this._salary; } }
import java.io.*; public class SerializationTest { public void serialize() throws IOException{ Employee em = new Employee("Matt",10000); FileOutputStream fos = null; ObjectOutputStream oos = null; try { fos = new FileOutputStream("employee.save"); oos = new ObjectOutputStream(fos); System.out.println("Serialized - "+ em.toString()); oos.writeObject(em); }finally{ try { oos.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } public void deSerialize() throws ClassNotFoundException, IOException { FileInputStream fis = null; ObjectInputStream ois = null; try { fis = new FileInputStream("employee.save"); ois = new ObjectInputStream(fis); Employee e = (Employee) ois.readObject(); System.out.println("Deserialized - "+ e.toString()); }finally{ try { ois.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } public static void main(String[] args) throws IOException, ClassNotFoundException { SerializationTest st = new SerializationTest(); st.serialize(); st.deSerialize(); } }
输出结果如下
Serialized - Employee Name: Matt with salary 10000.0 Deserialized - Employee Name: Matt with salary 0.0
这说明_salary 变量的值没有被序列化。
WirteObject & ReadObject
WriteObject 和ReadObject方法对于实现了Serializable 接口
的类来说是可选方法。如果实现了,那么在序列化/反序列化的时候,会调用。否则,默认的序列化/反序列化将被执行。在这两个方法里,只需要关心方法所在类本身的字段域,不需要对其父类或子类负责。在这两个方法里,我们还是需要调用ObjectOutputStream/
ObjectInputStream 的方法
defaultWriteObject/defaultReadObject 以执行Java的默认序列化/反序列化过程。如下例所示,其中
SerializationTest 类与上例相比,没有变化,故省略。 Employee类如下import java.io.Serializable;
public class Employee implements Serializable {
private static final long serialVersionUID = -7331553489509930824L;
private String _name;
private double _salary;
public Employee(String name,double salary) {
this._name = name;
this._salary = salary;
}
private void writeObject(java.io.ObjectOutputStream stream)
throws java.io.IOException
{
_salary = _salary * _name.hashCode(); //只做实例,可以使用任何你认为合适的加密算法。
stream.defaultWriteObject();
System.out.println("Customized writeObject method called.");
}
private void readObject(java.io.ObjectInputStream stream)
throws java.io.IOException, ClassNotFoundException
{
stream.defaultReadObject();
_salary = _salary / _name.hashCode(); //只做实例,可以使用任何你认为合适的解密算法。
System.out.println("Customized readObject method called.");
}
public String toString(){
return "Employee Name: " + this._name + " with salary " + this._salary;
}
}
运行SerializationTest类的输出结果如下
Serialized - Employee Name: Matt with salary 10000.0 Customized writeObject method called. Customized readObject method called. Deserialized - Employee Name: Matt with salary 10000.0
SealedObject & SignedObject
上面已经提到的方法为我们操作序列化对象内的局部变量提供了灵活性。但一种更简单的方法就是通过javax.crypto.SealedObject 和 java.security.SignedObject类,我们可以把整个序列化的流进行加密。你可能注意到这两个类分别存放在了不同的Java package里,虽然他们都对对象的真实性,完整性提供了保证,有人更倾向于在进行Java API设计时将他们放到一起。 据说,造成这种情况的原因是受到美国关于加密软件出口相关规定的约束造成的。JCE
(Java Cryptography Extension,SealedObject属于其中) 最开始设计时,美国政府要求加密软件出口必须要获得军火商类似的许可才行。这都是题外话了。这里不对这Java的加密做过多的介绍,只是使用SealedObject进行一下实例说明,从而我们能看到使用他们可以很方便的对可序列化的对象进行加密,从而保证信息安全。 代码如下
import java.io.Serializable; public class Employee implements Serializable { private static final long serialVersionUID = -7331553489509930824L; private String _name; private double _salary; public Employee(String name,double salary) { this._name = name; this._salary = salary; } /* * 通过实现writeReplace方法来自动返回一个替代的SealedObject对象不可行,会导致栈溢出。因为SealedObject会对传入的待加密对象进行深Copy。这个操作就是通过序列化完成的。所以,会递归成死循环。 */ /* private Object writeReplace()throws java.io.ObjectStreamException { SealedObject so = null; try { so = new SealedObject(this, new NullCipher()); } catch (IllegalBlockSizeException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } return so; } */ public String toString(){ return "Employee Name: " + this._name + " with salary " + this._salary; } }
import java.io.*; import java.security.InvalidKeyException; import java.security.Key; import java.security.NoSuchAlgorithmException; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.KeyGenerator; import javax.crypto.NoSuchPaddingException; import javax.crypto.NullCipher; import javax.crypto.SealedObject; import javax.crypto.SecretKey; public class SerializationTest { private static Key _key = null; public void serialize() throws IOException, IllegalBlockSizeException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException{ Employee em = new Employee("Matt",10000); FileOutputStream fos = null; ObjectOutputStream oos = null; try { fos = new FileOutputStream("employee.save"); oos = new ObjectOutputStream(fos); KeyGenerator keyGenerator = KeyGenerator.getInstance("DESede"); _key = keyGenerator.generateKey(); Cipher cipher = Cipher.getInstance("DESede"); cipher.init(Cipher.ENCRYPT_MODE, _key); SealedObject so = new SealedObject(em,cipher); oos.writeObject(so); System.out.println("Serialized - "+ em.toString()); }finally{ try { oos.close(); } catch (IOException e) { e.printStackTrace(); } } } public void deSerialize() throws ClassNotFoundException, IOException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException { FileInputStream fis = null; ObjectInputStream ois = null; try { fis = new FileInputStream("employee.save"); ois = new ObjectInputStream(fis); SealedObject so = (SealedObject)ois.readObject(); Employee e = (Employee) so.getObject(_key); System.out.println("Deserialized - "+ e.toString()); }finally{ try { ois.close(); } catch (IOException e) { e.printStackTrace(); } } } public static void main(String[] args) throws IOException, ClassNotFoundException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException { SerializationTest st = new SerializationTest(); st.serialize(); st.deSerialize(); } }
如上所示,我们主要是在序列化对象的时候,对其作了一个加密操作。反序列化时,必须要拿到序列化时使用的Key,才可以。另外,我曾尝试在Employee类中实现WriteReplace方法直接将待序列化的对象包装成SealedObject 进行返回,但出现了栈溢出。因为SealedObject会对传入的待加密对象进行深Copy。这个操作就是通过序列化完成的。所以,会造成无限递归,直到栈溢出。
Validation
Java 在反序列化的过程中不会对Deserialized的对象进行有效性检查。而且,一旦对象是可序列化的,那就说明对象状态对应的的字节序列可以脱离Java的安全体系存在。关键是这个序列化后的字节序列对用户是可读的,基本是明文显示。所以在反序列化时,为了安全起见,我们最好对得到的数据进行校验。这时,需要我们实现接口java.io.ObjectInputValidation,这样我们可以定义反序列化中的回调函数来进行验证工作。代码如下
import java.io.InvalidObjectException; import java.io.ObjectInputValidation; import java.io.Serializable; public class Employee implements Serializable,ObjectInputValidation { private static final long serialVersionUID = -7331553489509930824L; private String _name; private double _salary; public Employee(String name,double salary) { this._name = name; this._salary = salary; } public String toString(){ return "Employee Name: " + this._name + " with salary " + this._salary; } private void readObject(java.io.ObjectInputStream stream) throws java.io.IOException, ClassNotFoundException { stream.defaultReadObject(); stream.registerValidation(this, 0); System.out.println("Customized readObject method called."); } @Override public void validateObject() throws InvalidObjectException { System.out.println("Validation object after deserialization."); if (_salary < 0) throw new InvalidObjectException("The Deserialized object is invalid. Salary can't be negative."); else System.out.println("The Deserialized object is valid."); } }
import java.io.*; public class SerializationTest { public void serialize() throws IOException{ Employee em = new Employee("Matt",10000); FileOutputStream fos = null; ObjectOutputStream oos = null; try { fos = new FileOutputStream("employee.save"); oos = new ObjectOutputStream(fos); System.out.println("Serialized - "+ em.toString()); oos.writeObject(em); }finally{ try { oos.close(); } catch (IOException e) { e.printStackTrace(); } } } public void deSerialize() throws ClassNotFoundException, IOException { FileInputStream fis = null; ObjectInputStream ois = null; try { fis = new FileInputStream("employee.save"); ois = new ObjectInputStream(fis); Employee e = (Employee) ois.readObject(); System.out.println("Deserialized - "+ e.toString()); }finally{ try { ois.close(); } catch (IOException e) { e.printStackTrace(); } } } public static void main(String[] args) throws IOException, ClassNotFoundException { SerializationTest st = new SerializationTest(); st.serialize(); st.deSerialize(); } }
输出结果如下
Serialized - Employee Name: Matt with salary 10000.0 Customized readObject method called. Validation object after deserialization. The Deserialized object is valid. Deserialized - Employee Name: Matt with salary 10000.0
如果你想看到验证失败的结果,你可以把SerializationTest类中的代码
Employee em = new Employee("Matt",10000);
改为
Employee em = new Employee("Matt",-10000);
输出结果如下
Serialized - Employee Name: Matt with salary -10000.0 Customized readObject method called. Validation object after deserialization. Exception in thread "main" java.io.InvalidObjectException: The Deserialized object is invalid. Salary can't be negative. at com.tr.serialization.validation.Employee.validateObject(Employee.java:37) at java.io.ObjectInputStream$ValidationList$1.run(ObjectInputStream.java:2206) at java.security.AccessController.doPrivileged(Native Method) at java.io.ObjectInputStream$ValidationList.doCallbacks(ObjectInputStream.java:2202) at java.io.ObjectInputStream.readObject(ObjectInputStream.java:357) at com.tr.serialization.validation.SerializationTest.deSerialize(SerializationTest.java:38) at com.tr.serialization.validation.SerializationTest.main(SerializationTest.java:55)
Say NO to 序列化
既然序列化会带来很多安全问题,那我们不用不就完了? 不实现Serializable接口喽, 多简单啊? 但有时候,事情不是我们想像的那么简单。 比如说我们有一个类PartimeEmployee类继承自Employee类,Employee类实现了Serializable接口。 如下类图所示
这时候,我们不想让PartTimeEmployee被序列化。那我们该怎么办? 我们只需要在PartTimeEmployee类的writeObject和readObject方法中抛出异常NotSerializableException即可。代码如下
import java.io.Serializable; public class Employee implements Serializable { private static final long serialVersionUID = -7331553489509930824L; private String _name; private double _salary; public Employee(String name,double salary) { this._name = name; this._salary = salary; } public String toString(){ return "Employee Name: " + this._name + " with salary " + this._salary; } } import java.io.NotSerializableException; public class PartTimeEmployee extends Employee { private int working_days_each_month; private double salary_each_hour; public PartTimeEmployee(String name, double salary) { super(name, salary); // TODO Auto-generated constructor stub } private void writeObject(java.io.ObjectOutputStream stream) throws java.io.IOException { throw new NotSerializableException("This class is not serializable"); } private void readObject(java.io.ObjectInputStream stream) throws java.io.IOException, ClassNotFoundException { throw new NotSerializableException("This class is not serializable"); } }
import java.io.*; public class SerializationTest { public void serialize() throws IOException{ PartTimeEmployee em = new PartTimeEmployee("Matt",10000); FileOutputStream fos = null; ObjectOutputStream oos = null; try { fos = new FileOutputStream("employee.save"); oos = new ObjectOutputStream(fos); oos.writeObject(em); System.out.println("Serialized - "+ em.toString()); }finally{ try { oos.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } public void deSerialize() throws ClassNotFoundException, IOException { FileInputStream fis = null; ObjectInputStream ois = null; try { fis = new FileInputStream("employee.save"); ois = new ObjectInputStream(fis); PartTimeEmployee e = (PartTimeEmployee) ois.readObject(); System.out.println("Deserialized - "+ e.toString()); }finally{ try { ois.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } public static void main(String[] args) throws IOException, ClassNotFoundException { SerializationTest st = new SerializationTest(); st.serialize(); st.deSerialize(); } }
输出结果如下
Exception in thread "main" java.io.NotSerializableException: This class is not serializable at com.tr.serialization.no.PartTimeEmployee.writeObject(PartTimeEmployee.java:20) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25) at java.lang.reflect.Method.invoke(Method.java:597) at java.io.ObjectStreamClass.invokeWriteObject(ObjectStreamClass.java:940) at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1469) at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1400) at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1158) at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:330) at com.tr.serialization.no.SerializationTest.serialize(SerializationTest.java:18) at com.tr.serialization.no.SerializationTest.main(SerializationTest.java:56)
总结
Java API在 JDK 1.1 的版本中还引入了 Externalizable 接口,通过实现该接口,用户可以通过WriteExternal和ReadExternal方法对序列化/反序列化的过程进行完全掌控,当然我们也可以对字段进行满足安全考虑的任何处理。实现该接口,灵活性增加了,但意味着用户要自己对序列化/反序列化的过程负责,增加了用户的复杂度,同时序列化/反序列化的性能问题是否会更突出,也是一个需要考虑的问题。