分享

Java RMI基础

 hewii 2022-10-27 发布于上海

基本概念

RMI的全称是Rmote Method Invocation,即远程方法调用

远程服务器提供具体的类和方法,本地会通过某种方式获得远程类的一个代理,然后通过这个代理调用远程对象的方法,方法的参数是通过序列化与反序列化的方式传递的。

所以,1. 只要服务端的对象提供了一个方法,这个方法接收的是一个Object类型的参数,2. 且远程服务器的classpath中存在可利用pop链,那么我们就可以通过在客户端调用这个方法,并传递一个精心构造的对象的方式来攻击rmi服务。

RMI的组成

RMI模式中除了有Client与Server,还借助了一个Registry(注册中心)。rmi反序列化攻击就是攻击这个注册中心。

RMI的角色:

  • Server:提供具体的远程对象。

  • Registry:一个注册表,存放远程对象的位置(ip、端口等)。

  • Client:远程对象的调用者。

RMI的调用过程:

  • Registry先启动,并监听一个端口,一般为1099;

  • Server向Registry注册远程对象;

  • Client从Registry获得远程对象的代理(这个代理知道远程对象在网络中的具体位置:ip、端口、标识符),然后Client通过这个代理调用远程方法;

  • Server也是有一个代理的,Server端的代理会收到Client端的调用的方法、参数等,然后代理执行对应方法,并将结果通过网络返回给Client。

图源参考链接:

代码实现

项目结构:

1. 创建接口

创建一个接口Hello,该接口需要继承Remote接口,接口所定义的方法需要抛出RemoteException错误:

package model;import java.rmi.Remote;import java.rmi.RemoteException;public interface Hello extends Remote {
    public String welcome(String name) throws RemoteException;}

2. 实现接口类

基于上面定义的接口实现一个类Helloimpl,该实现类需要继承UnicastRemoteObject类,同样重载的方法需要抛出RemoteException错误:

package model.impl;import model.Hello;import java.rmi.RemoteException;import java.rmi.server.UnicastRemoteObject;public class Helloimpl extends UnicastRemoteObject implements Hello {
    @Override
    public String welcome(String name) throws RemoteException {
        return "Hello " + name;
    }
    public Helloimpl() throws RemoteException{
    }}

3. 创建服务端

服务端创建了一个注册表,并注册了客户端需要的对象:

package server;import model.Hello;import model.impl.Helloimpl;import java.rmi.RemoteException;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;public class Server {
    public static void main(String[] args) throws RemoteException{
        // 创建对象        Hello hello = new Helloimpl();
        // 创建注册表        Registry registry = LocateRegistry.createRegistry(1099);
        // 绑定对象到注册表,并给他取名为hello        registry.rebind("hello", hello);
    }}

4. 客户端调用远程对象

package client;import model.Hello;import java.rmi.NotBoundException;import java.rmi.RemoteException;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;public class Client {
    public static void main(String[] args) throws RemoteException, NotBoundException {
        // 获取到注册表的代理        Registry registry = LocateRegistry.getRegistry("localhost", 1099);
        // 利用注册表的代理去查询远程注册表中名为hello的对象        Hello hello = (Hello) registry.lookup("hello");
        // 调用远程方法        System.out.println(hello.welcome("axin"));
    }}

调用的远程方法如果需要传入参数,需要保证参数是可序列化的。这里传的参数为字符串,可序列化。如果传参是自定义的对象,那么这个对象需要实现Serilizable接口。

先启动服务端,再启动客户端,可以看到客户端已经成功调用远程方法:

如果服务端和客户端在不同机器上,需要保证远程调用对象所实现的那个接口(这里是Hello接口)在服务端和客户端都存在,因为客户端有一个实例化的过程。

动态类加载和安全策略

1. 动态类加载

如果客户端在调用时,传递了一个可序列化对象,这个对象在服务端不存在,则在服务端会抛出 ClassNotFound 的异常,但是 RMI 支持动态类加载,如果设置了 java.rmi.server.codebase,则会尝试从其中的地址获取 .class 并加载及反序列化。

可使用 System.setProperty("java.rmi.server.codebase", "http://127.0.0.1:9999/"); 进行设置,或使用启动参数 -Djava.rmi.server.codebase="http://127.0.0.1:9999/" 进行指定。

2. 安全策略

