Java安全 - RMI源码分析

RMI远程服务创建流程分析

1、远程对象创建过程

首先步入对象的构造方法

下一步

这里步入了父类UnicastRemoteObject的构造函数,传入一个参数port,作用是将远程对象随即发布到一个端口,此时默认值为0

这里对端口以及相关属性初始化,主要关注构造方法中的exportObject方法,将远程对象转换为Remote类型,与port端口传入方法中。两个被赋值null的参数与RMI获取客户端或服务端套接字有关。

进入exportObject方法

返回值的两个参数,第一个参数obj为创建的远程对象,第二个参数中新建了一个UnicastServerRef对象,并传入了端口port

继续跟进第二个参数

UnicastServerRef构造函数中调用父类构造器,创建一个LiveRef对象作为参数。var1=port

跟进LiveRef

ObjID应该与UID相关,目前不深究,继续跟进构造方法

到此已经可以看到与网络通信相关的类了,跟进TCPEndpoint,在跟进getLocalEndpoint方法之前,先来看看这个类的构造函数

发现了host和port属性被赋值了,就是在这个构造函数中,确定了远程对象的主机与端口

再来看getLocalEndpoint方法

后两个参数与RMI获取套接字有关,由于传入的值为null,不做分析

最终返回一个192.168.56.1:0的 地址:端口

回到LiveRef构造方法

可以知道通过第二个参数获取了远程对象需要的地址与端口,继续跟进LiveRef方法

this.ep被赋值为Endpoint,与远程对象的地址端口有关。

重点看一下ep

其中包括了hostport,以及一个非常重要的transport对象,transport才是真正处理网络请求的对象,TCPEndpoint是一层封装。

赋值完成后继续跟进,会回到UnicastServerRef调用父类构造器的地方。

父类构造器只是对ref属性赋值为创建的LiveRef对象

到这里,说明一下UnicastServerRefUnicastRef的关系

UnicastRef是UnicastServerRef的父类,UnicastServerRef继承UnicastRef使用于服务端,后者直接使用于客户端。

继续跟进回到exportObject方法

到目前为止,创建的LiveRef对象已经包含在sref中,并将sref赋值给远程对象obj的ref中。到此远程对象与需要的地址,端口等信息已经成功绑定。

继续跟进sref.exportObject

在这个方法中,为远程对象创建了一个代理,也就是客户端需要调用时使用的stub,在后续创建注册表后,将远程对象的stub放到注册中心,后续客户端从注册表获取stub进行远程调用。

跟进createProxy

当前的if判断,若进入会创建stub,条件主要根据stubClassExists值判断

可以看到stubClassExists中会尝试获取类名拼接_Stub后的类,即是否是jdk中已经存在的类,这里主要在注册表相关操作中生效。

不进入if语句中,之后进入动态代理的创建流程

获取类加载器,获取远程接口,获取调用处理器,使用三个参数创建代理

类加载器为AppClassLoader

远程接口中包含了远程对象接口RemoteObj等信息

处理器本质上还是LiveRef的封装

回到exportObject

这个判断创建的stub是否是RemoteStub的实例,这里判否,跳过

跟进Target构造方法

最重要的两个属性是dispstub,具体看看他们封装的内容

dispUnicastServerRef类型,而stub代理中的refUnicastRef类型,而stub是需要发布到注册表中,提供客户端获取使用的,其中都封装了同一个LiveRef,这也是RMI进行通信的核心,即使用LiveRef提供服务端和客户端使用。

其中还有一个比较重要的属性weakImpl

这里包含了接口的具体实现。

创建好Target后,执行ref.exportObject

跟进该方法

listen方法,这个是开启网络监听的方法。跟进listen

主要看newServerSocket方法,这个方法创建了用于监听的socket

继续跟进

端口是通过createServerSocket方法修改的,继续跟进

ServerSocket中,若将端口设为0,操作系统会为服务器自动分配一个端口,远程对象的端口在这个位置被修改了,listen根据hostport创建监听。

到此服务端远程对象的创建已经结束。接下来还需要在服务端对发布的对象进行记录。

调用ObjectTable.putTarget方法,其中有两个put方法。

var0为创建的Target对象,使用put方法将target的信息保存在系统的两个表中,到此服务端远程对象创建流程介绍完毕。

