MyException - 我的异常网
当前位置:我的异常网» 软件架构设计 » 怎么做才能让Java 序列化机制 更安全 ? Security p

怎么做才能让Java 序列化机制 更安全 ? Security principles we follow to make Java Serialization safe

www.myexceptions.net  网友分享于:2013-10-29  浏览:1次
怎样做才能让Java 序列化机制 更安全 ? Security principles we follow to make Java Serialization safe.

概述

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方法对序列化/反序列化的过程进行完全掌控,当然我们也可以对字段进行满足安全考虑的任何处理。实现该接口,灵活性增加了,但意味着用户要自己对序列化/反序列化的过程负责,增加了用户的复杂度,同时序列化/反序列化的性能问题是否会更突出,也是一个需要考虑的问题。


文章评论

科技史上最臭名昭著的13大罪犯
科技史上最臭名昭著的13大罪犯
当下全球最炙手可热的八位少年创业者
当下全球最炙手可热的八位少年创业者
看13位CEO、创始人和高管如何提高工作效率
看13位CEO、创始人和高管如何提高工作效率
总结2014中国互联网十大段子
总结2014中国互联网十大段子
程序猿的崛起——Growth Hacker
程序猿的崛起——Growth Hacker
十大编程算法助程序员走上高手之路
十大编程算法助程序员走上高手之路
什么才是优秀的用户界面设计
什么才是优秀的用户界面设计
鲜为人知的编程真相
鲜为人知的编程真相
 程序员的样子
程序员的样子
程序员都该阅读的书
程序员都该阅读的书
不懂技术不要对懂技术的人说这很容易实现
不懂技术不要对懂技术的人说这很容易实现
老程序员的下场
老程序员的下场
聊聊HTTPS和SSL/TLS协议
聊聊HTTPS和SSL/TLS协议
我是如何打败拖延症的
我是如何打败拖延症的
10个帮程序员减压放松的网站
10个帮程序员减压放松的网站
程序员的一天:一寸光阴一寸金
程序员的一天:一寸光阴一寸金
编程语言是女人
编程语言是女人
程序员的鄙视链
程序员的鄙视链
Java程序员必看电影
Java程序员必看电影
一个程序员的时间管理
一个程序员的时间管理
Web开发人员为什么越来越懒了?
Web开发人员为什么越来越懒了?
亲爱的项目经理,我恨你
亲爱的项目经理,我恨你
10个调试和排错的小建议
10个调试和排错的小建议
60个开发者不容错过的免费资源库
60个开发者不容错过的免费资源库
旅行,写作,编程
旅行,写作,编程
程序员和编码员之间的区别
程序员和编码员之间的区别
每天工作4小时的程序员
每天工作4小时的程序员
代码女神横空出世
代码女神横空出世
做程序猿的老婆应该注意的一些事情
做程序猿的老婆应该注意的一些事情
中美印日四国程序员比较
中美印日四国程序员比较
程序员应该关注的一些事儿
程序员应该关注的一些事儿
为什么程序员都是夜猫子
为什么程序员都是夜猫子
团队中“技术大拿”并非越多越好
团队中“技术大拿”并非越多越好
初级 vs 高级开发者 哪个性价比更高?
初级 vs 高级开发者 哪个性价比更高?
“肮脏的”IT工作排行榜
“肮脏的”IT工作排行榜
要嫁就嫁程序猿—钱多话少死的早
要嫁就嫁程序猿—钱多话少死的早
如何区分一个程序员是“老手“还是“新手“?
如何区分一个程序员是“老手“还是“新手“?
那些争议最大的编程观点
那些争议最大的编程观点
5款最佳正则表达式编辑调试器
5款最佳正则表达式编辑调试器
程序员必看的十大电影
程序员必看的十大电影
漫画:程序员的工作
漫画:程序员的工作
程序员眼里IE浏览器是什么样的
程序员眼里IE浏览器是什么样的
Web开发者需具备的8个好习惯
Web开发者需具备的8个好习惯
如何成为一名黑客
如何成为一名黑客
程序员周末都喜欢做什么?
程序员周末都喜欢做什么?
软件开发程序错误异常ExceptionCopyright © 2009-2015 MyException 版权所有