bios000's Blog

My Security Understanding Of JNDI (2)

2023/04/26

前言

上一章我们介绍了JNDI涉及到的关键概念,以及RMI服务的实现过程,本章将介绍JNDI服务相关的实现,当然其中也穿插着一些概念(os:该死的JNDI API,这些要命的核心接口概念对java 非深入研究者真的很不友好)

JNDI的实现

上一篇我们已经讲过JNDI是一个jdk内置的接口,接下来我们来聊一下JNDI的实现

JNDI 可以使用的几种常见服务实现:

  1. 文件系统实现:使用文件系统作为命名和目录服务的存储介质,以文件和目录的形式来表示命名对象。
  2. LDAP 实现:使用基于 LDAP (Lightweight Directory Access Protocol) 的目录服务,允许 Java 应用程序通过 LDAP 协议访问 LDAP 目录服务器。
  3. Java 对象实现:使用 Java 对象来表示目录服务,将 Java 对象绑定到命名空间中,以便应用程序可以对它们进行查找、绑定和解绑定。
  4. Jini 实现:使用 Jini 框架来实现分布式命名和服务发现,Java 应用程序可以通过 Jini 注册和查找服务。
  5. CORBA 实现:使用 CORBA (Common Object Request Broker Architecture) 来实现命名和目录服务,允许 Java 应用程序通过 IIOP (Internet Inter-ORB Protocol) 访问到 CORBA 命名服务或目录服务。
  6. Dns 实现:用于将 IP 地址映射到域名
  7. RMI(Remote Method Invocation):用于呼叫在另一个 Java 虚拟机中运行的对象。

使用到的包和类

在Java JDK里面提供了5个包

1
2
3
4
5
6
7
8
9
10
javax.naming:主要用于命名操作,它包含了命名服务的类和接口,该包定义了Context接口和InitialContext类;

javax.naming.directory:主要用于目录操作,它定义了DirContext接口和InitialDir- Context类;

javax.naming.event:在命名目录服务器中请求事件通知;

javax.naming.ldap:提供LDAP支持;

javax.naming.spi:允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI可以访问相关服务。

关于Context和InitialContext 类

一个上下文(Context)是一系列名称和对象的绑定的集合,每个上下文都有与之关联的命名规范。一个上下文通常提供了一个lookup操作来返回对象,也可能提供绑定,接触绑定,列举绑定名等操作。
一个上下文中的名称可以绑定到一个具有相同命名规范的上下文中,称之为子上下文(subcontext)。
例如:在文件系统中,/usr是一个Context,/usr/bin是usr的subcontext。
你也可以把上下文理解成容器,对应着现实世界中的盒子,里面存着一个个映射关系

根据上述内容,我们知道如果想将名称和对象对应关系存储起来,必须要有一个上下文

InitialContext类是JNDI API的核心类之一,它提供了命名和目录服务的基本方法,InitialContext对象是连接到命名和目录服务的入口点,它提供了一组方法来查找、绑定、解绑定和重新绑定命名对象

在创建InitialContext 对象时,可以传递一个可选的哈希表参数env,该参数包含一组上线文环境属性,用于指定JNDI的配置信息、目标命名服务、安全策略等

InitialContext构造器

InitialContext 的构造器有多个重载形式,以下是其中一些常用的构造器:

  • InitialContext():使用默认的上下文环境属性创建一个初始上下文对象。默认情况下,JNDI 将搜索和使用系统提供的命名和目录服务。

  • InitialContext(Hashtable<?,?> environment):使用指定的上下文环境属性创建一个初始上下文对象。这个构造器接受一个哈希表参数 environment,包含一组上下文环境属性,用于指定 JNDI 的配置信息、目标命名服务、安全策略等。

