Spring Boot integrated rmi quickstart demo

HBLOG
5 min readJul 8, 2024

--

1. What is RMI?

Remote Method Invocation (RMI) is a basic idea in distributed programming. There are many technologies that implement remote method calls, such as CORBA and WebService, both of which are independent of each programming language. Java RMI is a remote method call mechanism designed for Java environments, and is a Java API used to implement remote call (RPC), which can directly transfer serialized Java objects and distributed garbage collection. Its implementation relies on the JVM, so it supports calls from one JVM to another. In Java RMI, the remote server implements specific Java methods and provides interfaces, and the client only needs to provide the corresponding parameters according to the definition of the interface class to invoke the remote methods, where the objects are encoded and transmitted by serialization. Therefore, the exploitation of deserialization vulnerabilities often involves RMI, which is what it means. RMI relies on the Java Remote Message Protocol (JRMP), which is customized for Java and requires both the server and the client to be written in Java.

Interaction process

The interaction process can be briefly summarized as:

  1. First, start the RMI Registry service, and you can specify the port of service listening or use the default port (1099) when starting.
  2. Secondly, the server first instantiates an implementation class that provides services locally, and then registers the instantiated implementation class to the RMI Registry through the bind or rebind method of Naming/Context/Registry provided by RMI and exposes a name.
  3. Finally, the client uses the Naming/Context/Registry lookup method provided by RMI to get the implementation class from the RMI Service through the local interface and a known name (i.e., the name exposed by the RMI Registry). In this way, although there is no local implementation class of this class, all the methods are in the interface, and the methods of the remote call object can be implemented;

2. Code engineering

Objectives of the experiment

Experiment with a simple RMI service and call it through a client

rmi-server

This is a server-side project that mainly provides the RMI service interface

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>rmi</artifactId>
<groupId>com.et</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>rmi-server</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.et</groupId>
<artifactId>rmi-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.200</version>
</dependency>
</dependencies>
</project>

config

package com.et.rmi.server.config;
import com.et.rmi.server.dao.CustomerRepository;
import com.et.rmi.server.model.Customer;
import com.et.rmi.server.service.CustomerServiceImpl;
import om.et.rmi.common.CustomerService;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.remoting.rmi.RmiServiceExporter;
import org.springframework.stereotype.Component;
import java.util.logging.Logger;
@Component
public class RmiServerApplicationRunner implements ApplicationRunner {
private CustomerRepository repository;
private CustomerServiceImpl customerService;
private final Logger log = Logger.getLogger(this.getClass().getName());
public RmiServerApplicationRunner(CustomerServiceImpl customerService) {
this.customerService = customerService;
}
@Override
public void run(ApplicationArguments args) throws Exception {
Customer customer1 = new Customer("John", "Smith", "123-456-7890");
customerService.saveCustomer(customer1);
customerService.getCustomers().forEach(System.out::println);
}
@Bean
public RmiServiceExporter customerServiceExporter() {
RmiServiceExporter customerServiceExporter = new RmiServiceExporter();
customerServiceExporter.setRegistryPort(1199);
customerServiceExporter.setServiceName("customerService");
customerServiceExporter.setServiceInterface(CustomerService.class);
customerServiceExporter.setService(customerService);
log.info("Started RMI Server");
return customerServiceExporter;
}
}

service

package com.et.rmi.server.service;
import com.et.rmi.server.dao.CustomerRepository;
import com.et.rmi.server.mapper.CustomerMapper;
import com.et.rmi.server.model.Customer;
import om.et.rmi.common.CustomerDTO;
import om.et.rmi.common.CustomerService;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class CustomerServiceImpl implements CustomerService {
private CustomerRepository repository;
public CustomerServiceImpl(CustomerRepository repository) {
this.repository = repository;
}
@Override
public CustomerDTO getCustomer(long id) {
Customer customer = repository.findById(id).orElseThrow(IllegalArgumentException::new);
CustomerMapper mapper = new CustomerMapper();
CustomerDTO dto = mapper.mapToDTO(customer);
System.out.println(dto);
return dto;
}
public List<Customer> getCustomers() {
return (List<Customer>)repository.findAll();
}
public void saveCustomer(Customer customer) {
repository.save(customer);
}
}

dao

package com.et.rmi.server.dao;
import com.et.rmi.server.model.Customer;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface CustomerRepository extends CrudRepository<Customer, Long> {
Optional<Customer> findById(long id);
}

