Java安全基础之Java Web核心技术

Java EE

Java 平台有 3 个主要版本:

  • Java SE(Java Platform Standard Edition,Java平台标准版)

  • Java EE(Java Platform Enterprise Edition,Java 平台企业版)

  • Java ME(Java Platform Micro Edition,Java 平台微型版)

其中 Java EE 是 Java 应用最广泛的版本。Java EE 也称为 Java 2 Platform 或 Enterprise Edition(J2EE),它提供了一套全面的技术规范和API,用于构建分布式、可伸缩、安全的企业级应用程序。

几乎所有的 Java Web 应用都是基于 Java EE 平台开发。

Java MVC

当谈到 Web 应用的时候就不得不提到大名鼎鼎的 MVC。

MVC(Model-View-Controller)框架是一种设计模式,它将应用程序分为三个核心部分:模型(Model)、视图(View)和控制器(Controller)。目的是更好地组织和管理应用程序的代码以提高代码的可维护性、可扩展性和可重用性。

MVC 工作流程

首先 Controller 层接收用户的请求,并决定应该调用哪个 Model 来进行处理。然后由 Model 使用逻辑处理用户的请求并返回数据。最后返回的数据通过 View 层呈现给用户。

MVC 的主要优势

  • 分离关注点: MVC 将应用程序的数据逻辑、用户界面和用户交互分离开来,降低耦合度,开发者可以更加关注各自的功能。

  • 可重用性: MVC 将应用程序分为模型、视图和控制器,每个部分都可以独立开发,可以在不同的项目中重复使用。

  • 易于维护: MVC 使代码分为不同的模块,每个模块都有特定的责任,使得应用程序的维护更加简单。

MVC 充分展现了 低耦合(Low Coupling)和 高内聚(High Cohesion)这两个重要的软件设计原则。

Java MVC 模式与普通 MVC 的区别不大:

  • 模型(Model):负责管理数据的状态和行为,以及处理与数据相关的操作。模型通常包括实体类、数据访问对象(DAO)、业务逻辑层等组件。

  • 视图(View):负责展示应用程序的用户界面。它将模型中的数据以可视化的形式呈现给用户,并负责接收用户的输入。

  • 控制器(Controller):控制器充当模型和视图之间的中介,负责处理用户的请求并作出相应的响应。控制器通常由Java类实现,处理URL映射和请求路由。

比较常见的一些Java MVC 框架:Struts 2、Spring MVC、JSF 等。

Servlet

Servlet 毫不夸张地说可以是 Java EE 的核心技术,也是所有 MVC 框架的实现的根本。

Servlet 主要用于创建 Web 应用程序中的服务器端组件,能够接收来自客户端(浏览器)的请求,并生成相应的响应。使用 Servlet 来处理一些较为复杂的服务器端的业务逻辑。

Servlet 的配置

Servlet 的配置有两种方式:

  1. Servlet 3.0 之前的版本都是在 web.xml 中配置。

  2. Servlet 3.0 之后的版本使用注解来配置。

在 web.xml 中,Servlet 的配置在 Servlet 标签中,Servlet 标签是由 Servlet 和 Servlet-mapping 标签组成,两者通过在 Servlet 和 Servlet-mapping 标签中相同的 Servlet-name 名称实现关联。

比如这样:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">

    <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>

</web-app>

<servlet> 用于定义一个 Servlet,其中 <servlet-name> 标签用于指定 Servlet 的名称,<servlet-mapping> 标签用于将 Servlet 映射到 URL 地址。

使用注解配置,在 Servlet 类中直接使用注解来定义 Servlet 的属性和映射关系。

比如这样:

@WebServlet(name = "HelloServlet", urlPatterns = {"/hello"})
public class HelloServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
		Code...
    }
}

使用 @WebServlet 注解来定义 Servlet,也可以指定 Servlet 的名称和 URL 映射等。

Servlet 工作流程&生命周期

1、加载和初始化阶段:

  • Servlet 容器启动时,会加载部署配置的 Servlet 类。

  • 加载后,容器会实例化 Servlet 类,并调用其 init() 方法进行初始化。

    在 init() 方法中,可以进行一些初始化操作,如读取配置文件、建立数据库连接等。init() 方法只会在 Servlet 的生命周期中被调用一次。

2、请求处理阶段:

  • 当客户端发送请求到达时,Servlet 容器会根据配置信息,将请求映射到相应的 Servlet。

  • Servlet 容器会创建一个 HttpServletRequest 对象和一个 HttpServletResponse 对象,并将它们传递给相应的 Servlet 的 service() 方法。

    在 service() 方法中,Servlet 根据请求类型(GET、POST 等)调用相应的处理方法,如 doGet()、doPost() 等。