当使用默认构造器创建 InitialContext 对象时,JNDI 将遵循下列规则来确定要使用的命名和目录服务提供者:

  1. 如果设置了 java.naming.provider.url 属性,则使用指定的命名和目录服务提供者。

  2. 否则,如果设置了 java.naming.factory.initial 属性,则使用指定的命名和目录服务提供者工厂类创建提供者。

  3. 否则,如果存在默认的命名和目录服务提供者,则使用该提供者。

  4. 否则,如果存在多个命名和目录服务提供者,则抛出 NoInitialContextException 异常。

当使用带有 environment 参数的构造器创建 InitialContext 对象时,可以在 environment 参数中指定要使用的命名和目录服务提供者的相关属性。这些属性的名称和含义取决于提供者的实现。例如,如果使用 LDAP 提供者,则可以指定服务器的地址、端口号、用户名和密码等属性。

除了上述构造器外,还有一些其他的构造器可以创建不同类型的上下文对象,如 InitialLdapContextInitialDirContext 等。这些构造器会根据指定的上下文环境参数来创建不同类型的上下文对象,以便访问特定类型的命名和目录服务。

相关实现代码

1
2
3
4
Hashtable<String, String> env = new Hashtable<String, String>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.fscontext.RefFSContextFactory");
env.put(Context.PROVIDER_URL, "file:/tmp");
Context ctx = new InitialContext(env);

常用方法

下面是 InitialContext 中一些常用的方法:

  • bind(String name, Object obj):将一个命名对象绑定到指定的名称上,如果指定名称已经存在则会抛出 NameAlreadyBoundException 异常。
  • rebind(String name,Object obj):将一个命名对象重新绑定到指定的名称上,如果指定名称已经存在则会覆盖它。
  • unbind(String name):从命名服务中解绑定指定名称的命名对象,如果指定名称不存在则会抛出 NameNotFoundException 异常。
  • lookup(String name):从命名服务中查找指定名称的命名对象,如果指定名称不存在则会抛出 NameNotFoundException 异常。
  • list(String name):列出指定名称下的所有命名对象,如果指定名称不存在或不是一个上下文环境则会抛出 NamingException 异常。

Naming Service 命名服务

Naming 就是将Java对象以某个名称的形式绑定(binding)到一个容器环境(Context)中,以后调用容器环境到JNDI容器环境(Context)的查找方法(lookup)方法又可以查找出某个名称所绑定的JAVA对象

实现

JNDI NamingService 服务的实现主要分为两类:本地服务和远程服务。

本地服务实现

本地服务实现是运行在单个 Java 虚拟机中的 JNDI 服务。它通常是以 Java 应用程序的形式提供的,比如一个 Web 应用程序或者一个 Java EE 容器。本地服务实现可以使用文件系统、内存、数据库等作为命名服务的后端存储介质。

常见的本地服务实现包括:

  • SimpleNamingContext(简单内存命名服务):提供一个基于内存的命名服务实现,用于开发和测试阶段的快速原型开发。
  • File System Context Provider(文件系统上下文提供者):使用文件系统作为 JNDI 命名服务实现的存储介质。
  • Database Context Provider(数据库上下文提供者):使用数据库作为 JNDI 命名服务实现的存储介质,支持多种数据库实现,如 Oracle、DB2、MySQL 等。

远程服务实现

远程服务实现是运行在多个 Java 虚拟机中的 JNDI 服务。它通常用于分布式应用程序中,可以让多个 Java 应用程序之间进行交互和协作。
常见的远程服务实现包括:

  • LDAP Server(LDAP 服务器):提供分布式目录服务在线用户目录、组织目录、资源目录等。
  • JBoss JNDI Service(JBoss JNDI 服务):提供一个分布式 JNDI 实现,用于 JBoss 应用程序服务器中。
  • RMI Naming Service(RMI 命名服务):使用 Java RMI 实现的命名服务,可以让远程 Java 应用程序之间进行调用和交互。

这里只给出数据库实现的服务代码

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import javax.naming.Context;  
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Hashtable;

