单例模式(二)

  • 单例模式,如何防止反射和序列化漏洞?(不包含枚举单例模式)

为了保证系统绝对的安全性,就需要考虑到一系列操作会引发的后果,特别是如果是在研发一款产品,我们只能尽量让别人对我们的产品无可挑剔,我这里所说的无可挑剔不单单指的是简单的使用用户,而是让懂技术的人员也觉得这个产品做的好,那么这个时候,我们就需要把所有可能性考虑完全。

前面有讲述了获取单实例的五种方法,其中前面四种(饿汉、懒汉、双重检测、内部类)四种方式都会引发反射和序列化所破坏单例对象。第一篇关于单例模式的文章中我们已经讲解过了五种创建单例模式的方式了,如果有不知道的小伙伴可以看我关于单例模式的第一篇分享。

这里我们以懒汉式为例去验证一下上述这个事实

方式一:反射获取单例对象(破坏和防止)

/**
 * @author json.yang
 * @Description 懒汉式方式创建单例
 * @Date 2019/9/8
 */
public class SingtonDemo6 implements Serializable{

    private static final long serialVersionUID = 406855825891544021L;

    private static SingtonDemo6 instance;

    private SingtonDemo6(){

    }

    //多线程情况下,会触发线程安全问题,因此方法需要同步
    public static synchronized SingtonDemo6 getInstance(){
        if(instance == null){
            instance =  new SingtonDemo6();
        }
        return instance;
    }
    

}
public static void main(String[] args) throws Exception {

    /**
     * 通过反射方式来获取单例对象
     */
    SingtonDemo6 s1 = SingtonDemo6.getInstance();
    SingtonDemo6 s2 = SingtonDemo6.getInstance();
    System.out.println("破解前:" + s1);
    System.out.println("破解前:" + s2);
    Class aClass = (Class) Class.forName("com.design.sington.SingtonDemo6");
    Constructor c = aClass.getDeclaredConstructor(null);
    c.setAccessible(true);
    SingtonDemo6 singtonDemo1 = c.newInstance();
    SingtonDemo6 singtonDemo2 = c.newInstance();
    System.out.println(singtonDemo1);
    System.out.println(singtonDemo2);
}

 运行结果如下:

单例模式(二)_第1张图片 

通过运行结果可以看到反射造成了原本是单例的对象,变成了多例了,破坏了单例模式。

那么如何避免反射破坏单例呢?我们只需要在使用创建单例对象的私有构造方法中添加该实例的非空判断,如下:

private SingtonDemo6(){
    if(instance != null){
        throw new RuntimeException();
    }
}

我们来说一下这样做的原理:

我们在创建这个实例的时候

 

@CallerSensitive
public T newInstance(Object ... initargs)
    throws InstantiationException, IllegalAccessException,
           IllegalArgumentException, InvocationTargetException
{
    if (!override) {
        if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
            Class caller = Reflection.getCallerClass();
            checkAccess(caller, clazz, null, modifiers);
        }
    }
    if ((clazz.getModifiers() & Modifier.ENUM) != 0)
        throw new IllegalArgumentException("Cannot reflectively create enum objects");
    ConstructorAccessor ca = constructorAccessor;   // read volatile
    if (ca == null) {
        ca = acquireConstructorAccessor();
    }
    @SuppressWarnings("unchecked")
    T inst = (T) ca.newInstance(initargs);
    return inst;
}

在上面代码块19行中,因为我们这里的ca有多个实现类,我们断点可以看出ca对象进入的时DelegatingConstructorAccessorImpl中的instance方法,

代码如下:

public Object newInstance(Object[] var1) throws InstantiationException, IllegalArgumentException, InvocationTargetException {
    return this.delegate.newInstance(var1);
}

这个地方又会调用NativeConstracotrAccessorImpl的newInstance方法,最后再这个方法调用本地方newInstance0方法,这个方法是c++的,这里我们不做讲解。

public Object newInstance(Object[] var1) throws InstantiationException, IllegalArgumentException, InvocationTargetException {
    if (++this.numInvocations > ReflectionFactory.inflationThreshold() && !ReflectUtil.isVMAnonymousClass(this.c.getDeclaringClass())) {
        ConstructorAccessorImpl var2 = (ConstructorAccessorImpl)(new MethodAccessorGenerator()).generateConstructor(this.c.getDeclaringClass(), this.c.getParameterTypes(), this.c.getExceptionTypes(), this.c.getModifiers());
        this.parent.setDelegate(var2);
    }

    return newInstance0(this.c, var1);
}

 