上面的通过网络加载外部类并执行方法,所以我们必须要有一个安全管理器来进行管理,如果没有设置安全管理,则 RMI 不会动态加载任何类,通常我们使用:

if (System.getSecurityManager() == null) {
    System.setSecurityManager(new RMISecurityManager());}

安全管理器应与管理策略相辅相成,所以我们还需要提供一个策略文件,里面配置允许哪些主机进行哪些操作,这里为了方便测试,直接设置全部权限:

rmi.policy

// Standard extensions get all permissions by default

grant {
    permission java.security.AllPermission;
};

同样可以使用 -Djava.security.policy=rmi.policySystem.setProperty("java.security.policy", RemoteServer.class.getClassLoader().getResource("rmi.policy").toString()); 来进行设置。

======================================

【定义】

Java RMIJava远程方法调用,即Java RMI(Java Remote Method Invocation)是Java编程语言里,一种用于实现远程过程调用的应用程序编程接口。它使客户机上运行的程序可以调用远程服务器上的对象。远程方法调用特性使Java编程人员能够在网络环境中分布操作。RMI全部的宗旨就是尽可能简化远程接口对象的使用。

我们知道远程过程调用(Remote Procedure Call, RPC)可以用于一个进程调用另一个进程(很可能在另一个远程主机上)中的过程,从而提供了过程的分布能力。Java 的 RMI 则在 RPC 的基础上向前又迈进了一步,即提供分布式对象间的通讯。

RMI(Remote Method Invocation)为远程方法调用,是允许运行在一个Java虚拟机的对象调用运行在另一个Java虚拟机上的对象的方法。

这两个虚拟机可以是运行在相同计算机上的不同进程中,也可以是运行在网络上的不同计算机中。

【JavaRMI】

一、工作原理

RMI能让一个Java程序去调用网络中另一台计算机的Java对象的方法,那么调用的效果就像是在本机上调用一样。通俗的讲:A机器上面有一个class,通过远程调用,B机器调用这个class 中的方法。

RMI,远程方法调用(Remote Method Invocation)是Enterprise JavaBeans的支柱,是建立分布式Java应用程序的方便途径。RMI是非常容易使用的,但是它非常的强大。

RMI的基础是接口,RMI构架基于一个重要的原理:定义接口和定义接口的具体实现是分开的。下面我们通过具体的例子,建立一个简单的远程计算服务和使用它的客户程序

二、RMI包含部分:

  • 远程服务的接口定义

  • 远程服务接口的具体实现

  • 桩(Stub)和框架(Skeleton)文件

  • 一个运行远程服务的服务器

  • 一个RMI命名服务,它允许客户端去发现这个远程服务

  • 类文件的提供者(一个HTTP或者FTP服务器)

  • 一个需要这个远程服务的客户端程序

三、RMI的用途?

RMI的用途是为分布式Java应用程序之间的远程通信提供服务,提供分布式服务。

目前主要应用时封装在各个J2EE项目框架中,例如Spring,EJB(Spring和EJB均封装了RMI技术)

在Spring中实现RMI:

①在服务器端定义服务的接口,定义特定的类实现这些接口;

②在服务器端使用org.springframework.remoting.rmi.RmiServiceExporter类来注册服务;

③在客户端使用org.springframework.remoting.rmi.RmiProxyFactoryBean来实现远程服务的代理功能;

④在客户端定义访问与服务器端服务接口相同的类

四、RMI的局限?

RMI目前使用Java远程消息交换协议JRMP(Java Remote Messaging Protocol)进行通信。JRMP是专为Java的远程对象制定的协议,由于JRMP是专为Java对象制定的,因此,RMI对于用非Java语言开发的应用系统的支持不足。不能与用非Java语言书写的对象进行通信(意思是只支持客户端和服务器端都是Java程序的代码的远程调用)。

五、RMI的使用局限?

由于客户机和服务器都是使用Java编写的,二者平台兼容性的要求仅仅是双方都运行在版本兼容的Java虚拟机上。

六、RMI调用远程方法的参数和返回值

当调用远程对象上的方法时,客户机除了可以将原始类型的数据作为参数一外,还可以将对象作为参数来传递,与之相对应的是返回值,可以返回原始类型或对象,这些都是通过Java的对象序列化(serialization)技术来实现的。(换而言之:参数或者返回值如果是对象的话必须实现Serializable接口)

七、 RMI应用程序的基本模型