public class TestNaming {
public static void main(String[] args) {
try {
// 创建一个带有数据库上下文环境属性的哈希表
Hashtable<String, String> env = new Hashtable<String, String>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.fscontext.RefFSContextFactory");
env.put(Context.PROVIDER_URL, "file:/tmp");

// 创建初始上下文对象
Context context = new InitialContext(env);

// 获取数据源对象(通过 JNDI 查找)
DataSource dataSource = (DataSource)context.lookup("java:comp/env/jdbc/TestDB");

// 使用数据源对象连接到 MySQL 数据库
Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement();

// 执行 SQL 语句,并输出结果集
ResultSet rs = stmt.executeQuery("SELECT * FROM employees");
while (rs.next()) {
System.out.println(rs.getInt("emp_no") + " " + rs.getString("first_name") + " " + rs.getString("last_name"));
}

// 关闭数据库连接和上下文对象
rs.close();
stmt.close();
conn.close();
context.close();
} catch (NamingException | SQLException e) {
e.printStackTrace();
}
}
}
}
}

在上面的示例代码中,我们使用了 Database Context Provider 实现了 JNDI 数据库访问。其中,我们首先创建了一个带有数据库上下文环境属性的哈希表,并使用 InitialContext 类创建了一个初始上下文对象。然后我们通过 JNDI 查找获取了一个数据源对象,最后使用数据源对象连接到 MySQL 数据库,执行 SQL 语句并输出结果集。执行完成后,我们关闭了数据库连接和上下文对象。在实际应用中,我们可以根据自己的需求和环境,修改上面代码中的一些参数和属性,以适应不同的数据库实现和方案。

Directory Service 目录服务

在JNDI 中,目录服务是一种提供名字和对象之间映射的服务,它允许Java应用程序查找、绑定和解绑命名对象(是不是和RMI很像,在实际应用中,JNDI 和 RMI 经常一起使用,以实现分布式应用程序的命名和访问远程对象方法。例如,Java EE 中的 EJB 组件通常会使用 JNDI 来注册和查找自身以及其他组件,并使用 RMI 远程调用其他 EJB 组件的方法。同时,JNDI 也可以使用 RMI 实现访问远程的命名和目录服务,例如远程 LDAP 目录服务。)

JNDI 的目录服务其实是对命名服务的扩展,首先JNDI的目录和文件系目录概念有很大的不同,JNDI的目录服务是将一个对象的所有属性信息都保存到一个上下文环境中。也就是说它不仅提供名称和对象之间的关联,还允许对象具有属性。

关于JNDI目录服务的实现

实现包

javax.naming.directory软件包扩展了javax.naming软件包,以提供除命名服务之外的用于访问目录服务的功能。该程序包允许应用程序检索与目录中存储的对象相关联,并使用指定的属性搜索对象。

JNDI API中提供的代表目录容器环境的类为DirContext,DirContext是Context的子类,显然它除了能完成目录相关操作外,也能完成所有的命令(Naming)操作。DirContext是对Context的扩展,它在Context的基础上增加了对目录属性的操作功能,可以在其中绑定对象的属性信息和查找对象的属性信息

关于InitialDirContext类

InitialDirContext 是 javax.naming.directory 包中的一个类,它是 InitialContext 的子类,提供了对目录服务的访问功能。与 InitialContext 不同,它支持访问和操作目录服务中的目录对象和属性,包括添加、删除、修改、搜索等。

InitialDirContext 构造器的使用方式与 InitialContext 类似,可以使用默认的上下文环境参数创建对象,也可以使用指定的上下文环境属性来与命名和目录服务提供商建立联系。