private static native Object newInstance0(Constructor var0, Object[] var1) throws InstantiationException, IllegalArgumentException, InvocationTargetException;

 这里不难猜测,我们在c++的代码中有做判断,我们再私有构造器中,如果有判断实例不为空,那么我们就会在再次创建这个实例的时候抛出异常;反之,如果我们没有在私有构造器中去判断当前类实例是否为空,那么我们就可以通过反射成功创建对象了,只是不是单例的对象而已。

方式二:序列化获取单例对象(破坏和防止)

public static void main(String[] args) throws Exception {
    /**
     * 通过反射方式来获取单例对象
     */
    SingtonDemo6 s1 = SingtonDemo6.getInstance();
    SingtonDemo6 s2 = SingtonDemo6.getInstance();
    System.out.println("破解前:" + s1);
    System.out.println("破解前:" + s2);
    /**
     * 通过序列化方式来获取单例对象
     */
    /**
     * 1.要序列化的类需要实现序列化接口,把对象永久的保存在磁盘中
     */
    FileOutputStream fops = new FileOutputStream("d:/a.txt");
    ObjectOutputStream oos = new ObjectOutputStream(fops);
    oos.writeObject(s1);
    /**
     * 2.通过反序列化方式把对象从磁盘中读取出来
     */
    ObjectInputStream ois = new ObjectInputStream(new FileInputStream("d:/a.txt"));
    SingtonDemo6 sington = (SingtonDemo6) ois.readObject();
    System.out.println("破解后的对象"+sington);
}

 执行结果如下:

单例模式(二)_第2张图片

由执行结果我们不难看出,我们的对象有变化了,这就表示序列化破坏了单例模式。那么我们如何解决了。我们只需要在要反序列化的类(这里指SingtonDemo6)上面加一个readResolve方法,就可以防止反射破坏序列化了。

/**
 * 反序列化的时候如果实现了readResolve接口,则直接返回此方法指定的对象, 而不需要单独再创建新对象
 * @return
 */
private Object readResolve(){
    return instance;
}

 我们来说一下这样实现的原理:

我们通过源码来进行分析

public final Object readObject()
        throws IOException, ClassNotFoundException
    {
         /** if true, invoke readObjectOverride() instead of readObject() */
         /** 如果为true,调用readObjectOverride()而不是readObject(); */
         /** 因为我们这里的enableOverride是通过有参的构造器来进行给enableOverride复制的,而有参的构造器赋值为false */
        if (enableOverride) {
            return readObjectOverride();
        }

        // if nested read, passHandle contains handle of enclosing object
        int outerHandle = passHandle;
        try {
            Object obj = readObject0(false);
            handles.markDependency(outerHandle, passHandle);
            ClassNotFoundException ex = handles.lookupException(passHandle);
            if (ex != null) {
                throw ex;
            }
            if (depth == 0) {
                vlist.doCallbacks();
            }
            return obj;
        } finally {
            passHandle = outerHandle;
            if (closed && depth == 0) {
                clear();
            }
        }
    }
    public ObjectInputStream(InputStream in) throws IOException {
        verifySubclass();
        bin = new BlockDataInputStream(in);
        handles = new HandleTable(10);
        vlist = new ValidationList();
        enableOverride = false;
        readStreamHeader();
        bin.setBlockDataMode(true);
}

上面最重要的地方是在readObject0(false);这个地方,我们顺着这个地方往下面看

/**
 * Underlying readObject implementation.   底层readObject实现。
 */
