概述
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
的类来说是可选方法。如果实现了,那么在序列化/反序列化的时候,会调用。否则,默认的序列化/反序列化将被执行。在这两个方法里,只需要关心方法所在类本身的字段域,不需要对其父类或子类负责。在这两个方法里,我们还是需要调用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;
}
}
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
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(); } }
Validation
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 序列化
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)
总结