Javaweb安全——Java动态代理

Java 动态代理

Java的java.lang.reflect包下提供了一个Proxy类和InvocationHandler接口,可以生成JDK动态代理类或对象来完成程序无侵入式扩展(即不通过继承接口编写实现类来完成功能拓展)。

Java动态代理主要使用场景:

  1. 统计方法执行所耗时间。
  2. 在方法执行前后添加日志。
  3. 检测方法的参数或返回值。
  4. 方法访问权限控制。
  5. 方法Mock测试。

动态代理

Java的代理感觉是,给原来的操作前后加上了增强代码,像是类的包装。

静态代理通过编写一个代理类实现,如下面这个例子,ProxysaveLog类作为saveLog的代理类,两者都是实现了Log接口。

这里注意不要和装饰模式搞混:

静态代理在编译时已经确定代理的具体对象,装饰模式是在运行动态的构造

interface Log {
    void save(String name);
}
class saveLog implements Log{
    @Override
    public void save(String log) {
        System.out.println("Good morning, " + log);
    }
}
public class ProxysaveLog implements Log {
    private Log proxied;
    // 不传入被代理的类,直接声明,所以一个静态代理对应一个类,这是与装饰者模式的主要差别。
    private ProxysaveLog(){
        this.proxied = new saveLog();
    }
    public static void main(String[] args) {
        Log log = new ProxysaveLog();
        log.save("log20220403233728-save");
    }
    public void save(String log) {
        System.out.println("Before invoke saveLog" );
        proxied.save(log);
        System.out.println("After invoke saveLog");
    }
}

静态代理需要手动为每一个目标类编写对应的代理类,而动态代理则直接得到代理类的Class对象,然后通过反射创造实例,从而避免了大量的重复劳动。

创建动态代理类会使用到java.lang.reflect.Proxy类和java.lang.reflect.InvocationHandler接口。

java.lang.reflect.Proxy主要用于生成动态代理类Class、创建代理类实例,该类实现了java.io.Serializable接口。

Proxy类:

static Class<?> getProxyClass(
    ClassLoader loader, //指定代理类的类加载器
    Class<?>... interfaces  //要实现的代理类的接口列表。 
)
//创建一个代理类所对应的Class对象。
    
static Object newProxyInstance(
    ClassLoader loader,  //指定代理类的类加载器。 
 	Class<?>[] interfaces,    //目标对象实现的接口的类型
 	InvocationHandler h      //事件处理器
) 
//返回一个指定接口的代理类实例,该接口可以将方法调用指派到指定的调用处理程序。

InvocationHandler类:java.lang.reflect.InvocationHandler接口用于调用Proxy类生成的代理类方法。

 Object invoke(Object proxy, Method method, Object[] args) 
// 在代理实例上处理方法调用并返回结果。
获取动态代理对象

由于是用反射生成动态代理,所以要想获取动态代理对象,先得获取一个动态代理类的Class对象,再用获取构造方法,最后调用newInstance方法去实例化。

public interface Foo{
    void foo();
}
public class DynamicAgent {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        InvocationHandler handler = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("正在执行的方法:"+method);
                return null;
            }
        };
        //生成一个动态代理类的Class对象
        Class proxyClass = Proxy.getProxyClass(Foo.class.getClassLoader(),Foo.class);
        //获取proxyClass类中带一个InvocationHandler参数的构造器
        Constructor constructor = proxyClass.getConstructor(InvocationHandler.class);
        //调用构造器的newInstance方法来创建实例
        Foo f = (Foo)constructor.newInstance(handler);
        System.out.println(f.getClass());
        f.foo();
    }
}

getProxyClass()方法从传入的接口的Class(被代理)中,“拷贝”类结构信息到一个新的带有构造器的Class对象中(即下图当中的class $Proxy0),就可以像正常的类一样创建对象了。

在这里插入图片描述

动态调试可见,constructor实际是获取的Proxy的构造方法,所以接口能够实例化就说得通了,不是凭空捏出个构造方法的。

protected InvocationHandler h;
protected Proxy(InvocationHandler h) {
    Objects.requireNonNull(h);
    this.h = h;
}

根据代理Class的构造器创建对象时,需要传入InvocationHandler。通过构造器传入一个引用,就是由这个成员变量h去接受的。

在这里插入图片描述

