Java RMI學習與解讀(一)
Java RMI學習與解讀(一)
寫在前面
本文記錄在心情美麗的一個晚上。
嗯。就是心情很美麗。
那為什麼晚上還要學習呢?
emm… 卷… 捲起來。
全文基本都是根據su18師傅和其他師傅的文章學習的,本文也只是做一個學習的記錄,建議大家最好也是去學習這些師傅們的文章,寫的真的很棒。
About RMI
首先,關於RMI介紹,建議看這篇文章,裏面對不少RMI相關概念解釋的都很清晰。
RMI(Remote Method Invocation) 遠程方法調用協議,實現了Java程序之間跨JVM通信,可以遠程調用其他虛擬機中的對象來執行方法。也就是獲取遠程對象的引用,通過遠程對象的引用調用遠程對象的某個方法。
它讓我們獲得對遠程主機上對象的引用,並像在我們自己的虛擬機中一樣使用它。RMI 允許我們調用遠程對象上的方法,將真實的 Java 對象作為參數傳遞並獲取真實的 Java 對象作為返回值。
無論在何處使用引用,方法調用都發生在原始對象上,該對象仍位於其原始主機上。如果遠程主機向您返回對其對象之一的引用,您可以調用該對象的方法;實際的方法調用將發生在對象所在的遠程主機上。
關於遠程調用(Remote Invocation)在C語言中的RPC(Remote Procedure Calls遠程過程調用)就已經實現了可以在遠程主機上執行C語言函數並返回結果。而C中的RPC與Java中的RMI最大的區別在於,C中主要關注的是數據結構,在進行RPC遠程過程調用的時候打包傳輸的數據時相對簡單,而Java中,例如序列化通常是將一整個類直接序列化之後進行傳輸,而類中就需要包含該類的屬性以及方法。那麼RMI相較於RPC而言就不僅僅是傳輸數據結構了,Java需要將整個類(屬性、方法)進行傳輸並且在落地後是要可以調用該類中的方法的。
RMI進行傳輸時使用了序列化與反序列化機制,必要時會利用動態類加載和安全管理機制(CC5提到的概念)來安全的傳輸Java類,個人感覺RMI相較於RPC真正的突破在於可以在網絡上傳輸數據(對象的屬性)和行為(對象的方法)。
It should be no surprise that RMI uses object serialization, which allows us to send graphs of objects (objects and all of the connected objects that they reference). When necessary, RMI can also use dynamic class loading and the security manager to transport Java classes safely. Thus, the real breakthrough of RMI is that it』s possible to ship both data and behavior (code) around the Net.
遠程與非遠程對象
遠程對象:RMI中的遠程對象首先需要可以序列化;並且需要實現特殊遠程接口的對象,該接口指定可以遠程調用對象的哪些方法(這個後面會詳細提到);其次該對象是通過一種可以通過網絡傳遞的特殊對象引用來使用的。和普通的 Java 對象一樣,遠程對象是通過引用傳遞。也就是在調用遠程對象的方法時是通過該對象的引用完成的。
非遠程對象:非遠程對象與遠程對象相比只是可被序列化而已,並不會像遠程對象那樣通過調用遠程對象的引用來完成調用方法的操作,而是將非遠程對象做一個簡單地拷貝(simply copied),也就是說非遠程對象是通過拷貝進行傳遞。
Stubs and skeletons
RMI的實現用到了存根Stubs(client端)和骨架Skeletons(server端)
存根Stubs: 什麼是Stubs?之前也說到了:當客戶端在調用遠程對象上的方法時,是通過遠程對象的引用調用遠程對象的方法,而這個所謂的”遠程對象的引用”實際上是充當該對象代理的本地代碼,這段代碼就是存根Stub。
骨架Skeletons:而在調用遠端(Server)的目標類之前,也會經過一個對應的遠端代理類,就是骨架 Skeleton,它從 Stubs 中接收遠程方法調用並傳遞給真實的目標類。
Stubs 以及 Skeletons 的調用對於 RMI 服務的使用者來講是隱藏的,我們無需主動的去調用相關的方法。但實際的客戶端和服務端的網絡通信時通過 Stub 和 Skeleton 來實現的。
那麼現在可以小結通過RMI進行遠程方法調用時有如下這麼一個簡單的流程:
Client端 ==> 存根Stubs ==> 骨架Skeletons ==> Server端
Remote Interface
上面我們提到了遠程對象需要實現特殊的遠程接口,下面會涉及三個概念:
- 遠程對象
- 遠程對象所實現的特殊的遠程接口
- java.rmi.Remote接口
在使用RMI進行遠程方法調用時,首先需要定義這個特殊的遠程接口;而在java.rmi包中有一個接口Remote,實際中遠程對象實現的遠程接口需要extend這個Remote接口,後續遠程對象的創建就實現我們定義的特殊的遠程接口即可。且同時生成的存根Stubs也是如此。
大概是這樣的流程:
java.rmi.Remote ==> 特殊的遠程接口 extends Remote ==> 遠程對象類 implements 特殊的遠程接口
並且在這個特殊的接口中聲明的方法都需要拋出java.rmi.RemoteException
異常,例如:
import java.rmi.*;
public interface RemoteObject extends Remote{
String doSomething(String thing) throws RemoteException;
String say() throws RemoteException;
String sayGoodbye() throws RemoteException;
}
Remote Object
而遠程對象類通常還需要繼承 java.rmi.server.UnicastRemoteObject
類,在RMI中 UnicastRemoteObject類是與Object超類等效的,該類提供了equals( )
, hashcode( )
, toString( )
方法;並且在RMI運行時,繼承UnicastRemoteObject類的子類會被exports
出去,綁定隨機端口,開始監聽來自客戶端(Stubs)的請求。
About Export of Remote Object
在 export 時,會隨機綁定一個端口,監聽客戶端的請求,所以即使不註冊,直接請求這個端口也可以通信,這部分在後面學習與解讀RMI攻擊時會詳細展開。如果不想讓遠程對象成為 UnicastRemoteObject 的子類,後面就需要主動的使用其靜態方法
exportObject
來手動 export 對象。
同時創建遠程對象類需要顯示定義構造方法並拋出RemoteException,即使是個無參構造也需要如此,不然會報錯。
import java.rmi.*;
import java.rmi.server.UnicastRemoteObject;
public class RemoteObjectImpl extends UnicastRemoteObject implements RemoteObject {
protected RemoteObjectImpl() throws RemoteException {
}
@Override
public String doSomething(String thing) throws RemoteException {
return String.format("Doing ", thing);
}
@Override
public String say() throws RemoteException {
return "This is the say Method";
}
@Override
public String sayGoodbye() throws RemoteException {
return "GoodBye RMI";
}
}
關於遠程對象以及遠程對象所需要implements的’特殊的遠程接口’的編寫就大致如上所述。
下面學習一下如何通過RMI進行對遠程對象上某個方法的調用,在此之前還需要了解一個概念 RMI registry。
RMI registry
About registry
這個概念很好理解,它類似一個電話薄或者路由表,可以通過註冊表(RMI registry)來查找對另一台主機上已註冊遠程對象的引用
好比通過電話薄根據姓名查找到某人電話號碼然後通話或者說查找路由表中某ip的路由,通過那個gateway發送就能找到該ip的主機。
而在RMI中的註冊表(registry)就是類似於這種機制,當我們想要調用某個遠程對象的方法時,通過該遠程對象在註冊時提供在註冊表(registry)中的別名(Name),來讓註冊表(registry)返回該遠程對象的引用,後續通過該引用實現遠程方法調用。
註冊表(registry)由java.rmi.Naming
和 java.rmi.registry.Registry
實現。
Naming類提供了進行存儲及獲取遠程對象等操作註冊表(registry)的相關方法,如bind()實現遠程對象別名與遠程對象之間的綁定。其他的還有如:
查詢(lookup)、重新綁定(rebind)、接觸綁定(unbind)、list(列表)
而這些方法的具體實現,其實是調用
LocateRegistry.getRegistry
方法獲取了 Registry 接口的實現類,並調用其相關方法進行實現的
比如bind方法的源碼
/**
* Binds the specified <code>name</code> to a remote object.
*
* @param name a name in URL format (without the scheme component)
* @param obj a reference for the remote object (usually a stub)
* @exception AlreadyBoundException if name is already bound
* @exception MalformedURLException if the name is not an appropriately
* formatted URL
* @exception RemoteException if registry could not be contacted
* @exception AccessException if this operation is not permitted (if
* originating from a non-local host, for example)
* @since JDK1.1
*/
public static void bind(String name, Remote obj)
throws AlreadyBoundException,
java.net.MalformedURLException,
RemoteException
{
ParsedNamingURL parsed = parseURL(name);
Registry registry = getRegistry(parsed);
if (obj == null)
throw new NullPointerException("cannot bind to null");
registry.bind(parsed.name, obj);
}
這個類提供的每個方法都有一個 URL 格式的參數,格式如下:
//host:port/name
:
- host 表示註冊表所在的主機
- port 表示註冊表接受調用的端口號,默認為 1099
- name 表示一個註冊 Remote Object 的引用的名稱,不能是註冊表中的一些關鍵字
而java.rmi.registry.Registry
接口在RMI中有兩個實現類RegistryImpl
以及 RegistryImpl_Stub
創建註冊中心(registry)
一般通過LocateRegistry#createRegistry()
方法創建
import java.rmi.*;
import java.rmi.registry.LocateRegistry;
public class Registry {
public static void main(String[] args) {
try {
//默認綁定1099端口
LocateRegistry.createRegistry(1099);
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
創建Server端
通過Server端將需要調用的類(遠程對象類)進行別名與遠程對象的綁定
import java.net.MalformedURLException;
import java.rmi.*;
public class RemoteServer {
public static void main(String[] args) throws RemoteException, AlreadyBoundException, MalformedURLException {
//實例化遠程對象類,創建遠程對象
RemoteObject remoteObject = new RemoteObject();
//通過Naming.bind()方法綁定別名與 RemoteObject
Naming.bind("rmi://127.0.0.1:1099/Zh1z3ven", remoteObject);
}
}
Client端調用
創建Client端,通過遠程對象引用實現對遠程方法的調用
import java.rmi.*;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Arrays;
public class RMIClient {
public static void main(String[] args) throws RemoteException, NotBoundException {
//創建註冊中心對象
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
//打印註冊中心中的遠程對象別名list
System.out.println(Arrays.toString(registry.list()));
//通過別名獲取遠程對象存根stub並調用遠程對象的方法
RemoteInterface stub = (RemoteInterface) registry.lookup("Zh1z3ven");
System.out.println(stub.say());
System.out.println(stub.doSomething("Sing Song"));
System.out.println(stub.sayGoodbye());
}
}
這裡用一張Longofo師傅的圖加深下理解
RMI Demo
上面大概將RMI整個過程中的三個角色Client、RMI Registry、Server端簡單的代碼demo放了出來,下面我們把它揉到一起實現一次簡單的RMI過程。
那麼一般RMI Registry和Server端是在同一端的,我們就把它們放在同一個類中
RMI Registry&Server
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Arrays;
public class RegistryServer {
public static void main(String[] args) {
try {
//創建Registry
Registry registry = LocateRegistry.createRegistry(1099);
//實例化遠程對象類,創建遠程對象
RemoteObject remoteObject = new RemoteObject();
//通過Naming類綁定別名與 RemoteObject
Naming.bind("rmi://127.0.0.1:1099/Zh1z3ven", remoteObject);
System.out.println("Registry&Server Start");
//打印別名
System.out.println("Registry List: " + Arrays.toString(registry.list()));
} catch (Exception e) {
e.printStackTrace();
}
}
}
RMI Client
package Rmi;
import java.rmi.*;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Arrays;
public class RMIClient {
public static void main(String[] args) throws RemoteException, NotBoundException {
//獲取註冊中心對象
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
//打印註冊中心中的遠程對象別名list
System.out.println(Arrays.toString(registry.list()));
//通過別名獲取遠程對象存根stub並調用遠程對象的方法
RemoteInterface stub = (RemoteInterface) registry.lookup("Zh1z3ven");
System.out.println(stub.say());
System.out.println(stub.doSomething("Sing Song"));
System.out.println(stub.sayGoodbye());
}
}
先運行RMI Registry&Server端
之後啟動RMI Client端
成功調用了遠程對象的方法。
如果運行過程拋出了如下圖的異常,一般是端口佔用的問題。
建議:
-
排查是否1099端口起了別的服務
-
因為RemoteInterface是存在於RegistryServer和Client兩端的項目中,那麼這個接口代碼是需要一致的;且在實例化RemoteObject時,遠程對象的類型應為RemoteInterface。例如:
RegistryServer:
RemoteInterface remoteObject = new RemoteObject();
Client:
RemoteInterface stub = (RemoteInterface) registry.lookup("Zh1z3ven");
還有一個點就是關於RemoteInterface接口應該在Registry/Server/Client端都存在,否則在registry.lookup
之後拿到stub但是無法通過 .
調用遠程對象的相關方法。
那麼接下來是當傳遞的參數不是String而是一個對象時需要注意的點,涉及到兩個概念
- RMI的動態加載類,
java.rmi.server.codebase
- Java SecurityManager安全管理機制
RMI 流程
小結一下如何從0實現1次RMI:
-
創建遠程對象接口(RemoteInterface)
-
創建遠程對象類(RemoteObject)實現遠程對象接口(RemoteInterface)並繼承UnicastRemoteObject類
-
創建Registry&Server端,一般Registry和Server都在同一端。
- 創建註冊中心(Registry)
LocateRegistry.getRegistry("ip", port);
- 創建Server端:主要是實例化遠程對象
- 註冊遠程對象:通過
Naming.bind(rmi://ip:port/name ,RemoteObject)
將name與遠程對象(RemoteObject)進行綁定
- 創建註冊中心(Registry)
-
遠程對象接口(RemoteInterface)應在Client/Registry/Server三個角色中都存在
-
創建Client端
- 獲取註冊中心
LocateRegistry.getRegistry('ip', prot)
- 通過
registry.lookup(name)
方法,依據別名查找遠程對象的引用並返回存根(Stub)
- 獲取註冊中心
-
通過存根(Stub)實現RMI(Remote Method Invocation)
RMI 動態加載類
在RMI過程中Client端和Server端的數據傳輸有如下特點:
RMI的Client和Server&Registry進行通信時是將數據進行序列化傳輸的,所以當我們傳遞一個可序列化的對象作為參數進行傳輸時,在Server端肯定會對其進行反序列化。
關於RMI的動態加載類機制:
如果RMI需要用到某個類但當前JVM中沒有這個類,它可以通過遠程URL去下載這個類。那麼這個URL可以是http、ftp協議,加載時可以加載某個第三方類庫jar包下的類,或者在指定URL時在最後以\
結束來指定目錄,從而通過類名加載該目錄下的指定類。
動態加載時用到的是java.rmi.server.codebase
屬性,需要將URL賦值給該屬性
一般是通過System.setProperty("java.rmi.server.codebase", "//127.0.0.1:8080/");
設置
或以java -Djava.rmi.server.codebase="//myserver/foo/"
的方式指定URL
還有就是Java SecurityManager機制,這個在CC5中也提到了:
當運行未知的Java程序的時候,該程序可能有惡意代碼(刪除系統文件、重啟系統等),為了防止運行惡意代碼對系統產生影響,需要對運行的代碼的權限進行控制,這時候就要啟用Java安全管理器。該管理器默認是關閉的。
而在RMI中進行動態加載類時有一個限制[1]為:
需要設置RMISecurityManager作為安全管理器(SecurityManager),這樣RMI時才會動態加載類。
System.setSecurityManager(new RMISecurityManager());
同時需要給定一個管理策略文件,該文件以.policy
結尾,內容如下可以給定全部權限
grant {
permission java.security.AllPermission;
};
之後可通過讀取靜態資源文件的方式加載該管理策略
System.setProperty("java.security.policy", RemoteServer.class.getClassLoader().getResource("rmi.policy").toString());
那麼還有一個限制[2]為:
屬性 java.rmi.server.useCodebaseOnly 的值必需為false。但是從JDK 6u45、7u21開始,java.rmi.server.useCodebaseOnly 的默認值就是true。當該值為true時,將禁用自動加載遠程類文件,僅從CLASSPATH和當前虛擬機的java.rmi.server.codebase 指定路徑加載類文件。使用這個屬性來防止虛擬機從其他Codebase地址上動態加載類,增加了RMI ClassLoader的安全性。
動態加載類主要是分為兩個場景,角色分別為Client和Server
- Client端接受通過RMI遠程調用Server端某個方法產生的返回值,但是該返回值是個對象且Client端並沒有該對象的類,那麼就可以通過Server端提供的URL去動態加載類。
- Server端在RMI過程中收到Client端傳來的參數,該參數可能是個對象,如果該對象對應的類在Server端並不存在,那麼就可以通過Client端提供的URL去動態加載類
場景1:Client端動態加載Server端
測試環境均為JDK7u17
RemoteInterface
import java.rmi.*;
public interface RemoteInterface extends Remote{
String doSomething(String thing) throws RemoteException;
String say() throws RemoteException;
String sayGoodbye() throws RemoteException;
String sayServerLoadClient(Object name) throws RemoteException;
Object sayClientLoadServer() throws RemoteException;
}
RegistryServer端的RemoteObject(implements RemoteInterface)
import java.rmi.*;
import java.rmi.server.UnicastRemoteObject;
public class RemoteObject extends UnicastRemoteObject implements RemoteInterface {
protected RemoteObject() throws RemoteException {
}
@Override
public String doSomething(String thing) throws RemoteException {
return String.format("Doing ", thing);
}
@Override
public String say() throws RemoteException {
return "This is the say Method";
}
@Override
public String sayGoodbye() throws RemoteException {
return "GoodBye RMI";
}
@Override
public String sayServerLoadClient(Object name) throws RemoteException {
return name.getClass().getName();
}
@Override
public Object sayClientLoadServer() throws RemoteException {
return new ServerObject();
}
}
Server端待動態加載的類
import java.io.Serializable;
public class ServerObject implements Serializable {
private static final long serialVersionUID = 3274289574195395731L;
}
RegistryServer2
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Arrays;
public class RegistryServer2 {
public static void main(String[] args) {
try {
System.setProperty("java.rmi.server.codebase", "//127.0.0.1:8080/");
//創建Registry
Registry registry = LocateRegistry.createRegistry(1099);
//實例化遠程對象類,創建遠程對象
RemoteInterface remoteObject = new RemoteObject();
//通過Naming類綁定別名與 RemoteObject
Naming.bind("rmi://127.0.0.1:1099/Zh1z3ven2", remoteObject);
System.out.println("Registry&Server Start");
//打印別名
System.out.println("Registry List: " + Arrays.toString(registry.list()));
} catch (Exception e) {
e.printStackTrace();
}
}
}
RMIClient2
import java.rmi.NotBoundException;
import java.rmi.RMISecurityManager;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Arrays;
public class RMIClient2 {
public static void main(String[] args) throws RemoteException, NotBoundException {
//設置java.security.policy屬性值與RMISecurityManager
System.setProperty("java.security.policy", RMIClient2.class.getClassLoader().getResource("rmi.policy").getFile());
System.setSecurityManager(new RMISecurityManager());
//獲取註冊中心對象
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
//打印註冊中心中的遠程對象別名list
System.out.println(Arrays.toString(registry.list()));
//通過別名獲取遠程對象存根stub並調用遠程對象的方法
RemoteInterface stub = (RemoteInterface) registry.lookup("Zh1z3ven2");
System.out.println(stub);
System.out.println(stub.say());
System.out.println(stub.doSomething("Sing Song"));
System.out.println(stub.sayGoodbye());
System.out.println("The Class Name: " + stub.sayClientLoadServer().getClass().getName());
}
}
測試結果
場景2:Server端動態加載Client端
RMIClient
import java.rmi.*;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Arrays;
public class RMIClient {
public static void main(String[] args) throws RemoteException, NotBoundException {
//將指定URL賦值給codebase
System.setProperty("java.rmi.server.codebase", "//127.0.0.1:8080/");
//創建註冊中心對象
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
//打印註冊中心中的遠程對象別名list
System.out.println(Arrays.toString(registry.list()));
//通過別名獲取遠程對象存根stub並調用遠程對象的方法
RemoteInterface stub = (RemoteInterface) registry.lookup("Zh1z3ven");
System.out.println(stub.say());
System.out.println(stub.doSomething("Sing Song"));
System.out.println(stub.sayGoodbye());
System.out.println("The Class Name: " + stub.sayServerLoadClient(new ClientObject()));
}
}
Client端待動態加載的類
這個類限制不多,主要是注意serialVersionUID
需要設置一下,以免反序列化時出問題。
import java.io.Serializable;
public class ClientObject implements Serializable {
private static final long serialVersionUID = 3274289574195395731L;
}
Registry&Server端
import java.rmi.Naming;
import java.rmi.RMISecurityManager;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Arrays;
public class RegistryServer {
public static void main(String[] args) {
try {
System.setProperty("java.security.policy", RegistryServer.class.getClassLoader().getResource("rmi.policy").getFile());
System.setSecurityManager(new RMISecurityManager());
//創建Registry
Registry registry = LocateRegistry.createRegistry(1099);
//實例化遠程對象類,創建遠程對象
RemoteObject remoteObject = new RemoteObject();
//通過Naming類綁定別名與 RemoteObject
Naming.bind("rmi://127.0.0.1:1099/Zh1z3ven", remoteObject);
System.out.println("Registry&Server Start");
//打印別名
System.out.println("Registry List: " + Arrays.toString(registry.list()));
} catch (Exception e) {
e.printStackTrace();
}
}
}
rmi.policy
grant {
permission java.security.AllPermission;
};
測試結果
END
本來記錄的時候心情很美麗,結果學起來真的很吃力。
最近有點懶忙,後續關於RMI攻擊的深入解讀還不知道何時能搞定。
測試代碼後續會貼到Github上(學的時候沒有新建項目,有點亂需要重新弄一下)
如有錯誤還煩請各位師傅不吝賜教。
Reference
//www.oreilly.com/library/view/learning-java/1565927184/ch11s04.html
//github.com/longofo/rmi-jndi-ldap-jrmp-jmx-jms