3、销毁阶段:

  • 当 Servlet 容器关闭或者 Servlet 长时间不被使用时,容器会调用 Servlet 的 destroy() 方法进行销毁。

    在 destroy() 方法中,开发者可以进行一些清理操作,如关闭数据库连接、释放资源等。

在 Servlet 的整个生命周期中,init() 和 destroy() 方法只会被调用一次,而 service() 方法会根据请求的到达而被多次调用。整个生命周期的管理由 Servlet 容器负责。

JSP

JSP (JavaServer Pages) 是与 PHP、ASP 等类似的脚本语言,JSP 是为了简化 Servlet 的处理流程而出现的替代品。

在 JSP 中可以直接调用 Java 代码,这就导致了一些安全问题,比如一些 JSP 的Webshell。虽然说现在比较新的 Java MVC 框架中已经放弃了 JSP,但还是需要稍稍了解一点。

工作原理

从本质上说 JSP 就是一个Servlet,在 JSP 页面在第一次被访问时会被 Servlet 容器(如 Tomcat)编译成一个特殊的 Servlet,并在服务器上运行。

当客户端请求一个 JSP 页面时,Servlet 容器将 JSP 页面转换成一个 Servlet,并执行其中的 Java 代码。然后,Servlet 生成 HTML 页面,并将其发送给客户端。

JSP 的基本语法

指令

  • <%@ 开头,以 %> 结尾,用于设置全局的 JSP 属性引入 Java 类库等。

  • <%@ page ... %> 定义网页依赖属性,比如脚本语言、error页面、缓存需求等。

  • <%@ include ... %> 包含其他文件(静态包含)。

脚本:

  • <% 开头,以 %> 结尾,用于插入 Java 代码块,可以在其中编写任意的 Java 代码。

表达式:

  • <%= 开头,以 %> 结尾,用于输出 Java 表达式的结果到页面上。

EL 表达式

EL(Expression Language)表达式,常用于在 JSP 页面中插入和操作数据。

可以直接访问 JavaBean 对象的属性,例如 ${user.name} 可以获取名为 "name" 的属性值。

可以调用 JavaBean 对象的方法,并获取返回值。例如 ${user.getName()} 可以调用 "getName" 方法并获取返回值。

Filter

在 Java Servlet 中,过滤器(Filter)是一种用于在请求被处理之前或之后执行某些任务的组件。主要用于过滤 URL 请求,通过 Filter 我们可以实现 URL 请求资源权限验证、用户登陆检测等功能。

Filter 的生命周期由容器管理,通过实现 javax.servlet.Filter 接口来创建,可以在 web.xml 或者使用注解来对 Filter 进行配置。

实现一个 Filter 只需要重写 init()、doFilter()、destroy() 方法,过滤逻辑都在 doFilter 方法中实现。

比如这样:

public class MyFilter implements Filter {
    public void init(FilterConfig config) throws ServletException {
        // 过滤器初始化时执行的操作
    }

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        // 执行过滤逻辑,对请求进行处理
        chain.doFilter(request, response); // 将请求传递给下一个过滤器或目标 Servlet
        // 在此可对响应进行处理
    }

    public void destroy() {
        // 过滤器销毁时执行的操作
    }
}

对于基于 Filter 和 Servlet 实现的项目,代码审计的重心集中于找出所有的 Filter 分析其过滤规则,找出是否有做全局的安全过滤、敏感的 URL 地址是否有做权限校验并尝试绕过 Filter 过滤。

Filter 和 Servlet

Filter 和 Servlet 基础概念不一样,Servlet 定义是容器端小程序,用于直接处理后端业务逻辑,而 Filter 的思想则是实现对 Java Web 请求资源的拦截过滤。

filter 的生命周期与 Servlet 的生命周期比较类似,在一个生命周期中,filter 和 Servlet 都经历了被加载、初始化、提供服务及销毁的过程。

JDBC

JDBC (Java Database Connectivity) 是 Java 语言访问数据库的标准 API。

使用 JDBC 连接数据库通常包括以下步骤:

1、加载数据库驱动程序:使用 Class.forName() 方法加载数据库驱动程序,使 JVM 能够与数据库通信。

2、建立数据库连接:使用 DriverManager.getConnection() 方法建立与数据库的连接。

3、创建和执行 SQL 语句:创建一个 Statement 对象或者 PreparedStatement 对象,用于执行 SQL 查询或更新操作。