mapper

package com.et.rmi.server.mapper;
import com.et.rmi.server.model.Customer;
import om.et.rmi.common.CustomerDTO;
public class CustomerMapper {
public CustomerMapper() {
}
public CustomerDTO mapToDTO(Customer customer){
CustomerDTO dto = new CustomerDTO();
dto.setFirstName(customer.getFirstName());
dto.setLastName(customer.getLastName());
dto.setSocialSecurityCode(customer.getSocialSecurityCode());
return dto;
}
}

model

package com.et.rmi.server.model;
import javax.persistence.*;
@Entity
@SequenceGenerator(name = "CUST_SEQ", initialValue = 1_000_001)
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "CUST_SEQ")
private long id;
private String firstName;
private String lastName;
private String socialSecurityCode;
public Customer() {
}
public Customer(String firstName, String lastName, String socialSecurityCode) {
this.firstName = firstName;
this.lastName = lastName;
this.socialSecurityCode = socialSecurityCode;
}
public long getId() {
return id;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getSocialSecurityCode() {
return socialSecurityCode;
}
public void setSocialSecurityCode(String socialSecurityCode) {
this.socialSecurityCode = socialSecurityCode;
}
@Override
public String toString() {
return "Customer{" +
"id=" + id +
", firstName='" + firstName + '\'' +
", lastName='" + lastName + '\'' +
", socialSecurityCode='" + socialSecurityCode + '\'' +
'}';
}
}

application.properties

spring.jpa.show-sql=false
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect

rmi-client

This is a client-side project that primarily calls remote RMI services

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>rmi</artifactId>
<groupId>com.et</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>rmi-cilent</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.et</groupId>
<artifactId>rmi-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>

controller

package com.et.rmi.client.controller;
import om.et.rmi.common.CustomerDTO;
import om.et.rmi.common.CustomerService;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.remoting.rmi.RmiProxyFactoryBean;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@Controller
@RequestMapping(value = "customers")
public class CustomerController {
private RmiProxyFactoryBean proxyFactoryBean;
public CustomerController(RmiProxyFactoryBean proxyFactoryBean) {
this.proxyFactoryBean = proxyFactoryBean;
}
@RequestMapping(value = "{id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<CustomerDTO> getCustomer(@PathVariable long id) {
CustomerService service = (CustomerService) proxyFactoryBean.getObject();
CustomerDTO dto = service.getCustomer(id);
return ResponseEntity.ok(dto);
}
}

config

package com.et.rmi.client.config;
import om.et.rmi.common.CustomerService;
import org.springframework.context.annotation.Bean;
import org.springframework.remoting.rmi.RmiProxyFactoryBean;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.logging.Logger;
@Component
public class Config {
public final Logger log = Logger.getLogger(this.getClass().getName());
@Bean
public RmiProxyFactoryBean proxyFactoryBean() {
String remoteHost = System.getProperty("RMI_SERVER_HOST");
if(StringUtils.isEmpty(remoteHost)){
remoteHost="127.0.0.1";
}
String rmiHost = String.format("rmi://%s:1199/customerService", remoteHost);
log.info("RMI Host name is " + rmiHost);
RmiProxyFactoryBean proxy = new RmiProxyFactoryBean();
proxy.setServiceInterface(CustomerService.class);
proxy.setServiceUrl(rmiHost);
proxy.afterPropertiesSet();
return proxy;
}
}

application.properties

server.port=8081

rmi-common

This is a public package that both server and client need to reference

package om.et.rmi.common;
import java.io.Serializable;
public class CustomerDTO implements Serializable {
private String firstName;
private String lastName;
private String socialSecurityCode;
public CustomerDTO() {
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getSocialSecurityCode() {
return socialSecurityCode;
}
public void setSocialSecurityCode(String socialSecurityCode) {
this.socialSecurityCode = socialSecurityCode;
}
@Override
public String toString() {
final StringBuffer sb = new StringBuffer("CustomerDTO{");
sb.append("firstName='").append(firstName).append('\'');
sb.append(", lastName='").append(lastName).append('\'');
sb.append(", socialSecurityCode='").append(socialSecurityCode).append('\'');
sb.append('}');
return sb.toString();
}
}
package om.et.rmi.common;
public interface CustomerService {
CustomerDTO getCustomer(long id);
}

The above are just some of the key codes, all of which can be found in the repositories below

Code repositories

3. Testing

4. References

--

--