总结一下,在服务端创建远程对象过程中,核心为LiveRefLiveRef对象中包含了远程对象占用的地址和端口,以及对象UID。当一个远程对象被创建时,会生成一个服务器本地的RemoteObject对象,它持有一个UnicastServerRef对象,UnicastServerRef对象持有一个LiveRef对象;远程对象被创建时,还会生成一个UnicastRef对象,并封装到stub代理中,它持有与UnicastServerRef相同的LiveRefstub后续发布到注册表,以供客户端使用。

2、注册中心创建以及远程对象发布流程分析

//创建注册表,设置端口1099
Registry registry = LocateRegistry.createRegistry(1099);

通过设定端口创建注册中心

createRegistry通过new RegistryImpl返回一个Registry对象,进入RegistryImpl构造方法下断点,分析对象的创建过程

这里是安全检测,直接跳过

根据idport创建LiveRef流程与上文相同,不再赘述

分析setup方法

这里需要了解UnicastServerRef对象的创建参数,第一个参数用于封装LiveRef对象,第二个参数RegisImpl::registryFilter是一个反序列化的过滤器,跟进分析

private static Status registryFilter(FilterInfo var0) {
        if (registryFilter != null) {
            Status var1 = registryFilter.checkInput(var0);
            if (var1 != Status.UNDECIDED) {
                return var1;
            }
        }

        if (var0.depth() > 20L) {
            return Status.REJECTED;
        } else {
            Class var2 = var0.serialClass();
            if (var2 != null) {
                if (!var2.isArray()) {
                    return String.class != var2 && !Number.class.isAssignableFrom(var2) && !Remote.class.isAssignableFrom(var2) && !Proxy.class.isAssignableFrom(var2) && !UnicastRef.class.isAssignableFrom(var2) && !RMIClientSocketFactory.class.isAssignableFrom(var2) && !RMIServerSocketFactory.class.isAssignableFrom(var2) && !ActivationID.class.isAssignableFrom(var2) && !UID.class.isAssignableFrom(var2) ? Status.REJECTED : Status.ALLOWED;
                } else {
                    return var0.arrayLength() >= 0L && var0.arrayLength() > 1000000L ? Status.REJECTED : Status.UNDECIDED;
                }
            } else {
                return Status.UNDECIDED;
            }
        }
    }

在jdk1.8.121之后,加入了该过滤器用于校验传入的反序列化类,只有白名单内的类允许序列化,否则会抛出REJECTED

进入sun.rmi.registry.RegistryImpl#setup

UnicastServerRef对象赋值给ref后,调用UnicastServerRefexportObject方法,这里可以发现,与远程对象创建时调用的UnicastServerRef.exportObject是同一个方法,但区别在于第三个参数为true,第三个参数是判断传入对象是临时对象还是永久对象,由于这里的RegistryImpl是jdk中已经实现的类,所以第三个参数为true。

进入sun.rmi.server.UnicastServerRef#exportObject

与远程对象创建相同,需要创建一个stub代理。

进入sun.rmi.server.Util#createProxy

走到第一个if语句

这一步是关键,会通过stubClassExists对传进来的对象进行判断

进入sun.rmi.server.Util#stubClassExists

在上文中提到过,这里会将类名与_Stub拼接,可以看到类名拼接后是已经存在的类,返回true。

回到sun.rmi.server.Util#createProxy

根据方法名,这是创建Stub代理的方法,传入第一个参数是第二个参数是提供客户端使用的UnicastRef,本质还是一个LiveRef的封装,第二个参数是RegistryImpl类。

进入sun.rmi.server.Util#createStub

RegistryImpl_Stub拼接,获取存在类名,获取有参构造方法,并返回一个RegistryImpl_Stub类的实例化,且对象强转为RemoteStub类型。

继续调试,返回到sun.rmi.server.UnicastServerRef#exportObject

由于var5为上述步骤返回的RemoteStub类型对象,会进入if语句,执行setSkeleton

进入sun.rmi.server.UnicastServerRef#setSkeleton

关键在createSkeleton方法,进入sun.rmi.server.Util#createSkeleton

这里将RegistryImpl_Skel拼接,并通过拼接后的类名在不进行静态初始化的情况下返回一个RegistryImpl_Skel对象。

之后返回到sun.rmi.server.UnicastServerRef#exportObject

与上文一样,将创建的对象封装为一个Target,这里可以分析一个Target里比较重要的内容。

