bios000's Blog

My Security Understanding Of JNDI (1)

2023/04/24

前言

这段时间一直在研究JNDI的相关内容,感觉一直研究的比较浅,索性尝试写篇文记录一下学习的路程,看能不能将之前理解的比较浅显的地方再深一步挖掘一下。

概念

学习一项新的技术,首先要做的就是把他的关键词概念都搞懂,再去研究概念间的各种联系和实现原理,但令人尴尬的是,光这些概念,就需要占用一篇文章的字数去介绍,不过说明的越详细和清晰,在后续实现原理的理解上也就越容易。

JNDI

JNDI中文名Java 命名和目录接口(JAVA Naming and Directory Interface),他是Java内置的API ,J2EE规范要求所有J2EE容器都要提供JNDI规范的实现,他的角色就是相当于J2EE中的”114“,即JNDI是J2EE提供给J2EE组件在运行时间定位查找其他组件、资源和服务的一种机制(所以JNDI是一种规范和机制的实现)

1
2
3
4
5
在现实生活中,当你知道这个世界上存在一个你知道其名字或拥有具体技能的人,你想找到他,
但却从未和他建立过联系,你会怎么办?再具体点,傍晚下班回家,你发现你钥匙丢了,
你需要一个开锁公司的人帮你开门那么你会怎么办?(真实经历)
我想一般的做法都是会拨打114(找生活服务平台)找到对应提供服务的人帮你干成这个事情
而JNDI就类似于这样的角色存在,他会帮你寻找和转接到(lookup)到你所需要的服务资源

我为什么介绍得那么详细呢,因为我也想弄清楚What is JNDI ,在没有实际的J2EE项目开发的经验下理解抽象的机制和接口规范确实是一件很困难的事情。如果还是不能理解,网上也给了一个不全面但易于理解的解释:
”你可以认为JNDI是一个配置中心,也可以认为它是一个命名服务。其根本功能,就是让你可以通过一个简短的字符串,就能获取一系列的复杂配置和初始化后的功能。“

SPI

SPI 的全程为Service Provider Interface,他又是一个J2EE高级开发必须理解的一个机制(我愿称java是一个机制怪),它是一套用来被第三方实现或扩展的API,他可以用来启动框架扩展和替换组件。这个SPI的核心思想就是帮助项目应用解耦。这里继续用一张图和一个例子帮助去理解SPI
image.png

1
2
3
4
今天是星期六没上班,也就意味着你得在家里把**吃饭(调用方)** 的问题解决了,
那这个时候你疯狂转动大脑想该**吃啥(标准服务接口)**,摆在小易面前有两个选择:
**外卖、楼下的饭店(服务提供方)**。最后小易同学选择吃了楼下便宜又方便的大排面,
毕竟贫穷限制了选择空间。

JNDI 架构

image.png

上述角色如下

  • 应用程序代码(Application Code):使用JNDI API访问命名和目录服务的Java应用程序。

  • JNDI API:Java Naming and Directory Interface,是Java语言中用于访问命名和目录服务的API。

  • Naming Manger :这里你可以理解为命名管理,他的作用是实例化JNDI SPI

  • 服务提供程序接口(Service Provider Interface):是由服务提供程序实现的接口,用于向JNDI API提供特定的命名和目录服务的实现。服务提供程序接口定义了一组Java接口和类,JNDI API通过这些接口和类与服务提供程序进行交互。

上图为官方提供的JNDI架构图,大致流程为

  1. Java应用程序通过JNDI API访问命名服务,
  2. JNDI API会调用命名服务(Naming Manager)去实例化 JNDI SPI对象
  3. JNDI SPI 去根据配置选择对应服务的实现(如LDAP,DNS,RMI),将最终结果返回至JNDI API

LDAP与RMI

LDAP

ldap(Lightweight Directory Access Protocol) 是一种轻量级的目录访问协议,用于分布式环境中访问和维护目录信息。LDAP通常用于存储和访问用户、组、设备等信息,例如企业的员工信息、权限信息

RMI

RMI(Remote Method Invocation 远程方法调用),远程方法是分布式编程中的一个基本思想,RMI则是专门为Java设计,依赖JRMP通信协议(Java Remote Message Protocol java 远程消息交换协议),你可以理解成RMI是java语言中实现RPC的一种方式

JNDI与RMI LDAP的联系

JDK中包含了RMI LDAP内置的目录服务,他们在JNDI中提供了绑定和查找的方法,其实JNDI还实现了非常多服务,这里只挑最典型的说一下

代码实现

看完之前的这些概念,你已经大致清楚想弄清楚JNDI 需要涉及到哪些东西,接下来,我们将通过实现的过程进一步去理解相关的概念

RMI 与LDAP 的实现

RMI 架构的实现

一般情况下java的方法调用指的是同一个JVM内的方法调用,而RMI就是跨越JVM去调用一个远程的方法。通俗点说,RMI 可以使我们引用远程主机上的对象,将Java对象作为参数传递。

那我们需要实现代码的目的也很简单,运行在JVM a上的程序A远程调用运行在JVM b 上的项目B上的服务

RMI 架构有三部分组成:

  1. RMIServer 即我们编写的JVM b 上的java项目,它对外提供服务
  2. RMIClient 即我们编写的JVM a 上的程序,它需要远程使用B提供的服务
  3. RMI Service(RMI Register) 即实现远程调用的注册中心,他承担着最核心的功能

其工作原理如下图所示

image.png