在newInstance方法被调用时,Proxy(InvocationHandler h)构造方法这里的变量h实际为DynamicAgent的一个内部匿名类DynamicAgent$1。因为InvocationHandler也是一个接口,其中的invoke方法是没有被实现的,所以在一开始就先new了一个InvocationHandler()给handler变量,并实现了invoke函数。

InvocationHandler是一个函数式接口(一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口)。

函数式接口可以被隐式转换为 lambda 表达式。

所以像下面这样写也是可以的。

InvocationHandler handler = (proxy, method, args1) -> {
    System.out.println("正在执行的方法:"+method);
    return null;
};

而一开始的写法是在Java8之前的Java 匿名类,就是一种语法糖,可以使代码更加简洁。

下面的示例将invoke的实现分开写到被代理类中

在这里插入图片描述

再去动态调试handler就是MyInvocationHandler的实例了。

实际当中getProxyClass这个方法并不常用,多使用newProxyInstance方法来实现动态代理,直接返回代理实例,连中间得到代理Class对象的过程都帮你隐藏:

动态生成了代理类 Class 的字节码byte[],然后通过defineClass0方法,如之前提到的ClassLoader当中方法名为defineClassXXXnative方法,动态的向JVM创建一个类对象

所以上面那一坨就可以变成一句代码了(虽然有点长)

Foo f = (Foo)Proxy.newProxyInstance(Foo.class.getClassLoader(),new Class[]{Foo.class},
(proxy, method, args1) -> {
System.out.println("正在执行的方法:"+method);
return null;
});

在执行代理对象的任意方法时,实际都是去执行InvocationHandler对象的invoke方法。

在这里插入图片描述

上面给出的实例主要是解释下动态代理的过程,离使用还有一些距离,invoke还没有返回值、动态代理实例不能动态生成(需要一个动态工厂类)。

通常在使用动态代理时会有一个或多个实现类如:

public class RunFoo implements Foo{
    @Override
    public void foo() {
        System.out.println("execute Foo");
    }
}

还会有一个工具类,用于存放方法调用前后的增强方法:

public class FooUtil {
    //开始的增强方法
    public void before(){
        System.out.println("Before invoke saveLog");
    }
    //结束的增强方法
    public void after(){
        System.out.println("After invoke saveLog");
    }
}

通过动态代理工厂类实现动态代理对象的自动生成

public class MyProxyFactory {
    private static Object getProxy(final Object target) throws Exception {
        Object proxy = Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                (proxy1, method, args) -> {
                    FooUtil util = new FooUtil();
                    util.before();
                    //通过反射以target为主调来执行method方法
                    //回调了target对象的原有方法
                    Object result = method.invoke(target, args);
                    util.after();
                    return result;
                }
        );
        return proxy;
    }
}

用来测试的主程序为:

public class DynamicAgent {
    public static void main(String[] args) throws Exception {
        Foo target = new RunFoo();
        Foo Foo = (Foo)MyProxyFactory.getProxy(target);
        Foo.foo();
    }
}

动态代理可以灵活的实现解耦合,程序执行RunFoo当中的foo方法时候在前后插入了增强方法,但在RunFoo的方法中并没有硬编码调用before()和after()方法。

这种动态代理在AOP(面向切面编程)中被称为AOP代理,AOP代理可代替目标对象,其中包含了目标代理的所有方法,但可以在方法前后插入一些通用处理。

动态代理类生成的$ProxyXXX类代码分析

java.lang.reflect.Proxy类是通过创建一个新的Java类(类名为com.sun.proxy.$ProxyXXX)的方式来实现无侵入的类方法代理功能的。

动态代理生成出来的类有如下技术细节和特性:

  1. 动态代理的必须是接口类,通过动态生成一个接口实现类来代理接口的方法调用(反射机制)。
  2. 动态代理类会由java.lang.reflect.Proxy.ProxyClassFactory创建。
  3. ProxyClassFactory会调用sun.misc.ProxyGenerator类生成该类的字节码,并调用java.lang.reflect.Proxy.defineClass0()方法将该类注册到JVM
  4. 该类继承于java.lang.reflect.Proxy并实现了需要被代理的接口类,因为java.lang.reflect.Proxy类实现了java.io.Serializable接口,所以被代理的类支持**序列化/反序列化**。
  5. 该类实现了代理接口类(示例中的接口类是com.anbai.sec.proxy.FileSystem),会通过ProxyGenerator动态生成接口类(FileSystem)的所有方法,
  6. 该类因为实现了代理的接口类,所以当前类是代理的接口类的实例(proxyInstance instanceof FileSystemtrue),但不是代理接口类的实现类的实例(proxyInstance instanceof UnixFileSystemfalse)。
  7. 该类方法中包含了被代理的接口类的所有方法,通过调用动态代理处理类(InvocationHandler)的invoke方法获取方法执行结果。
  8. 该类代理的方式重写了java.lang.Object类的toStringhashCodeequals方法。
  9. 如果动过动态代理生成了多个动态代理类,新生成的类名中的0会自增,如com.sun.proxy.$Proxy0/$Proxy1/$Proxy2