4、处理结果集:如果执行的是查询操作,那么将返回一个结果集 ResultSet 对象,通过遍历该结果集来获取查询结果。

5、关闭连接和资源:在使用完数据库连接和相关资源后,需要关闭它们以释放数据库资源。

import java.sql.*;

public class JDBCExample {
    public static void main(String[] args) {
        Connection conn = null;
        Statement stmt = null;
        ResultSet rs = null;

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

            // 2. 建立数据库连接
            String url = "jdbc:mysql://localhost:3306/mydatabase";
            String username = "root";
            String password = "password";
            conn = DriverManager.getConnection(url, username, password);

            // 3. 创建并执行 SQL 查询
            stmt = conn.createStatement();
            String sql = "SELECT * FROM mytable";
            rs = stmt.executeQuery(sql);

            // 4. 处理结果集
            while (rs.next()) {
                // 处理每一行数据
                int id = rs.getInt("id");
                String name = rs.getString("name");
                System.out.println("ID: " + id + ", Name: " + name);
            }
        } catch (SQLException | ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            // 5. 关闭连接和资源
            try {
                if (rs != null) rs.close();
                if (stmt != null) stmt.close();
                if (conn != null) conn.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}

为什么第一步需要 Class.forName ?

这一步是利用了 Java 反射和类加载机制往 DriverManager 中注册了驱动包。注册驱动程序是为了使得 DriverManager 能够识别和管理特定的数据库驱动程序。

真实的 Java 项目中通常不会使用原生的 JDBC 的 DriverManager 去连接数据库,而是使用数据源 (javax.sql.DataSource) 来代替 DriverManager 管理数据库的连接。JDK 不提供 DataSource 的具体实现,而它的实现来源于各个驱动程序供应商或数据库访问框架,例如 Spring JDBC、Tomcat JDBC、MyBatis、Druid、C3P0、Seata 等。

JDBC 有两种方法执行 SQL 语句,分别为 Statement 和 PrepareStatement。

  • Statement 是 Java 中执行静态 SQL 语句的接口,每次执行 SQL 语句时都会将 SQL 语句发送给数据库进行解析和编译。

  • PreparedStatement 是 Statement 的子接口,执行时 SQL 语句时会被预先编译,可以通过设置参数来动态地填充 SQL 语句中的占位符。

正确使用 PrepareStatement 可以有效避免 SQL 注入的产生,使用 “?” 作为占位符时,填入对应字段的值会进行严格的类型检查。

Mysql 预编译

Mysql 默认也提供了预编译命令 prepare,使用 prepare 命令可以在 Mysql 数据库服务端实现预编译查询。

RMI

RMI(Remote Method Invocation,远程方法调用)是 Java 中用于实现远程通信的机制。它允许在不同的 Java 虚拟机(JVM)之间调用方法,就像调用本地方法一样。

RMI 的工作原理

RMI 有一个 Client 端和一个 Server 端。

Client 端有一个本地代理对象被称为 Stub,负责将方法调用参数序列化为网络消息,并将其发送到远程服务段。

Server 端有一个接收这个消息的对象被称为 Skeleton,负责将接收到的网络消息反序列化为方法调用,并将其传递给实际的远程对象进行处理。

这种 Stub 和 Skeleton 的机制使得远程调用在客户端和服务端之间建立了一个中介,隐藏了底层通信的细节,简化了远程方法调用的实现。

假设我们定义一个远程接口 RemoteInterface:

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface RemoteInterface extends Remote {
    String sayHello() throws RemoteException;
}

我们实现这个接口的远程对象 RemoteObject:

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class RemoteObject extends UnicastRemoteObject implements RemoteInterface {
    public RemoteObject() throws RemoteException {
        super();
    }

    @Override
    public String sayHello() throws RemoteException {
        return "Hello from RemoteObject!";
    }
}

在 RMI 服务端注册启动:

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class Server {
    public static void main(String[] args) throws Exception {
        RemoteObject remoteObject = new RemoteObject();
        // 注册 RMI 端口
        Registry registry = LocateRegistry.createRegistry(10099);
        // 绑定 Remote 对象
        registry.rebind("RemoteObject", remoteObject);
        System.out.println("Server started.");
    }
}

在客户端调用远程方法:

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class Client {
    public static void main(String[] args) throws Exception {
        // 获取到指定主机和端口上运行的 RMI 注册表的引用
        Registry registry = LocateRegistry.getRegistry("localhost", 10099);
        // 通过注册表查找远程对象
        RemoteInterface remoteObject = (RemoteInterface) registry.lookup("RemoteObject");
        // 代码调用远程对象上的 sayHello() 方法
        String message = remoteObject.sayHello();
        System.out.println("Message from server: " + message);
    }
}

启动服务端,当客户端调用 sayHello() 方法时,远程服务器上的 RemoteObject 对象将被调用。

注意:RMI 通信中所有的对象都是通过 Java 序列化传输的,只要有Java对象反序列化操作就有可能有漏洞。

JNDI

JNDI(Java Naming and Directory Interface)是 Java 提供的一种用于访问命名和目录服务的 API。通过调用 JNDI 的 API 应用程序可以定位资源和其他程序对象。这些对象可以存储在不同的命名或目录服务中,例如 RMI、LDAP、DNS、JDBC、CORBA、NIS。

这看起来似乎不太好理解,其实 JNDI 本质上是一个让配置参数和代码解耦的一种规范和思想

比如,JDBC来连接数据库,我们可以选择在代码中直接写入数据库的连接参数,旦如果数据源发生更改,就必须要改动代码后重新编译才能连接。如果将连接参数改成外部配置的方式,就实现了配置和代码之间的解耦。

JNDI 命名和目录服务

  1. Naming Service

命名服务是将名称和对象进行关联,提供通过名称找到对象的操作,称为"绑定"。

  1. Directory Service

目录服务是命名服务的扩展,除了提供名称和对象的关联,还允许对象具有属性。目录容器环境中保存的是对象的属性信息。

JNDI 的工作原理

  1. 上下文(Context):JNDI 中一个上下文是一系列名称和对象的绑定的集合,应用程序通过上下文来查找和访问对象。

  2. 命名服务提供者:JNDI 使用命名服务提供者来实现对不同命名服务的访问,不同的命名服务有对应的命名服务提供者。

  3. JNDI API:Java 应用程序通过 JNDI API 来与上下文和命名服务提供者进行交互,执行资源查找和操作。

如何使用 JNDI 来查找和访问一个命名和目录服务中的对象(假设这个对象是一个字符串):

// 1. 创建 InitialContext 对象
Context ctx = new InitialContext();

// 2. 指定 JNDI 名称( JNDI 路径)
String jndiName = "java:/comp/env/myString";

// 3. 查找对象
String myString = (String) ctx.lookup(jndiName);

// 4. 访问对象
System.out.println("Found string: " + myString);

// 5. 关闭 InitialContext
ctx.close();

有了 JDNI 之后,我们可以将一些与业务无关的配置转移到外部,更好的方便项目的维护。

JNDI RMI 远程方法调用

JNDI 和 RMI 结合使用时,可以通过 JNDI 来查找远程对象的引用,然后使用 RMI 来调用远程对象的方法。

在服务器端:

  • 创建远程对象的实现,并将其导出为 RMI 服务。

  • 将远程对象的引用绑定到 JNDI 目录中,以便客户端能够查找到它。

在客户端:

  • 使用 JNDI 查找远程对象的引用。

  • 通过 RMI 调用远程对象的方法。

接着使用上面的 RMI 服务器端:

import java.rmi.server.UnicastRemoteObject;
import javax.naming.Context;
import javax.naming.InitialContext;

public class JRServer {
    public static void main(String[] args) throws Exception {
        // 创建远程对象的实现
        RemoteObject remoteObject = new RemoteObject();

        // 导出远程对象为 RMI 服务
        RemoteObject stub = (RemoteObject) UnicastRemoteObject.exportObject(remoteObject, 0);

        // 将远程对象的引用绑定到 JNDI 目录中
        Context namingContext = new InitialContext();
        namingContext.bind("rmi://localhost/RemoteObject", stub);

        System.out.println("Server started.");
    }
}

在客户端:

import javax.naming.Context;
import javax.naming.InitialContext;

public class JRClient {
    public static void main(String[] args) throws Exception {
        // 使用 JNDI 查找远程对象的引用
        Context namingContext = new InitialContext();
        RemoteObject remoteObject = (RemoteObject) namingContext.lookup("rmi://localhost/RemoteObject");

        // 通过 RMI 调用远程对象的方法
        String message = remoteObject.sayHello();
        System.out.println("Message from server: " + message);
    }
}

使用 JNDI 查找了名为 "rmi://localhost/RemoteObject" 的远程对象的引用,然后通过 RMI 调用了远程对象的 sayHello() 方法。


若有错误,欢迎指正!o( ̄▽ ̄)ブ

热门相关:福慧双全   医门宗师   侯门弃女:妖孽丞相赖上门   四爷又被福晋套路了   永恒王权