八、RMI体系结构


  • 桩/框架(Stub/Skeleton)层:客户端的桩和服务器端的框架;

  • 远程引用(remote reference)层:处理远程引用行为

  • 传送层(transport):连接的建立和管理,以及远程对象的跟踪

九、 RMI类和接口(完成一个简单RMI需要用到的类)。




(一) Remote接口:是一个不定义方法的标记接口

Public interface Remote{}

在RMI中,远程接口声明了可以从远程Java虚拟机中调用的方法集。远程接口满足下列要求:

1、远程接口必须直接或间接扩展Java.rmi.Remote接口,且必须声明为public,除非客户端于远程接口在同一包中

2、在远程接口中的方法在声明时,除了要抛出与应用程序有关的一场之外,还必须包括RemoteException(或它的超类,IOExcepion或Exception)异常

3、在远程方法声明中,作为参数或返回值声明的远程对象必须声明为远程接口,而非该接口的实现类。

(二) RemoteObject抽象类实现了Remote接口和序列化Serializable接口,它和它的子类提供RMI服务器函数。

(三) LocateRegistry final()类用于获得特定主机的引导远程对象注册服务器程序的引用(即创建stub),或者创建能在特定端口接收调用的远程对象注册服务程序。

服务器端:向其他客户机提供远程对象服务

SomeService servcie=……;//远程对象服务

  1. Registry registry=LocateRegisty.getRegistry();//Registry是个接口,他继承了Remote,此方法返回本地主机在默认注册表端口 1099 上对远程对象Registry的引用。

  2. getRegistry(int port) 返回本地主机在指定 port 上对远程对象 Registry 的引用;

  3. getRegistry(String host) 返回指定host在默认注册表端口 1099 上对远程对象Registry的引用;
  4. getRegistry(String host, int port) 返回指定的host和port上对远程对象 Registry 的引用;

  5. registry.bind(“I serve”,service);//

  6. bind(String name,Remote obj) 绑定对此注册表中指定name 的远程引用。name : 与该远程引用相关的名称 obj : 对远程对象(通常是一个 stub)的引用;

  7. unbind(String name)移除注册表中指定name的绑定。

  8. rebind(String name,Remote obj)重新绑定,如果name已存在,但是Remote不一样则替换,如果Remote一样则丢弃现有的绑定

  9. lookup(String name) 返回注册表中绑定到指定 name 的远程引用,返回Remote

  10. String[] list() 返回在此注册表中绑定的名称的数组。该数组将包含一个此注册表中调用此方法时绑定的名称快照。

客户机端:向服务器提供相应的服务请求。

Registry registry=LocateRegisty.getRegistry();
SomeService servcie=(SomeService)registry.lookup(“I serve”);
Servcie.requestService();

(四) Naming类和Registry类类似。

客户端:

Naming.lookup(String url)

url 格式如下"rmi://localhost/"+远程对象引用
服务器端:
Registry registry=LocateRegistry.createRegistry(int port);
Naming.rebind(“service”,service);

(五) RMISecurityManager类

在RMI引用程序中,如果没有设置安全管理器,则只能从本地类路径加载stub和类,这可以确保应用程序不受由远程方法调用所下载的代码侵害

在从远程主机下载代码之前必须执行以下代码来安装RMISecurityManager:

System.setSecurityManager(new RMISecurityManager());

十、demo开发

为了编写一个demo,我们分为两部分,一部分是server端的代码,一部分是client端的代码,client端的代码主要是为了使用server端的代码。当然这个代码是非常简单的,只是为了说明问题,现实的使用会使比较复杂的。

(一) 我们的目的

建立一个server端的java project,包含远程端的代码,定义接口,定义接口实现,然后在建立一个client端的java project,通过RMI使用远端服务中的方法。

(二) 我们的代码结构

(三) 远程服务代码

1. 远程服务的接口定义

第一步就是建立和编译服务接口的Java代码。这个接口定义了所有的提供远程服务的功能,下面是源程序:

UserManagerInterface.java

动图封面

 1 package cn.com.tt.rmiserver.stub;
 2 
 3 import java.rmi.Remote;
 4 import java.rmi.RemoteException;
 5 
 6 import cn.com.tt.rmiserver.bean.Account;
 7 
 8 public interface UserManagerInterface extends Remote{
 9     public String getUserName() throws RemoteException;
10     public Account getAdminAccount() throws RemoteException;
11 }

