RMI原理

1.简介

Java远程方法调用,即Java RMI(Java Remote Method Invocation)是Java编程语言里,一种用于实现远程过程调用的应用程序编程接口。它使客户机上运行的程序可以调用远程服务器上的对象。远程方法调用特性使Java编程人员能够在网络环境中分布操作。RMI全部的宗旨就是尽可能简化远程接口对象的使用。接口的两种常见实现方式是:最初使用JRMP(Java Remote Message Protocol,Java远程消息交换协议)实现;此外还可以用与CORBA兼容的方法实现。RMI一般指的是编程接口,也有时候同时包括JRMP和API(应用程序编程接口),而RMI-IIOP则一般指RMI接口接管绝大部分的功能,以支持CORBA的实现。最初的RMI API设计为通用地支持不同形式的接口实现。后来,CORBA增加了传值(pass by value)功能,以实现RMI接口。然而RMI-IIOP和JRMP实现的接口并不完全一致。

2原理:

架构图:

RMI底层通讯采用了Stub(运行在客户端)Skeleton(运行在服务端)机制,RMI调用远程方法的大致如下:

  1. RMI客户端在调用远程方法时会先创建Stub(sun.rmi.registry.RegistryImpl_Stub)
  2. Stub会将Remote对象传递给远程引用层(java.rmi.server.RemoteRef)并创建java.rmi.server.RemoteCall(远程调用)对象。
  3. RemoteCall序列化RMI服务名称Remote对象。
  4. RMI客户端远程引用层传输RemoteCall序列化后的请求信息通过Socket连接的方式传输到RMI服务端远程引用层
  5. RMI服务端远程引用层(sun.rmi.server.UnicastServerRef)收到请求会请求传递给Skeleton(sun.rmi.registry.RegistryImpl_Skel#dispatch)
  6. Skeleton调用RemoteCall反序列化RMI客户端传过来的序列化。
  7. Skeleton处理客户端请求:bindlistlookuprebindunbind,如果是lookup则查找RMI服务名绑定的接口对象,序列化该对象并通过RemoteCall传输到客户端。
  8. RMI客户端反序列化服务端结果,获取远程对象的引用。
  9. RMI客户端调用远程方法,RMI服务端反射调用RMI服务实现类的对应方法并序列化执行结果返回给客户端。
  10. RMI客户端反序列化RMI远程方法调用结果。

参考:https://www.javasec.org/javase/RMI/

3代码示例

服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.rmi;

import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;

public class RmiServer {
public static String HOST = "127.0.0.1";
public static int PORT = 8989;
public static String RMI_PATH = "/test";
public static final String RMI_NAME = "rmi://" + HOST + ":" + PORT + RMI_PATH;

public static void main(String[] args) {
try {
// 注册RMI端口
LocateRegistry.createRegistry(PORT);

// 创建一个服务
Servicetest servicetest = new ServicetestImpl();

// 服务命名绑定
Naming.rebind(RMI_NAME, servicetest);

System.out.println("启动RMI服务在" + RMI_NAME);
} catch (Exception e) {
e.printStackTrace();

}
}
}



上述代码中,在8989端口起了RMI服务,以键值对的形式存储了RMI_PATH和rmiInterface的对应关系,也就是rmi://127.0.0.1:8989/hello对应一个ServicetestImpl类实例,然后通过Naming.rebind(RMI_NAME, rmiInterface)绑定对应关系。再来看Servicetest.java

1
2
3
4
5
6
7
8
9
package com.rmi;

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface Servicetest extends Remote {

void test() throws RemoteException;;
}

定义了RMIInterface接口,继承自Remote,然后定义了一个test()方法作为接口。注意需要抛出RemoteException异常。继续看实现真正功能的类ServicetestImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.rmi;

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.sql.SQLOutput;

public class ServicetestImpl extends UnicastRemoteObject implements Servicetest{
protected ServicetestImpl() throws RemoteException {
super();
}

@Override
public void test() {
System.out.println("this is a rmi test");
}
}

继承自UnicastRemoteObject类,并且实现之前定义的Servicetest接口的test()方法。UnicastRemoteObject类提供了很多支持RMI的方法,具体来说,这些方法可以通过JRMP协议导出一个远程对象的引用,并通过动态代理构建一个可以和远程对象交互的Stub对象。现在就定义好了Server端,来看Client

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.rmi;

import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

import static com.rmi.RmiServer.RMI_NAME;

public class RmiClient {
public static void main(String[] args) {
try {
// 获取服务注册器
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 8989);
// 获取所有注册的服务
String[] list = registry.list();
for (String i : list) {
System.out.println("已经注册的服务:" + i);
}

// 寻找RMI_NAME对应的RMI实例
Servicetest rt = (Servicetest) Naming.lookup(RMI_NAME);

// 调用Server的test()方法,并拿到返回值.
String resultr = rt.test();
System.out.println(resultr);

} catch (Exception e) {
e.printStackTrace();
}
}
}

参考:https://su18.org/post/rmi-attack/#2-%E6%94%BB%E5%87%BB-registry-%E7%AB%AF

https://paper.seebug.org/1091/#java-rmi_2

https://xz.aliyun.com/t/7079?time__1311=n4%2BxnD0Dy7itGQNKGNnmAzti%3DDkW3DB7in1oD

https://xz.aliyun.com/t/7264?time__1311=n4%2BxnD0Dy7G%3DBxGqGNnmADR7DgDfErrx3%2BBbD#toc-0

https://y4er.com/posts/java-rmi/

https://www.javasec.org/javase/RMI/

http://www.mi1k7ea.com/2019/09/01/Java-RMI%E5%8E%9F%E7%90%86%E4%B8%8E%E4%BD%BF%E7%94%A8/

https://xz.aliyun.com/t/9261

https://www.cnblogs.com/nice0e3/p/14280278.html

https://www.bilibili.com/video/BV1L3411a7ax?p=5&vd_source=82398f68c82cb90e0d9aa4fea90e36a0

https://townmacro.cn/2022/04/18/java-%E5%AE%89%E5%85%A8-rmi%E5%AD%A6%E4%B9%A0%E6%80%BB%E7%BB%93/

https://su18.org/post/rmi-attack/#%E5%9B%9B-%E6%94%BB%E5%87%BB-rmi

https://xz.aliyun.com/t/7079?time__1311=n4%2BxnD0Dy7itGQNKGNnmAzKDtf%3D4AKDkWe6YeD

https://paper.seebug.org/1091/#java-rmi_2

https://forum.butian.net/share/2278

https://xz.aliyun.com/t/7264?time__1311=n4%2BxnD0Dy7G%3DBxGqGNnmADR7DgDfErrx3%2BBbD#toc-2

https://townmacro.cn/2022/04/18/java-%E5%AE%89%E5%85%A8-rmi%E5%AD%A6%E4%B9%A0%E6%80%BB%E7%BB%93/

https://lalajun.github.io/2020/06/22/RMI%20%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96-%E6%B7%B1%E5%85%A5-%E4%B8%8B/#%E5%89%8D%E8%A8%80