JAVA

JNDI 和 JDBC 的区别-个人理解

网上关于 JNDI 和 JDBC 的定义有很多,但是都很官方不容易理解,下面是我最近查阅资料得出的心得体会。希望对你在理解上有一点点的帮助,说的不对的请指正哦。

JDBC


看到最多的就是,Java Database Connectivity (JDBC)是一个标准的Java API,它由一组类和接口组成,Java应用程序开发人员使用它来访问数据库和执行SQL语句

通俗的讲就是JDBC用来连接数据库和执行SQL语句,但是它最大的特点是通过java程序去找数据库驱动,然后来连接数据库。是java亲自去连数据库。 

JNDI


JNDI(Java Name Directory Interface,Java命名和目录接口),它不仅仅是进行数据库定位的,它是给当前应用服务器所管理的所有资源一个唯一的标识,包括数据库,网页,文件,连接池等等。

JNDI提供了一种统一的方式,可以用在网络上查找和访问服务。通过指定一个资源名称,该名称对应于数据库或命名服务中的一个记录,同时返回数据库连接建立所必须的信息。

JNDI主要有两部分组成:应用程序编程接口和服务供应商接口。应用程序编程接口提供了Java应用程序访问各种命名和目录服务的功能,服务供应商接口提供了任意一种服务的供应商使用的功能。

代码示例:

try{
    Context cntxt = new InitialContext();
    DataSource ds = (DataSource) cntxt.lookup("jdbc/dpt");
 
}
 
catch(NamingException ne){
    ...

}

通俗的讲,JNDI不单单是用来连接数据库的,它是通过命名服务来找到数据库并返回数据库连接,当然JNDI还可以管理当前应用服务器上的其他资源,如网页,文件等,它用来连接数据库时和JDBC最大的区别就是它是通过应用服务器配置(如Tomcat)的配置文件context.xml来找数据库驱动的,其次就是JDBC连接能承受的同时请求数太低了,JNDI连接池连接与之相比会好很多。

总结:

  • 请求资源的主动性不一样。JDBC是通过java程序主动去连接数据库获得连接,而JNDI是通过请求命名服务器返回数据库连接。
  • 功能范围不一样。JDBC只能用来做一件事那就是连接数据库,而命名JNDI除了能和JDBC做同样的事以外还能管理当前应用服务器上的其他资源,如网页,文件等
  • 请求连接数不通。JNDI连接池数高于JDBC


JNDI漏洞


前段时间发生的 Log4j2 漏洞事件着实让人有点蛋疼,可以说是值得广大中国企业技术人员纪念的日子。在修复漏洞的过程中,让人看到的是:在风险面前,我们的系统就像一个裸奔的男人站在海边,被海风肆意地虐打着,毫无反抗力……

加上之前的 Fastjson 事件,以及后来的 Logback 事件,更进一步体现出:基础组件的重要性!技术创新的重要性!科技发展的重要性!尤其在此次的 Log4j2 漏洞上反映的更为淋漓尽致,各种“核弹级漏洞”、“超高危” 等惊人术语啪啪啪打在我们的脸上……

每次规模比较大的漏洞,JNDI好像都不会缺席。最近人尽皆知的Log4j2漏洞也和它有关,让人 不由得怀疑,是不是作者开的后门。

因为JNDI这个玩意,别说用过,很多人连听都没听说过。这么冷门酸爽的东西,有什么理由把它放在一个日志框架里呢?恐怕只有作者想得通。

数据库驱动


很多人接触JNDI,是从数据库的驱动开始的。当然,随着SpringBoot单体发布模式的流行,现在用这种方式来获取数据库配置的古董公司,是越来越少了。

比如,我们可以在tomcat得server.xml里,配置一个叫做xjjdogDB的资源。

<Resource name="jdbc/xjjdogDB" auth="Container" type="javax.sql.DataSource" maxTotal="100" maxIdle="30" maxWaitMillis="10000"
  username="xjjdog" password="123456" driverClassName="com.mysql.jdbc.Driver"
  url="jdbc:mysql://localhost:3306/xjjdog_db"/>

那么,我们只需要在SpringBoot中配置上JNDI这个名字,它就能加载正确的配置。前提是我们需要把SpringBoot服务打包成WAR包发布。像JBoss这样的宣称企业级服务器的软件,就喜欢这么干。

 spring:
    datasource:
    jndi-name: jdbc/xjjdogDB

从这里,我们可以看出。JNDI到底是个神马玩意呢?你可以认为它是一个配置中心,也可以认为它是一个命名服务。其根本功能,就是让你可以通过一个简短的字符串,就能获取一系列的复杂配置和初始化后的功能。