下面是 InitialDirContext 中一些常用的方法:

  • createSubcontext(String name, Attributes attrs):在指定名称下创建一个新的子目录,在创建时可以指定一个包含该目录属性和值的 Attributes 对象。

  • getAttributes(String name):获取指定目录对象的属性值列表。

  • modifyAttributes(String name, int mod_op, Attributes attrs):将指定目录对象的属性修改为指定的属性值列表。属性修改操作 mod_op 可以是 ADD_ATTRIBUTE、REPLACE_ATTRIBUTE、REMOVE_ATTRIBUTE 中的一个。

  • search(String name, Attributes matchAttrs):搜索与指定属性匹配的目录对象,返回一个包含匹配对象名称和属性的 NamingEnumeration 对象。

  • listBindings(String name):列出调用者在指定名称下拥有控制权的所有目录对象及其属性信息。

实现代码

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
27
28
29
30
31
32
33
34
35
36
import javax.naming.Context;  
import javax.naming.directory.Attributes;
import javax.naming.directory.BasicAttributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import java.util.Hashtable;

public class TestDiretory {
public static void main(String[] args) {
try {
//创建环境变量对象
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, "ldap://localhost:1389");

//创建初始上下文
DirContext ctx = new InitialDirContext();

//创建属性列表
Attributes attrs = new BasicAttributes(true);
attrs.put("objectclass", "top");
attrs.put("objectclass", "organizationalUnit");
attrs.put("ou", "Myou");

//创建目录服务对象
ctx.createSubcontext("ou=Myou", attrs);

//关闭上下文
ctx.close();

} catch (Exception e) {
e.printStackTrace();
}
}

}

这段代码是一个 Java 语言实现的使用 JNDI 操作 LDAP 目录服务的示例代码。
该代码中通过 JNDI API 操作 LDAP 目录服务,使用 InitialDirContext 对象来创建初始上下文,并使用 DirContext 对象操作目录服务。在创建目录服务和属性列表后,通过调用 createSubcontext() 方法将指定的属性列表和名称创建一个新的子上下文(Subcontext),从而实现向目录服务器中添加子节点的功能。

关于JNDI 的Reference

我为什么单独把他拿出来说呢,是因为后续的关于JNDI的安全性主要都是与这个类息息相关,所以准备详细说一下方便后续讲解

在 JNDI 中,Reference 是一个抽象类,表示一个对象的引用。一个 Reference 对象保存了与对象引用相关的信息,包括引用对象的地址、类名、工厂类、构造参数等。同时,Reference 对象还支持扩展属性的自定义添加。
上述大部分实现,都在使用lookup在进行资源的查找,而本身也是可以进行将引用对象以及命名进行绑定供其他服务查找的,就像之前RMI的实现一样,但之前实现的示例代码中引用的远程对象是在本地实现的,那我们可不可以允许使用系统以外的对象进行绑定呢,比如说在某些目录服务中直接引用远程的java对象

答案肯定是可以的,在 JNDI 服务中,可以使用 Reference 对象来表示系统之外的对象。当我们需要使用 JNDI 来管理和存储对象时(比如数据源对象、连接池对象等等),我们可以将这些对象表示为 Reference 对象,并将其绑定到 JNDI 命名空间中的相应名称上。在使用 JNDI API 查询这些对象时,我们可以通过查询到相应的 Reference 对象,获取到对象的引用和相关属性,从而实现对象的动态管理和存储。

这部分我们留到下一章去讲,这里只开一个头

引用

Java 命名和目录服务JNDI_已改行的博客-CSDN博客
Site Unreachable

CATALOG
  1. 1. 前言
  2. 2. JNDI的实现
    1. 2.1. 使用到的包和类
      1. 2.1.1. 关于Context和InitialContext 类
        1. 2.1.1.1. InitialContext构造器
      2. 2.1.2. 常用方法
    2. 2.2. Naming Service 命名服务
    3. 2.3. 实现
      1. 2.3.1. 本地服务实现
      2. 2.3.2. 远程服务实现
    4. 2.4. Directory Service 目录服务
      1. 2.4.1. 关于JNDI目录服务的实现
      2. 2.4.2. 实现包
      3. 2.4.3. 关于InitialDirContext类
    5. 2.5. 实现代码
    6. 2.6. 关于JNDI 的Reference
  3. 3. 引用