在这里插入图片描述

图片来自:https://www.jianshu.com/p/9bcac608c714

https://qiankunli.github.io/2020/04/09/java_dynamic_proxy.html

动态代理类实例序列化问题

动态代理类符合Java对象序列化条件(class Proxy implements java.io.Serializable),并且在序列化/反序列化时会被ObjectInputStream/ObjectOutputStream特殊处理。

类对象和动态代理对象也得实现Serializable接口,所以把上面那个demo稍微改一下:

public class RunFoo implements Foo, Serializable {
    @Override
    public void foo() {
        System.out.println("execute Foo");
    }
}

再把newProxyInstance方法那的InvocationHandler h参数单独写一个类去调用,不用匿名类或者lambda的写法了,因为要实现Serializable接口

public class MyInvocationHandler implements InvocationHandler, Serializable {
    private Object target;
    MyInvocationHandler(Object target){
        this.target = target;
    }
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        FooUtil util = new FooUtil();
        util.before();
        Object result = method.invoke(target, args);
        util.after();
        return result;
    }
}

测试动态代理类实例序列化的主程序为:

public class ProxySerializationTest {

    public static void main(String[] args) {
        try {
            Foo target = new RunFoo();
            Foo Foo = (Foo)Proxy.newProxyInstance(
                    target.getClass().getClassLoader(),
                    target.getClass().getInterfaces(),
                    new MyInvocationHandler(target)
            );

            ByteArrayOutputStream baos = new ByteArrayOutputStream();

            // 创建Java对象序列化输出流对象
            ObjectOutputStream out = new ObjectOutputStream(baos);

            // 序列化动态代理类
            out.writeObject(Foo);
            out.flush();
            out.close();

            // 利用动态代理类生成的二进制数组创建二进制输入流对象用于反序列化操作
            ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());

            // 通过反序列化输入流(bais),创建Java对象输入流(ObjectInputStream)对象
            ObjectInputStream in = new ObjectInputStream(bais);

            // 反序列化输入流数据为FileSystem对象
            Foo test = (Foo) in.readObject();

            System.out.println("反序列化类实例类名:" + test.getClass());
            System.out.println("反序列化类实例toString:" + test.toString());
        } catch (IOException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

动态代理生成的类在反序列化/反序列化时不会序列化该类的成员变量,并且serialVersionUID0L ,也将是说将该类的Class对象传递给java.io.ObjectStreamClass的静态lookup方法时,返回的ObjectStreamClass实例将具有以下特性:

  1. 调用其getSerialVersionUID方法将返回0L
  2. 调用其getFields方法将返回长度为零的数组。
  3. 调用其getField方法将返回null

但其父类(java.lang.reflect.Proxy)在序列化时不受影响,父类中的h变量(InvocationHandler)将会被序列化,这个h存储了动态代理类的处理类实例以及动态代理的接口类的实现类的实例。

动态代理生成的对象(com.sun.proxy.$ProxyXXX)序列化的时候会使用一个特殊的协议:TC_PROXYCLASSDESC(0x7D),这个常量在java.io.ObjectStreamConstants中定义的。在反序列化时也不会调用java.io.ObjectInputStream类的resolveClass方法而是调用resolveProxyClass方法来转换成类对象的。

详细描述请参考:Dynamic Proxy Classes-Serialization

动态代理类的实例序列化在Java反序列漏洞当中是相对重要的一环,比如CC1链中就有用到AnnotationInvocationHandler(JDK1.7及以下)

在这里插入图片描述

具体调用链分析在之前的初识Java反序列化漏洞这篇文章已经写过了,这里只是提一嘴。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值