private Object readObject0(boolean unshared) throws IOException {
    boolean oldMode = bin.getBlockDataMode();
    if (oldMode) {
        int remain = bin.currentBlockRemaining();
        if (remain > 0) {
            throw new OptionalDataException(remain);
        } else if (defaultDataEnd) {
            throw new OptionalDataException(true);
        }
        bin.setBlockDataMode(false);
    }

    byte tc;
    while ((tc = bin.peekByte()) == TC_RESET) {
        bin.readByte();
        handleReset();
    }

    depth++;
    try {
        switch (tc) {
            case TC_NULL:
                return readNull();

            case TC_REFERENCE:
                return readHandle(unshared);

            case TC_CLASS:
                return readClass(unshared);

            case TC_CLASSDESC:
            case TC_PROXYCLASSDESC:
                return readClassDesc(unshared);

            case TC_STRING:
            case TC_LONGSTRING:
                return checkResolve(readString(unshared));

            case TC_ARRAY:
                return checkResolve(readArray(unshared));

            case TC_ENUM:
                return checkResolve(readEnum(unshared));

            case TC_OBJECT:
                return checkResolve(readOrdinaryObject(unshared));

            case TC_EXCEPTION:
                IOException ex = readFatalException();
                throw new WriteAbortedException("writing aborted", ex);

            case TC_BLOCKDATA:
            case TC_BLOCKDATALONG:
                if (oldMode) {
                    bin.setBlockDataMode(true);
                    bin.peek();             // force header read
                    throw new OptionalDataException(
                        bin.currentBlockRemaining());
                } else {
                    throw new StreamCorruptedException(
                        "unexpected block data");
                }

            case TC_ENDBLOCKDATA:
                if (oldMode) {
                    throw new OptionalDataException(true);
                } else {
                    throw new StreamCorruptedException(
                        "unexpected end of block data");
                }

            default:
                throw new StreamCorruptedException(
                    String.format("invalid type code: %02X", tc));
        }
    } finally {
        depth--;
        bin.setBlockDataMode(oldMode);
    }
}

 因为我们的对象的类型默认是属于Object的,那么我们会进入到switch的checkResolve(readOrdinaryObject(unshared))方法中,我们再来看这个方法,它主要是通过读取流文件生成一个对象。

private Object readOrdinaryObject(boolean unshared)
    throws IOException
{
    if (bin.readByte() != TC_OBJECT) {
        throw new InternalError();
    }

    ObjectStreamClass desc = readClassDesc(false);
    desc.checkDeserialize();

    Class cl = desc.forClass();
    if (cl == String.class || cl == Class.class
            || cl == ObjectStreamClass.class) {
        throw new InvalidClassException("invalid class descriptor");
    }

    Object obj;
    try {
        obj = desc.isInstantiable() ? desc.newInstance() : null;
    } catch (Exception ex) {
        throw (IOException) new InvalidClassException(
            desc.forClass().getName(),
            "unable to create instance").initCause(ex);
    }

    passHandle = handles.assign(unshared ? unsharedMarker : obj);
    ClassNotFoundException resolveEx = desc.getResolveException();
    if (resolveEx != null) {
        handles.markException(passHandle, resolveEx);
    }

    if (desc.isExternalizable()) {
        readExternalData((Externalizable) obj, desc);
    } else {
        readSerialData(obj, desc);
    }

    handles.finish(passHandle);

    if (obj != null &&
        handles.lookupException(passHandle) == null &&
        desc.hasReadResolveMethod())
    {
        Object rep = desc.invokeReadResolve(obj);
        if (unshared && rep.getClass().isArray()) {
            rep = cloneArray(rep);
        }
        if (rep != obj) {
            handles.setObject(passHandle, obj = rep);
        }
    }

    return obj;
}

上面代码,最重要的在这一步Object rep = desc.invokeReadResolve(obj);这一步就已经告诉你返回的obj对象是什么了,那么我们重点看一下这个里面进行了什么操作,导致序列化没有破坏我们的单例的。上面这步我们通过断点调式可以看出在40~51行这个地方,我们有个判断就是判断我们要进行序列化的类中是否有定义过readResolve方法的,如果没有定义过这个方法则我们直接返回我们通过序列化产生的新的对象, 其实这个方法中obj再obj = desc.isInstantiable() ? desc.newInstance() : null;就已经创建了新的实例了。而我们在desc.hasReadResolveMethod中就已经判断了这个方法是否存在,存在我们获取的rep对象其实就是之前的单例对象,就会用之前的单例对象去替换这里通过序列化重新产生的对象,也就是用rep替换obj。

 

/** class-defined readResolve method, or null if none */
/** 类定义的readResolve方法,如果没有,则为null */
private Method readResolveMethod;
/**
 * Invokes the readResolve method of the represented serializable class and
 * returns the result.  Throws UnsupportedOperationException if this class
 * descriptor is not associated with a class, or if the class is
 * non-serializable or does not define readResolve.
 */