这里还有两个概念,Stub存根与Skeleton骨架,他们其实都相当于类似代理的角色,相关的流程大概如下

  1. 远程方法调用经存根、远程引用层和传输层向下,传递给主机,然后再次经过传输层,向上穿过远程引用层和骨架,到达服务器对象
  2. 骨架完成对服务器对象实际的方法调用,获取返回值,返回值向下经远程引用层、服务器端的传输层传递回客户端,再向上经传输层和远程调用层返回,最后存根获取到返回值

接下来就可以讲解相关的代码了

代码实现

声明远程接口,声明实现远程接口类

在java RMI 中,远程对象是指运行在服务器端的对象,它实现了一些可以由客户端访问的方法,为了让一个类成为远程对象,这个类需要实现java.rmi.Remote接口,这个接口是一个标记接口,意味着它没有定义任何的方法,只是用来表明这个类的对象可以从其他JAVA虚拟机上进行远程调用,远程接口可以定义方法用于被远程调用。

这里声明一个Clock接口,继承Remote

1
2
3
4
5
6
7
import java.rmi.Remote;  
import java.rmi.RemoteException;

public interface Clock extends Remote {

String Hello() throws RemoteException;
}

紧接着实现这个接口,并重写Hello方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.rmi.RemoteException;  
import java.rmi.server.UnicastRemoteObject;

public class ClockImpl extends UnicastRemoteObject implements Clock {
@Override
public String Hello() throws RemoteException {
return "Hello RMI";
}

//无参数构造方法,继承UnicastRemoteObject
public ClockImpl() throws RemoteException {

}
}

这里要说明远程对象必须要实现java.rmi.server.UnicastRemoteObject类,这样才能保证客户端访问获得远程对象时,该远程对象会把自身的一个拷贝以socket的形式传输给客户端–也就是我们上面所说的Stub存根,而远程对象本身也就是我们所说的Skeleton骨架

服务端实现完成远程对象绑定

JDK 提供了一个RMI注册表(RMIRegister),RMIRegister 也是一个远程对象,默认监听在1099端口上,可以使用代码启动RMIRegister
要注册远程对象,需要RMI URL和一个远程对象的引用
你也可以用java.rmi.Naming类进行注册绑定

1
2
3
4
5
6
7
8
9
10
11
12
13

//创建一个registry实例
LocateRegistry.createRegistry(1099);

// 获取register 实例
Registry registry = LocateRegistry.getRegistry();

//将远程对象绑定到RMI注册表中的方法 将名为"Clock"的远程对象'stub'绑定到RMI 注册表中
//1.使用java.rmi.registry.Registry
registry.bind("Clock", stub);

//2.使用java.rmi.Naming进行绑定
java.rmi.Naming.rebind("rmi://localhost:1099/Clock",stub)

这里有一个小知识点:
Registry.rebind是使用RMI注册表绑定,所以不需要完整RMI URL Naming.rebind是通过Java的名称服务进行绑定,由于名称服务不止为RMI提供查询服务,所以绑定时需要填入完整RMI URL

完整的服务端代码实现

一般来说我们会将绑定实现代码写在Server端中

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
import java.rmi.AlreadyBoundException;  
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;

public class ClockServer {

public static void main(String[] args) throws RemoteException, AlreadyBoundException {
// 创建clock 实例 远程对象
Clock impl = new ClockImpl();
// 把接口对象暴露到网络中

// Remote 接口的 impl 对象导出为一个远程对象,并将其绑定到 1099 端口上。导出远程对象后
// ,其他远程 JVM(Java 虚拟机)就可以通过网络访问该远程对象,并调用其提供的远程方法。
//创建一个registry实例
LocateRegistry.createRegistry(1099);
// 获取register 实例
Registry registry = LocateRegistry.getRegistry();

//将远程对象绑定到RMI注册表中的方法 将名为"Clock"的远程对象'stub'绑定到RMI 注册表中
registry.bind("Clock", skeleton);
System.out.println("Clock server ready");

}
}

客户端代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.rmi.NotBoundException;  
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class ClockClient {
public static void main(String[] args) throws RemoteException, NotBoundException {
//null :localhost
Registry registry = LocateRegistry.getRegistry();
// 获取实例
Clock clock = (Clock) registry.lookup("Clock");
// 正常方法调用
String dt = clock.Hello();
System.out.println(dt);
}
}

这里的lookup方法即寻找对应绑定字对应的远程对象

下一篇将继续讲LDAP 服务及JNDI 的相关代码实现

引用

https://blog.csdn.net/wn084/article/details/80729230
https://www.jianshu.com/p/46b42f7f593c
https://cloud.tencent.com/developer/article/1695243
java RMI原理详解_xinghun_4的博客-CSDN博客

CATALOG
  1. 1. 前言
  2. 2. 概念
  3. 3. JNDI
  4. 4. SPI
  5. 5. JNDI 架构
  6. 6. LDAP与RMI
    1. 6.1. LDAP
    2. 6.2. RMI
    3. 6.3. JNDI与RMI LDAP的联系
  7. 7. 代码实现
    1. 7.1. RMI 与LDAP 的实现
    2. 7.2. RMI 架构的实现
  8. 8. 代码实现
    1. 8.1. 声明远程接口,声明实现远程接口类
    2. 8.2. 服务端实现完成远程对象绑定
    3. 8.3. 完整的服务端代码实现
    4. 8.4. 客户端代码实现
  9. 9. 引用