动图封面


接口必须继承Remote类,每一个定义地方法都要抛出RemoteException异常对象。

2. 接口的具体实现

第二步就是对于上面的接口进行实现:

UserManagerImp.java

 1 package cn.com.tt.rmiserver;
 2 
 3 import java.rmi.RemoteException;
 4 
 5 import cn.com.tt.rmiserver.stub.UserManagerInterface;
 6 import cn.com.tt.rmiserver.bean.Account;
 7 
 8 public class UserManagerImp implements UserManagerInterface {
 9     public UserManagerImp() throws RemoteException {
10 
11     }
12     private static final long serialVersionUID = -3111492742628447261L;
13 
14     public String getUserName() throws RemoteException{
15         return "TT";
16     }
17     public Account getAdminAccount() throws RemoteException{
18         Account account=new Account();
19         account.setUsername("TT");
20         account.setPassword("123456");
21         return account;
22     }
23 }

动图封面

3. 定义一个bean,实现implements Serializable序列化接口。也就是可以在client和server端进行传输的可序列化对象。

Account.java

 1 package cn.com.tt.rmiserver.bean;
 2 
 3 import java.io.Serializable;
 4 
 5 public class Account implements Serializable,Cloneable{
 6     private static final long serialVersionUID = -1858518369668584532L;
 7     private String username;
 8     private String password;
 9     
10     public String getUsername() {
11         return username;
12     }
13     public void setUsername(String username) {
14         this.username = username;
15     }
16     public String getPassword() {
17         return password;
18     }
19     public void setPassword(String password) {
20         this.password = password;
21     }
22 }

动图封面

4. 定义server端的主程序入口。

Entry.java

动图封面

 1 package cn.com.tt.rmiserver.entry;
 2 
 3 import java.rmi.AlreadyBoundException;
 4 import java.rmi.RemoteException;
 5 import java.rmi.registry.LocateRegistry;
 6 import java.rmi.registry.Registry;
 7 import java.rmi.server.UnicastRemoteObject;
 8 
 9 import cn.com.tt.rmiserver.UserManagerImp;
10 import cn.com.tt.rmiserver.stub.UserManagerInterface;
11 
12 public class Entry {
13     public static void main(String []args) throws AlreadyBoundException, RemoteException{
14         UserManagerImp userManager=new UserManagerImp();
15         UserManagerInterface userManagerI=(UserManagerInterface)UnicastRemoteObject.exportObject(userManager,0);
16         // Bind the remote object's stub in the registry
17         Registry registry = LocateRegistry.createRegistry(2002);
18        
19         registry.rebind("userManager", userManagerI);
20         System.out.println("server is ready");
21         }
22 }[object Object]
(四) client端代码
  1. 把Server端的Account类和接口UserManagerInterface 导出Export成jar包,命名为:RmiServerInterface.jar。导入到client中。

  2. 项目——右键——Export——java——jar file——next——选择Account类和接口UserManagerInterface——命名为:RmiServerInterface.jar如下图:


3. 新建一个java Project,导入jar包,编写客户端代码。

4. 代码

ClientEntry.java

动图封面

 1 package weiblog.rmi;
 2 
 3 import java.rmi.NotBoundException;
 4 import java.rmi.RemoteException;
 5 import java.rmi.registry.LocateRegistry;
 6 import java.rmi.registry.Registry;
 7 
 8 import cn.com.tt.rmiserver.stub.UserManagerInterface;
 9 
10 public class ClientEntry {
11     
12     public static void main(String []args){
13         
14         try {
15             Registry registry = LocateRegistry.getRegistry("localhost",2004);
16             UserManagerInterface userManager = (UserManagerInterface)registry.lookup("userManager");
17             System.out.println("用户名是"+userManager.getAdminAccount().getUsername()
18                     +"密码"+userManager.getAdminAccount().getPassword());
19         } catch (RemoteException e) {
20             // TODO Auto-generated catch block
21             e.printStackTrace();
22         } catch (NotBoundException e) {
23             // TODO Auto-generated catch block
24             e.printStackTrace();
25         }
26         
27     }
28 
29 }[object Object]
5. 先运行服务器端代码, 然后运行客户端代码,就会显示运行结果,客户端可以运行多次,每次都可以取得服务器端的对象。如果要再次运行客户端代码就需要更改端口号,如果不更改就会显示端口号被占用。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多