rmi代码分析
简介前的简介
额,一直以来貌似养成了不好的习惯,在写完这一篇之后发现,我为什么要写文章啊。。
博客是放笔记的,对吧,写的自己看懂就行了。还指望给别人看呢。
所以这是本博客最后一篇可能有可能别人能看懂的文章。
简介
在处理rmi的时候发现当时学的比较早导致一些东西没有分析的很清楚,今天把他其中的一些过程重新分析一下,rmi还是很重要的一部分。
对rmi其中的各种流程的代码部分进行简单的调试和分析。
简单写一个rmi本地调用的project即可开始调试。
1 | package com.demo02; |
服务端
服务端创建远程对象
rmi中的远程对象都继承了UnicastRemoteObject
类,然后实现远程接口,IHello
接口也继承了Remote
1 | public class RMIHello extends UnicastRemoteObject implements IHello{ |
所以我们要看的就是他的构造方法
UnicastRemoteObject.exportObject
会先进入父类UnicastRemoteObject
的构造方法,在UnicastRemoteObject
构造方法中会进入java.rmi.server.UnicastRemoteObject#exportObject(java.rmi.Remote, int)
方法
其中传入的默认的port为0
所以首先会创建一个UnicastServerRef
对象,
在创建这个对象的时候,首先又会封装一个LiveRef
对象进去
LiveRef
下面多次封装同一个LiveRef对象,就是为了客户端和服务端的通信
sun.rmi.transport.LiveRef
对象会封装进三个属性,其中包含了本地ip,它实际上是负责网络通信的一个对象
UnicastServerRef
UnicastServerRef类继承自UnicastRef类,其实也就是简单的把liveRef对象封装进去。
当我们封装好了UnicastServerRef
对象的时候,再进入java.rmi.server.UnicastRemoteObject#exportObject
方法中
封装好的UnicastServerRef
对象:
其实又是一个封装的过程,把UnicastServerRef
对象封装到一开始的远程对象中,也就是RMIHello
这个远程对象
此时我们的远程对象已经创建好了
UnicastServerRef.exportObject
接着会进入sun.rmi.server.UnicastServerRef#exportObject(java.rmi.Remote, java.lang.Object, boolean)
方法中
先用创建动态代理的方式创建了一个存根stub,他就是RMIHello的动态代理类
Util.createProxy
稍微跟进一下sun.rmi.server.Util#createProxy
方法,他是一个创建动态代理的方法
getRemoteClass方法会对传入的远程对象进行一下验证然后返回它的class
接着就是创建动态代理,classloader,handler
创建动态代理
可以看到返回的Proxy0动态代理对象,里面也封装了LiveRef这个对象
接着新建一个Target对象,把刚刚的一系列东西都封装进去
接下来进到sun.rmi.transport.LiveRef#exportObject
方法中,也就是哪里都封装了的LiveRef对象
这个方法进到sun.rmi.transport.tcp.TCPTransport#exportObject
方法,实际上他就是开一个端口,然后把Target对象放上去
接着下面还会exportObject一次target,是进到sun.rmi.transport.Transport#exportObject
方法中
根据注释理解一下,这个方法就是把远程对象放在服务端,等待客户端来的请求
下面的sun.rmi.transport.ObjectTable#putTarget
也就是一个表put的操作,把target装入
最后返回的是这个对象,至此远程对象创建完毕
注册中心创建逻辑
其实就是新建一个RegistryImpl对象,传入的参数值是默认的1099端口
首先会创建一个LiveRef作用于通讯
又创建一个UnicastServerRef类,在服务端创建的时候我们已经遇到过了一次这个构造方法,再一次进入
可以看到也就只是封装了一个ref对象用于网络通讯
接着进入setup方法
在其中我们又看到了熟悉的sun.rmi.server.UnicastServerRef#exportObject(java.rmi.Remote, java.lang.Object, boolean)
方法,在上面创建服务端对象的时候也遇到过
不过这里参数有所不同
这里创建的代理是这个注册中心的代理类,也就是RegistryImpl类
他还会进入setSeleton方法中
可以看到createSelection方法接受一个Remote对象
然后加载它后缀加上Skel的class对象来获取Skeleton
这个方法过后,skeleton类被加载到这个UnicastServerRef中
skeleton和stub就是分别对应服务端和客户端的网络代理
注册中心同时提供了skl和stub,因为要和服务端和客户端两端通讯
因此最终createRegistry()
的结果就是返回了一个RegistryImpl
对象,并且赋值this.skel=RegistryImpl_Skel
。
注册中心绑定逻辑
先获取到注册中心
然后调用sun.rmi.registry.RegistryImpl_Stub#bind
把UnicastServerRef对象绑定上去
客户端
同样的先获取到注册中心
返回的是注册中心的stub对象
然后进到sun.rmi.registry.RegistryImpl_Stub#lookup
方法中,也就是我们根据注册的名称来查找类
先会对我们查找的这个远程类名称字符串序列化进去
newCall方法就是一个网络请求的操作,这也和之前我们分析的流量对应了起来
说明注册中心也会有一个反序列化的操作
然后在下面的invoke方法中实现网络请求
也就是这个executeCall方法
然后就通过反序列化得到返回来的远程对象代理
这里有2个反序列化点,都是反序列化注册中心返回来的代理对象,如果我们创建一个恶意注册中心返回恶意stub就可能实现对客户端的反序列化攻击,分别是这两个地方(上面提了一个地方)
UnicastRef.invoke
sun.rmi.server.UnicastRef#invoke(java.rmi.server.RemoteCall)
第一个地方就是在invoke方法中的sun.rmi.transport.StreamRemoteCall#executeCall
方法中
在处理异常的地方里面有一个反序列化的操作
如果是这个异常的话,就会触发反序列化
并且这个sun.rmi.server.UnicastRef#invoke
方法在很多地方都会被调用
RegistryImpl_Stub.lookup
最后就会获得之前我们创建的服务端对象的远程代理,可以看到里面封装的LiveRef对象
远程方法调用
因为我们获取hello对象是远程对象的动态代理,所以对他调用任意方法的时候就会进入handler(调用处理器)的invoke方法
首先会对这个方法是否属于该远程对象进行一个判断
然后这里又会调用sun.rmi.server.UnicastRef#invoke(java.rmi.Remote, java.lang.reflect.Method, java.lang.Object[], long)
这个方法
接着sun.rmi.server.UnicastRef#marshalValue
方法中会把参数值序列化进去
接下来又会调用到executeCall方法,所有的客户端的请求都会用到这个方法
这里建立的连接就是和服务端直接建立连接了
接着就是获取方法调用结果
和上面的marshaValue对应,unmarshalValue方法中对返回的数据进行了反序列化处理
总结
引用自Javasec
RMI 底层通讯采用了Stub (运行在客户端) 和 Skeleton (运行在服务端) 机制,RMI 调用远程方法的大致如下:
- RMI 客户端在调用远程方法时会先创建 Stub (
sun.rmi.registry.RegistryImpl_Stub
)。 - Stub 会将 Remote 对象传递给远程引用层 (
java.rmi.server.RemoteRef
) 并创建java.rmi.server.RemoteCall
( 远程调用 )对象。 - RemoteCall 序列化 RMI 服务名称、Remote 对象。
- RMI 客户端的远程引用层传输 RemoteCall 序列化后的请求信息通过 Socket 连接的方式传输到 RMI 服务端的远程引用层。
- RMI服务端的远程引用层(
sun.rmi.server.UnicastServerRef
)收到请求会请求传递给 Skeleton (sun.rmi.registry.RegistryImpl_Skel#dispatch
)。 - Skeleton 调用 RemoteCall 反序列化 RMI 客户端传过来的序列化。
- Skeleton 处理客户端请求:bind、list、lookup、rebind、unbind,如果是 lookup 则查找 RMI 服务名绑定的接口对象,序列化该对象并通过 RemoteCall 传输到客户端。
- RMI 客户端反序列化服务端结果,获取远程对象的引用。
- RMI 客户端调用远程方法,RMI服务端反射调用RMI服务实现类的对应方法并序列化执行结果返回给客户端。
- RMI 客户端反序列化 RMI 远程方法调用结果。