与远程对象的Target相同,重要的是dispstub,在这里disp与远程对象相同,都是一个UnicastServerRef对象,封装了其他信息,不同的是skel是一个RegistryImpl_Skel对象,在RMI流程分析的时候,有提到过RMI中远程对象的调用并不直接通过客户端和服务端交互,而是通过在服务端创建一个代理Skeleton,客户端创建一个代理Stub,通过代理进行操作,这里的skel就是服务端的代理。stub的类型也变成了RegistryImpl_Stub,这也对应了客户端的代理,而其中的LiveRef依然是同一个对象,提供双端进行远程通信,LiveRef的端口1099,即注册中心占用的端口。

接下来就是Target发布过程,与远程对象发布相似,通过listen创建监听,到此已经完成注册中心的创建,接下来还需要将远程连接的对象进行记录。

进入sun.rmi.transport.Transport#exportObject

关键在putTarget,进入sun.rmi.transport.ObjectTable#putTarget

重点依然放在两个put方法上

查看objTable表和implTable表,其中都有三个对象

以objTable表为例,看看三个对象都是什么

第一个对象可以看到RegistryImpl_SkelRegistryImpl_Stub,这是刚刚创建用于注册中心创建的对象

第二个对象的stub是一个DGCImpl_Stub,这是一个默认创建的分布式垃圾回收类。

第三个对象是创建的RemoteObjImpl类远程对象。

到此注册中心的创建过程分析完毕。

3、注册中心绑定远程对象流程分析

主要分析使用注册中心进行绑定的两种方法

//重绑定
registry.rebind("Hello", remoteObj);
//绑定
registry.bind("Hello", remoteObj);

registry.rebind

进入sun.rmi.registry.RegistryImpl#rebind

这里调用了hashtable.put方法,以键值对的方式,将远程对象保存在注册中心的一个表中。

可以看到注册中心的bindings是一个Hashtable,以键值对的形式保存了RemoteObjImpl远程对象。

registry.bind

进入sun.rmi.registry.RegistryImpl#bind

bind方法通过同步锁阻塞防止多个线程访问,并检查绑定的键名name是否已经存在,若存在抛出AlreadyBoundException异常。

若不存在,调用put方法绑定远程对象。

bind与rebind的区别

  • bind会检查name是否已经存在,若name存在抛出AlreadyBoundException异常,若不存在绑定远程对象。
  • rebind不检查name,直接绑定远程对象,若name存在,将新的远程对象覆盖,若不存在绑定远程对象。

4、客户端获取注册中心

Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);

进入java.rmi.registry.LocateRegistry#getRegistry(java.lang.String, int)

进入java.rmi.registry.LocateRegistry#getRegistry(java.lang.String, int, java.rmi.server.RMIClientSocketFactory)

首先对porthost是否合法进行判断,然后通过传入的hostport创建一个LiveRef,并通过这个LiveRef创建一个RemoteRef。最后创建一个代理。

进入sun.rmi.server.Util#createProxy

由于在客户端使用,通过var2=false,创建一个stub,在获取注册中心时,stub的获取并不是通过远程传输到客户端,而是通过客户端设置参数,本地创建一个拥有相同LiveRefstub,说明了在获取注册中心的过程中,客户端与注册中心之间没有交互。

这样客户端已经获取到了注册中心的地址和端口,可以通过这一个stub向注册中心获取远程对象的stub

5、客户端获取远程对象

RemoteObj remoteObj = (RemoteObj) registry.lookup("Hello");

获取远程对象涉及到注册中心和客户端的交互,需要从两个方面分析

客户端

进入sun.rmi.registry.RegistryImpl_Stub#lookup

首先创建了一个StreamRemoteCall对象,再将传入的键名Hello写进输出流进行序列化。

this.ref是一个UnicastRef对象

进入sun.rmi.server.UnicastRef#invoke(java.rmi.server.RemoteCall)

进入sun.rmi.transport.StreamRemoteCall#executeCall

关键方法在releaseOutputStream中。

进入sun.rmi.transport.StreamRemoteCall#releaseOutputStream

this.out.flush会将输出流中缓冲数据发送给注册中心。

回到sun.rmi.transport.StreamRemoteCall#executeCall

从输入流中获得注册中心返回的数据,并对数据进行解析。

回到sun.rmi.registry.RegistryImpl_Stub#lookup

获取StreamRemoteCall对象的输入流,并进行反序列化,即读出从注册中心获取的代理。

注册中心

首先要解决的问题是在哪里下断点?

stub对应的,本地通过skel代理操作远程对象,与skel相关的类是RegistryImpl_Skel,其中有dispatch方法,接下来找哪里调用了dispatch方法。注册中心在创建后处于监听状态,和监听相关的方法是listen,可以在listen里看看。