这样,我们就可以避免将这些配置直接写在项目里。程序启动起来,到底加载的什么东西,要看运行的环境配置的什么东西。

具体实现的话,不就是一个可以根据key获取value的HashMap嘛。

关键是这个value,它不是String,它是一个Object。要从字符串变身为一个正常的类,还要做到通用,那就不得不依靠反射。

Log4j2 漏洞回顾


在解析 JNDI 前,我们先回顾一下 Log4j 2 漏洞事件,大概的脉络是:Apache Log4j 漏洞( JNDI注入 CVE-2021-44228 ),使得攻击者仅需向目标输入一段代码,不需要用户执行任何多余操作即可触发该漏洞,使攻击者可以远程控制用户受害者服务器。依据官方的描述:Apache Log4j2 中存在 JNDI 注入漏洞,当程序将用户输入的数据进行日志记录时,即可触发此漏洞,成功利用此漏洞可以在目标服务器上执行任意代码。转换成专业术语,即:若日志内容中包含关键词 “${” ,在输出 Log 的时候,攻击者便能够将关键字所包含的内容当作变量来解析成任何可以攻击的命令,并进行执行,以破坏应用系统。

关于 Apache log4j2 漏洞的检测及修复,本文暂不做描述,大家有兴趣可以去官网查阅。说到这里,可能很多新手就会问:“Log4j2 跟 JNDI 有毛关系?” 这不说了个寂寞 ……接下来,我们先看如下图所示:

基于上图,我们可以看到:所有的“关联”来自于 Lookups 组件。Log4j2 的强大之处在于,除了能够输出程序中的变量信息,同时,其还引入了一个叫 Lookup 的破玩意,可以用来输出更多内容。

Lookups,顾名思义,理解为“查找、搜索”,即允许在输出日志的时候,通过某种方式去查找要输出的内容。换言之,这家伙相当于是一个接口,具体去哪里查找,怎么查找,就需要编写具体的模块去实现了,类似于面向对象编程中多态思想。试想想这样一组场景:假设某一场景中需要通过日志输出一个 Java 对象,此时,这个对象在程序中没有定义,而是在其他环境中,这种情况下如何处理呢?在 Log4j2 官网 https://logging.apache.org/log4j/2.x/manual/lookups.html 我们可以看到 Log4j2 已经帮我们把常见的查找途径都进行实现了,具体如下所示:

通过上图,我们可以看到,Lookups 提供了一种在任意位置向 Log4j 配置添加值的方法。其实现的查找途径较为广泛,几乎支持所有的环境。它们是实现 StrLookup 接口的特定类型的插件。有关如何在配置文件中使用查找的信息,请参见 Configuration 页面的 Property Substitution 部分。

JNDI 体系解析


在上面的 Log4j2 Lookups 路径中,定义了 JNDI 环境入口,接下来,我们来了解一下 JNDI Lookups,JndiLookup 允许通过 JNDI 进行变量检索。在默认情况下,键的前缀为 java:comp/env /,但是,如果键包含“:”,则不会添加前缀。其具体的配置信息如下所示:

那么,什么是 JNDI 呢?JNDI 即全称为 “Java Naming and Directory Interface”,中文释义为 JAVA 命名和目录接口,它提供一个目录系统,并将服务名称与对象关联起来,从而使得开发人员在开发过程中可以使用名称来访问对象。站在数据请求角度可以这样理解:

存在一个类似于字典的数据源,应用程序可以通过 JNDI 接口,传一个特定的参数进去,就能获取到对象信息。但是,不同的数据源有不同的查找方式,所以 JNDI 也只是一个上层封装,在它下面也支持很多种具体的数据源。我们来了解一下其体系架构,具体如下所示:

基于上述架构图,我们可以看到:JNDI 体系结构由一个 API 和一个服务提供者接口 (Service Provider Interface) 组成。Java 应用程序使用 JNDI API 来访问各种命名和目录服务。SPI 允许以透明方式插入各种命名和目录服务,从而允许使用 JNDI API 的 Java 应用程序访问其服务。SPI 作为一种服务发现机制,通过在 ClassPath 路径下的 META-INF/services 文件夹查找文件,自动加载文件里所定义的类。这一机制为很多框架扩展提供了可能,比如在 Dubbo、JDBC 中都使用到了 SPI 机制。

     通常来讲,JNDI 由以下软件包组成:

     javax.nameing:包含用于访问命名服务的类和接口。

     javax.nameing.directory:扩展核心 javax.命名包,以提供除命名服务之外访问目录的功能。

     javax.nameing.event:包含用于支持命名和目录服务中的事件通知的类和接口。

     javax.nameing.ldap:包含用于支持 LDAPv3 扩展操作和控制的类和接口。

     javax.nameing.spi:包含允许在 JNDI 下动态插入各种命名和目录服务提供程序的类和接口。

接下来,我们再来了解一下 JNDI 服务提供商相关概念,通常,要将 JNDI 与特定的命名或目录服务配合使用,我们需要一个 JNDI 服务提供程序,该提供程序是插入 JNDI API 下方以访问命名或目录服务的模块。目前,Java SE 发行版包括以下服务提供程序:

  • LDAP 服务提供程序
  • COS 命名服务提供商
  • RMI 注册服务提供程序
  • 域名解析服务提供商

在整个Java 生态体系栈中,Java 命名和目录接口 (JNDI) 是 Java 平台的一部分,它为基于 Java 技术的应用程序提供了多个命名和目录服务的统一接口。我们可以使用此行业标准构建功能强大且支持便携式目录的应用程序。

命名和目录服务通过提供有关用户、计算机、网络、服务和应用程序的各种信息的全网络共享,在 Intranet 和 Internet 中发挥着至关重要的作用。基于协同支撑角度而言,JNDI 与 Java Platform、Enterprise Edition (Java EE) 中的其他技术协同工作,以在分布式计算环境中组织和定位组件。

目前,JNDI 主要应用于如下 Java 版本中:Java SE 8 |Java SE 7 |Java SE 6 |J2SE 5.0 |J2SE 1.4.2

在实际的业务场景中,使用 JNDI 的任何工作都需要了解底层服务以及可访问的实现。例如,数据库连接服务调用特定属性和异常处理。但是,在通常情况下,JNDI 的抽象将连接配置与应用程序分离。让我们来探讨一下包含 JNDI 核心功能的名称。

以 Name Interface 名称接口为例,具体如下所示:

Name objectName = new CompositeName("java:com/env/jdbc");

Name Interface 名称接口提供了管理 JNDI 名称的组件名称和语法的功能。字符串的第一个标记表示全局上下文,之后添加的每个字符串表示下一个子上下文:

Enumeration<String> elements = objectName.getAll();while(elements.hasMoreElements()) {
  System.out.println(elements.nextElement());
}

其输出结果为:

java:com
env
jdbc

如我们所定义,/ 是 Name 子上下文的分隔符。现在,让我们添加一个子上下文:

jectName.add("example");

然后,我们再进行测试所添加的参数信息:

assertEquals("example", objectName.get(objectName.size() - 1));

我们再以 MySql 数据库为例,简要介绍下 JNDI 在应用系统中如何运行,我们在 Java 环境中配置一个数据库连接,例如配置名为“java:MySqlDS”。

然后别的 Java 进程通过 JNDI 去查找 ”java:MysqlDs“, 接着就会得到一个数据库连接。其代码配置信息如下所示:

Connection conn=null;   // Context 为 JNDI 的类

Context ctx = new InitialContext(); // JNDI 关键方法,通过 Loopup 找一个对象

Object datasourceRef = ctx.lookup("java:MySqlDS"); //引用数据源 

DataSource ds = (Datasource) datasourceRef; 

conn = ds.getConnection(); 

...... 

c.close(); 

最后,我们来了解一下 JNDI 在现代应用程序体系架构中的作用。虽然 JNDI 在轻量级的容器化 Java 应用程序(如 Spring Boot)中扮演的角色越来越少,但还有其他用途。比如,在与后端组件交互,仍然使用 JNDI 的三种 Java 技术是 JDBC、EJB 和 JMS。所有这些都在 Java 企业应用程序中具有广泛的用途。

从管理规范角度而言,单独的 DevOps 团队可以管理相关环境变量资源,例如所有环境中敏感信息(数据库连接的用户名和密码)。可以在 Web 应用程序容器中创建 JNDI 资源,将 JNDI 用作在所有环境中工作的一致抽象层。基于此设置,允许开发人员创建和控制用于开发的本地定义,同时通过相同的 JNDI 名称连接到生产环境中的敏感资源。

最后,从本质上而言, JNDI 并没较多的内容,其仅仅是作为一种 Interface 存在,不过,正因为很不起眼,很容易让大家忽略掉,所以,一旦出现安全风险,还是很致命的。以上为 JNDI 的简要解析,希望对大家有用。