Object invokeReadResolve(Object obj)
    throws IOException, UnsupportedOperationException
{
    //判断是否有进行过初始化,如果没有会引发异常
    requireInitialized();
    //判断我们要序列化的类是否有定义过readResolve方法,如果定义过,则我们直接序列化一个空的对象
    if (readResolveMethod != null) {
        try {
            return readResolveMethod.invoke(obj, (Object[]) null);
        } catch (InvocationTargetException ex) {
            Throwable th = ex.getTargetException();
            if (th instanceof ObjectStreamException) {
                throw (ObjectStreamException) th;
            } else {
                throwMiscException(th);
                throw new InternalError(th);  // never reached
            }
        } catch (IllegalAccessException ex) {
            // should not occur, as access checks have been suppressed
            throw new InternalError(ex);
        }
    } else {
        throw new UnsupportedOperationException();
    }
}

然后看一下加上这个防止序列化之后得执行结果吧:

 单例模式(二)_第3张图片

可以看到,这样就防止了序列化破坏单例模式了。其它三种方式其实也是同样的道理啦,这里我们不做赘述了。 

  • 枚举单例模式如何防止反射和序列化所产生得漏洞的?

这里我们还是通过源码的方式来进行讲解

我们先来看反射方式

public static void main(String[] args) throws Exception {
    SingtonDemo5 s1 = SingtonDemo5.INSTACE;
    SingtonDemo5 s2 = SingtonDemo5.INSTACE;
    System.out.println(s1);
    System.out.println(s2);
    Constructor constructor = SingtonDemo5.class.getDeclaredConstructor();
    constructor.setAccessible(true);
    SingtonDemo5 s3 = constructor.newInstance();
    System.out.println(s3);
}

上述代码会在第六行这里报错,原因是我们在通过反射放回构造器的时候,用的是这个类无参的构造器,而我们看下枚举的源码它是没有无参构造器的,会导致报:java.lang.NoSuchMethodException,也就是方法找不到异常

单例模式(二)_第4张图片

我们换成枚举的有参构造器来试试就不会报上述这个错误了,但是会出现另外的一个问题,那就是

单例模式(二)_第5张图片 

 上面这个错误的意思就是”我们不能够通过反射去创建枚举对象

下面针对上述这个问题我们通过源码来进行分析:

@CallerSensitive
public T newInstance(Object ... initargs)
    throws InstantiationException, IllegalAccessException,
           IllegalArgumentException, InvocationTargetException
{
    //因为这里我们用的是父类的构造器,那么毫无疑问,这里是重写了父类的构造器,所以override为true
    if (!override) {
        if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
            Class caller = Reflection.getCallerClass();
            checkAccess(caller, clazz, null, modifiers);
        }
    }
    if ((clazz.getModifiers() & Modifier.ENUM) != 0)
        throw new IllegalArgumentException("Cannot reflectively create enum objects");
    ConstructorAccessor ca = constructorAccessor;   // read volatile
    if (ca == null) {
        ca = acquireConstructorAccessor();
    }
    @SuppressWarnings("unchecked")
    T inst = (T) ca.newInstance(initargs);
    return inst;
}