进入sun.rmi.transport.tcp.TCPTransport#listen

在这里开启了一个线程,查看AcceptLooprun方法(当前文件)

进入sun.rmi.transport.tcp.TCPTransport.AcceptLoop#executeAcceptLoop

executeAcceptLoop里创建了一个线程池,在看ConnectionHandlerrun方法

run0方法,由于其中有很多case,无法准确定位,从case前的if开始下断

关键方法handleMessages

进入sun.rmi.transport.tcp.TCPTransport#handleMessages

走到了case 80

进入sun.rmi.transport.Transport#serviceCall

通过getTarget方法拿到Target对象,getTarget方法从objTable表中获取已经保存的Target对象。

再通过getDispatcher方法获得Taregtdisp分发器。

接下来的通过分发器调用dispatch方法,传入Target对象和RemoteCall对象。

进入sun.rmi.server.UnicastServerRef#dispatch

判断服务端代理skel不为空后,调用oldDispatch

进入sun.rmi.server.UnicastServerRef#oldDispatch

再调用skel.dispatch方法

进入sun.rmi.registry.RegistryImpl_Skel#dispatch

看到进入case 2中,进行反序列化读出 Hello,调用RegistryImpllookup方法获取键名对应的远程对象引用。

进入sun.rmi.registry.RegistryImpl#lookup

通过键名,向注册中心的bindings表中获取远程对象引用,返回一个RemoteObjImpl对象。

回到sun.rmi.registry.RegistryImpl_Skel#dispatch

将获得的远程对象引用序列化发送给客户端。

6、客户端请求服务端

客户端

这里通过反射调用方法

进入java.rmi.server.RemoteObjectInvocationHandler#invoke

进入java.rmi.server.RemoteObjectInvocationHandler#invokeRemoteMethod

判断是否为Remote代理后调用Unicast的invoke方法,这里传入了从注册中心获取的代理以及需要调用的方法

进入sun.rmi.server.UnicastRef#invoke(java.rmi.Remote, java.lang.reflect.Method, java.lang.Object[], long)

newConnection()创建一个与服务端的连接

接下来调用marshalValue方法

进入sun.rmi.server.UnicastRef#marshalValue

这个方法会判断传入的参数类型,并进行序列化

回到sun.rmi.server.UnicastRef#invoke(java.rmi.Remote, java.lang.reflect.Method, java.lang.Object[], long)

executeCall方法用于客户端处理双方通信过程

进入sun.rmi.transport.StreamRemoteCall#executeCall

进入sun.rmi.transport.StreamRemoteCall#releaseOutputStream

可以看到在该方法里将数据发送给服务端

回到sun.rmi.transport.StreamRemoteCall#executeCall

接下来是获取输入缓冲区的过程

回到sun.rmi.server.UnicastRef#invoke(java.rmi.Remote, java.lang.reflect.Method, java.lang.Object[], long)

首先根据传入的方法,判断是否存在返回值,若没有返回值直接返回,若存在返回值,通过上述的输入缓冲区获取数据,关键方法unmarshalValue

进入sun.rmi.server.UnicastRef#unmarshalValue

该方法判断远程对象调用方法的返回类型,并将缓冲区数据反序列化。

回到sun.rmi.server.UnicastRef#invoke(java.rmi.Remote, java.lang.reflect.Method, java.lang.Object[], long)

已经获得了服务端的处理结果 Hello World

到此远程对象调用的服务端通信过程分析完毕

服务端

依然是listen创建监听端口,定位dispatch方法

进入sun.rmi.transport.Transport#serviceCall

第一次到达断点位置,可以看到是一个注册中心的代理,这是因为实验的注册中心与服务端搭建在同一环境,这个过程是上述注册中心对客户端获取远程调用对象的响应。

第二次到达断点是DGC垃圾回收相关过程,这里不做讨论

进入sun.rmi.server.UnicastServerRef#dispatch

第三次到达断点,可以看到skel=null,跳过判断不进入oldDispatch方法,这是客户端对服务端请求调用远程对象的过程

获取输入流以及服务端请求的对象方法

获取输入流数据并进行反序列化

将反序列化得到的数据通过反射的方式调用服务端请求的对象方法

判断是否存在返回值,若存在则需要将调用方法得到的返回值序列化

将序列化数据即Hello World发送给服务端

到此客户端请求服务端的服务端响应过程分析完毕。

posted @ 2023-01-29 02:29  PIAOMIAO1  阅读(368)  评论(1编辑  收藏  举报