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

其中包括了host与port,以及一个非常重要的transport对象,transport才是真正处理网络请求的对象,TCPEndpoint是一层封装。
赋值完成后继续跟进,会回到UnicastServerRef调用父类构造器的地方。

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

到这里,说明一下UnicastServerRef与UnicastRef的关系
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构造方法

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

disp是UnicastServerRef类型,而stub代理中的ref是UnicastRef类型,而stub是需要发布到注册表中,提供客户端获取使用的,其中都封装了同一个LiveRef,这也是RMI进行通信的核心,即使用LiveRef提供服务端和客户端使用。
其中还有一个比较重要的属性weakImpl

这里包含了接口的具体实现。
创建好Target后,执行ref.exportObject

跟进该方法

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

主要看newServerSocket方法,这个方法创建了用于监听的socket。
继续跟进

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

在ServerSocket中,若将端口设为0,操作系统会为服务器自动分配一个端口,远程对象的端口在这个位置被修改了,listen根据host和port创建监听。
到此服务端远程对象的创建已经结束。接下来还需要在服务端对发布的对象进行记录。


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

var0为创建的Target对象,使用put方法将target的信息保存在系统的两个表中,到此服务端远程对象创建流程介绍完毕。
总结一下,在服务端创建远程对象过程中,核心为LiveRef,LiveRef对象中包含了远程对象占用的地址和端口,以及对象UID。当一个远程对象被创建时,会生成一个服务器本地的RemoteObject对象,它持有一个UnicastServerRef对象,UnicastServerRef对象持有一个LiveRef对象;远程对象被创建时,还会生成一个UnicastRef对象,并封装到stub代理中,它持有与UnicastServerRef相同的LiveRef,stub后续发布到注册表,以供客户端使用。
2、注册中心创建以及远程对象发布流程分析
//创建注册表,设置端口1099
Registry registry = LocateRegistry.createRegistry(1099);
通过设定端口创建注册中心

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

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

根据id和port创建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后,调用UnicastServerRef的exportObject方法,这里可以发现,与远程对象创建时调用的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相同,重要的是disp和stub,在这里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_Skel和RegistryImpl_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)

首先对port和host是否合法进行判断,然后通过传入的host和port创建一个LiveRef,并通过这个LiveRef创建一个RemoteRef。最后创建一个代理。
进入sun.rmi.server.Util#createProxy

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

这样客户端已经获取到了注册中心的地址和端口,可以通过这一个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

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

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

在executeAcceptLoop里创建了一个线程池,在看ConnectionHandler的run方法

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


关键方法handleMessages
进入sun.rmi.transport.tcp.TCPTransport#handleMessages

走到了case 80
进入sun.rmi.transport.Transport#serviceCall

通过getTarget方法拿到Target对象,getTarget方法从objTable表中获取已经保存的Target对象。
再通过getDispatcher方法获得Taregt的disp分发器。

接下来的通过分发器调用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,调用RegistryImpl的lookup方法获取键名对应的远程对象引用。
进入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发送给服务端
到此客户端请求服务端的服务端响应过程分析完毕。

浙公网安备 33010602011771号