我们这里重点来看一下13行代码,因为上述的错误正式因为这行代码导致的,这里class.getModifiers()这个方法的意思是(访问该类或接口的语言修饰,返回类型为int类型

这行代码我们第一个表达式求得是当前类或接口语言修饰符对应得值,这个值得计算,弄懂了其实也是非常简单的。不过这个我们需要通过看java SE规范的类文件结构才能够去了解这个值是如何计算的。下面我用一个图形来进行说明

 单例模式(二)_第6张图片

这里我们的SingtonDemo5为枚举,所以我们这个类对应的ACC_ENUM是必须有的,然后因为我们这个类public修饰的,所以ACC_PUBLIC也是存在的,然后怎么看其它的是否存在了,这里我们只能通过反编译了,但是我这里有试过反编译,它其实是看不到我们想要的真实结果了,下面我先告诉大家结论,我们再去论证这个事实。当一个类被定义成枚举的时候,那么他就是不能够被继承的,也就是说自定义枚举一定是被final修饰的。这里所说的枚举不是Enum>源码,而单单只是指代我们自己所定义的枚举类型。下面先给大家看看反编译后的结果吧

单例模式(二)_第7张图片

你会发现,我们的类标识符依赖只是多了一个静态标识,其实反编译对枚举类型是由隐藏其特性的,这里有兴趣的朋友可以去了解一下。我们这里通过反汇编语言通过查看字节码的执行顺序来看一下实际自定义枚举是什么样子的这里我们使用javap fileName.class这个命令来实现。

单例模式(二)_第8张图片

 

其实上图通过类结构图我们就可以了解到我们SingtonDemo5用到的修饰符有public final Enum,所以我们这里需要通过组合相加这些属性值得到最终的证书编码。这里我们来简单说一下计算过程吧

我们通过类的反汇编可以看到我们可以对应的上图的标志名称有3个,ACC_PUBLIC,ACC_FINAL,ACC_ENUM,这上个,通过计算16进制数我们可以得到最终结果为1 + 16 + 16384 = 16401,这里算的刚好与我们断点看到的结果一致,证明我们的想法是没有错误的,我们在看一下后面的要进行逻辑与运算的值是多少,可以直接看到源码定义的是static final int ENUM = 0x00004000;那么我们通过&运算(只有二个数都为1的时候才计算得1,其余都计算得0)可以计算出如下结果

单例模式(二)_第9张图片

 这里的计算大家也可以通过程序员计算器去进行计算得出如下结果,不错总之知道原理还是最好的。然后就会抛出上述所说的异常了,这也正式为什么枚举对象可以避免反射破坏其单例的原因了。

  • 接下来我们来看枚举对象是如何避免序列化产生的漏洞的了

先看测试代码

public static void main(String[] args) throws Exception {
        SingtonDemo5 s1 = SingtonDemo5.INSTACE;
        SingtonDemo5 s2 = SingtonDemo5.INSTACE;
        System.out.println(s1);
        System.out.println(s2);
        /**
         * 通过序列化方式来获取单例对象
         */
        /**
         * 1.要序列化的类需要实现序列化接口,把对象永久的保存在磁盘中
         */
        FileOutputStream fops = new FileOutputStream("d:/a.txt");
        ObjectOutputStream oos = new ObjectOutputStream(fops);
        oos.writeObject(s1);
        /**
         * 2.通过反序列化方式把对象从磁盘中读取出来
         */
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("d:/a.txt"));
        SingtonDemo5 sington = (SingtonDemo5) ois.readObject();
        System.out.println("破解后的对象"+sington);
    }

我们主要只看这一段源码,就可以从中找到答案了

/**
 * Reads in and returns enum constant, or null if enum type is
 * unresolvable.  Sets passHandle to enum constant's assigned handle.
 */
private Enum readEnum(boolean unshared) throws IOException {
    if (bin.readByte() != TC_ENUM) {
        throw new InternalError();
    }

    ObjectStreamClass desc = readClassDesc(false);
    if (!desc.isEnum()) {
        throw new InvalidClassException("non-enum class: " + desc);
    }

    int enumHandle = handles.assign(unshared ? unsharedMarker : null);
    ClassNotFoundException resolveEx = desc.getResolveException();
    if (resolveEx != null) {
        handles.markException(enumHandle, resolveEx);
    }

    String name = readString(false);
    Enum result = null;
    Class cl = desc.forClass();
    if (cl != null) {
        try {
            @SuppressWarnings("unchecked")
            Enum en = Enum.valueOf((Class)cl, name);
            result = en;
        } catch (IllegalArgumentException ex) {
            throw (IOException) new InvalidObjectException(
                "enum constant " + name + " does not exist in " +
                cl).initCause(ex);
        }
        if (!unshared) {
            handles.setObject(enumHandle, result);
        }
    }

    handles.finish(enumHandle);
    passHandle = enumHandle;
    return result;
}

我们主要看一下27行这里其实这里就是返回的我们在定义枚举类第一行所定义的实例,所以不论如何,它总能够返回一个完全一样的实例对象。

综上所述:枚举可以防止反射和反序列化漏洞,而其它几种单例模式都需要通过自己处理才能够防止,不然产生的对象不是单实例的。

 

 

你可能感兴趣的:(设计模式)