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
发送给服务端
到此客户端请求服务端的服务端响应过程分析完毕。