JAVA EE十三大规范(1)RMI(全网最深入浅出)

本文深入介绍了RPC(远程过程调用)和RMI(远程方法调用)的概念,包括它们的工作原理、手写实现与JDK实现的流程,并探讨了RMI的安全漏洞及其防御措施。通过实例解析了RPC如何通过网络调用远程服务,以及RMI如何实现在不同JVM间的通信。同时,提到了RMI中序列化和反序列化带来的安全问题,如潜在的反序列化攻击,并给出了防范建议。
摘要由CSDN通过智能技术生成

目录

1.概述

2.详述

2.1.流程分析

2.1.1.手写实现

2.1.2.JDK实现

2.2.安全漏洞

2.2.1.漏洞成因

2.2.2.防御方法


1.概述

RPC:

RPC(Remote Procedure Call),一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议,RPC可以用HTTP协议实现,并且用HTTP是建立在 TCP 之上最广泛使用的 RPC,但是互联网公司往往用自己的私有协议,比如鹅厂的JCE协议,私有协议不具备通用性但是相比于HTTP协议,RPC采用二进制字节码传输,更加高效也更加安全。

用一个比较形象的例子来形容,你老婆出去打麻将,然后想起家里洗衣机里的衣服还没洗,于是打电话给在家里的你,叫你把洗衣机里的衣服洗了,这就是远程过程调用。微服务中服务的调用底层就是使用的RPC机制。

RMI:

RMI(Remote Method Invocation),在JDK1.2中推出,RPC的Java版本,官方的说法是RMI 支持存储于不同地址空间的程序级对象之间彼此进行通信,实现远程对象之间的无缝远程调用。直白的说法RMI其实就是支持一个JVM去调用另一个JVM中的对象中的方法。其底层的其实就是靠socket以及序列化和反序列化支撑起来的,使用分布式垃圾收集器(DGC)进行GC。

RMI是一个分布式的架构,由三部分组成:

  • 客户端

    远程对象的调用者

  • 服务器

    定义、发布远程对象

  • 注册中心

    JDK提供的一个可以独立运行的程序,在bin目录下,其运行在服务器端的一个固定端口上。

2.详述

2.1.流程分析

2.1.1.手写实现

为了方便理解RMI的整个流程,我们首先基于网络通信和序列化、反序列化来手写一个远程方法调用的demo:

核心为两个代理:

  • Skeleton

    骨架,服务器端的远程对象的代理,封装远程对象及网络通信能力。

  • stub

    存根,客户端的远程对象的代理,搭建一个远程对象的骨架,具体的方法调用通过网络通信来访问远程对象的对应方法。

接口:

public interface Person {
    int getAge() throws Throwable;
    String getName() throws Throwable;
}

服务端:

//实现类
public class PersonServer implements Person{
​
    private int age;
    private String name;
​
    public PersonServer(String name,int age){
        this.age=age;
        this.name=name;
    }
​
    public int getAge() throws Throwable {
        return age;
    }
​
    public String getName() throws Throwable {
        return name;
    }
}
//骨架
public class Person_Skeleton extends Thread{
    private PersonServer myServer;
    public Person_Skeleton(PersonServer server) {
        // get reference of object server
        this.myServer = server;
    }
    public void run() {
        try {
            // new socket at port 9000
            ServerSocket serverSocket = new ServerSocket(9000);
            // accept stub's request
            Socket socket = serverSocket.accept();
            while (socket != null) {
                // get stub's request
                ObjectInputStream inStream =
                        new ObjectInputStream(socket.getInputStream());
                String method = (String)inStream.readObject();
                // check method name
                if (method.equals("age")) {
                    // execute object server's business method
                    int age = myServer.getAge();
                    ObjectOutputStream outStream =
                            new ObjectOutputStream(socket.getOutputStream());
                    // return result to stub
                    outStream.writeInt(age);
                    outStream.flush();
                }
                if(method.equals("name")) {
                    // execute object server's business method
                    String name = myServer.getName();
                    ObjectOutputStream outStream =
                            new ObjectOutputStream(socket.getOutputStream());
                    // return result to stub
                    outStream.writeObject(name);
                    outStream.flush();
                }
            }
        } catch(Throwable t) {
            t.printStackTrace();
            System.exit(0);
        }
    }
}

客户端:

//存根
public class Person_Stub implements Person{
    private Socket socket;
    public Person_Stub() throws Throwable {
        // connect to skeleton
        socket = new Socket("127.0.0.1", 9000);
    }
    public int getAge() throws Throwable {
        // pass method name to skeleton
        ObjectOutputStream outStream =
                new ObjectOutputStream(socket.getOutputStream());
        outStream.writeObject("age");
        outStream.flush();
        ObjectInputStream inStream =
                new ObjectInputStream(socket.getInputStream());
        return inStream.readInt();
    }
    public String getName() throws Throwable {
        // pass method name to skeleton
        ObjectOutputStream outStream =
                new ObjectOutputStream(socket.getOutputStream());
        outStream.writeObject("name");
        outStream.flush();
        ObjectInputStream inStream =
                new ObjectInputStream(socket.getInputStream());
        return (String)inStream.readObject();
    }
}

2.1.2.JDK实现

结合前文的手写实现来看JDK给出的实现整个流程一目了然:

服务器端生产远程对象和骨架代理,将远程对象注册到注册中心中,客户端找注册中心拿远程对象的时候会获取到远程对象的存根代理,通过存根代理和骨架代理之间的网络通信就实现了远程方法调用。

 

 

代码示例:

1.服务器

//继承远程调用接口,定义方法模板
public interface IRemoteObj extends Remote {
    String sayHello(String keyWords) throws RemoteException;
}
​
//实现业务方法
public class RemoteObjImpl extends UnicastRemoteObject implements  IRemoteObj {
    protected RemoteObjImpl() throws RemoteException {
        //如果不继承UnicastRemoteObject就需要手动导出
        //UnicastRemoteObject.exportObject(this,0);
    }
​
    public String sayHello(String keyWords) throws RemoteException {
        System.out.println(keyWords);
        return "hello "+keyWords;
    }
}
​
//启动入口
public class RMIServer {
    public static void main(String[] args) throws Exception{
        IRemoteObj remoteObj=new RemoteObjImpl();
        //创建注册中心
        Registry registry= LocateRegistry.createRegistry(1099);
        //向注册中心中注册对象
        registry.bind("remoteObj",remoteObj);
    }
}

2.客户端

public interface IRemoteObj extends Remote {
    String sayHello(String keyWords) throws RemoteException;
}
​
public class RMIClient {
    public static void main(String[] args) throws Exception {
        //连接注册中心
        Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
        //调用对象
        IRemoteObj remoteObj = (IRemoteObj) registry.lookup("remoteObj");
        remoteObj.sayHello("world!");
    }
}

2.2.安全漏洞

2.2.1.漏洞成因

由于RMI底层使用了序列化和反序列化,因此存在序列化相关的漏洞。

Serializable接口其实隐式的提供了两个方法writeObject、readObject用来自定义序列化时的读写动作,这两个方法在重写快捷键中是看不到的,但是只要定义了这两个方法就会生效。

反序列化由于需要从外部去加载类,这样给一些恶意代码的注入提供了机会,以下为一个反序列化注入攻击的例子:

public class MyObject implements Serializable {
​
    private static final long serialVersionUID = -6554051283690579548L;
​
    public String name;
    //重写readObject()方法
    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
        //执行默认的readObject()方法
        in.defaultReadObject();
        //执行指定程序
        Runtime.getRuntime().exec("calc.exe");
    }
}
public class test {
​
    public static void main(String[] args) throws Exception {
        //定义myObj对象
        MyObject myObj = new MyObject();
        myObj.name = "hello world";
        //创建一个包含对象进行反序列化信息的”object”数据文件
        FileOutputStream fos = new FileOutputStream("object");
        ObjectOutputStream os = new ObjectOutputStream(fos);
        //writeObject()方法将myObj对象写入object文件
        os.writeObject(myObj);
        os.close();
        //从文件中反序列化obj对象
        FileInputStream fis = new FileInputStream("object");
        ObjectInputStream ois = new ObjectInputStream(fis);
        //恢复对象
        MyObject objectFromDisk = (MyObject)ois.readObject();
        System.out.println(objectFromDisk.name);
        ois.close();
    }
}

2.2.2.防御方法

  • 认知和签名

  • 禁止JVM执行外部命令Runtime.exec

  • RASP监测

序列化、反序列化漏洞攻击与防御是一个很大的话题,历年来JAVA开源社区中爆出的各类组件的安全漏洞中大多数与其相关,此处暂不展开做详细论述,后面会写专门的文章来讨论相关内容。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_BugMan

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值