- Java EE
- Java MVC
- MVC 工作流程
- Servlet
- Servlet 的配置
- Servlet工作流程&生命週期
- JSP
- EL 表示式
- Filter
- Filter和Servlet
- JDBC
- RMI
- RMI的工作原理
- JNDI
- JNDI命名和目錄服務
- JNDI工作原理
- JNDI RMI遠端方法呼叫
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 的配置有兩種方式:
-
Servlet 3.0 之前的版本都是在 web.xml 中配置。
-
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 主要基於 JRMP(Java Remote Method Protocol,Java 遠端訊息交換協議)實現的,JRMP 是 Java 平臺的一個私有協議,JRMP 是建立在 TCP/IP 協議棧之上的,通常使用埠 1099 作為 RMI 登錄檔的預設埠。
RMI 是物件導向的,允許物件之間進行通訊,使用 Java 的序列化機制來傳輸物件狀態。當一個物件被傳遞到另一個 JVM 時,它的狀態會被序列化並透過網路傳送,然後在接收端被反序列化。
RMI 分為三個主體部分:
-
Client 客戶端:客戶端呼叫服務端的方法
-
Server 服務端:遠端呼叫方法物件的提供者,也是程式碼真正執行的地方,將執行結果返回給 Client 客戶端
-
Registry 登錄檔:提供了一個命名服務,允許客戶端透過名稱查詢遠端物件的引用,其實本質就是一個 MAP,RMI 登錄檔通常執行在預設埠 1099 上
值得注意的是,RMI 通訊中所有的物件都是透過 Java 序列化傳輸的,只要有 Java 物件反序列化操作就有可能有漏洞。
RMI的工作原理
Client 端有一個本地代理物件被稱為 Stub,負責將方法呼叫引數序列化為網路訊息,並將其傳送到遠端服務段。
Server 端有一個接收這個訊息的物件被稱為 Skeleton,負責將接收到的網路訊息反序列化為方法呼叫,並將其傳遞給實際的遠端物件進行處理。
這種 Stub 和 Skeleton 的機制使得遠端呼叫在客戶端和服務端之間建立了一箇中介,隱藏了底層通訊的細節,簡化了遠端方法呼叫的實現。
假設我們在服務端定義一個遠端介面 RemoteInterface:
import java.rmi.Remote;
import java.rmi.RemoteException;
// 使用 public 宣告,同時需要繼承 Remote 介面
public interface RemoteInterface extends Remote {
String sayHello() throws RemoteException;
}
在服務端實現這個介面的遠端物件 RemoteObject:
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
// 繼承 UnicastRemoteObject 類,預設 socket 進行通訊
public class RemoteObject extends UnicastRemoteObject implements RemoteInterface {
// 建構函式需要丟擲一個 RemoteException 異常
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 埠預設1099
Registry registry = LocateRegistry.createRegistry(1099);
// 繫結 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", 1099);
// 透過登錄檔 Naming.lookup 查詢遠端物件
RemoteInterface remoteObject = (RemoteInterface) registry.lookup("RemoteObject");
// 程式碼呼叫遠端物件上的 sayHello() 方法
String message = remoteObject.sayHello();
System.out.println("Message from server: " + message);
}
}
啟動服務端,當客戶端呼叫 sayHello() 方法時,遠端伺服器上的 RemoteObject 物件將被呼叫。
上面這個簡單的示例,演示了一個 RMI 服務的正常業務流程,但是 RMI 反序列化漏洞其實不是出現在業務本身,而是 Java 的 RMI 服務和反序列化機制。
關於更多RMI通訊的細節,可參考文章:RMI-反序列化 (通訊細節-反序列化)
JNDI
JNDI(Java Naming and Directory Interface)是 Java 提供的一種用於訪問命名和目錄服務的 API。透過呼叫 JNDI 的 API 應用程式可以定位資源和其他程式物件。這些物件可以儲存在不同的命名或目錄服務中,例如 RMI、LDAP、DNS、JDBC、CORBA、NIS。
這看起來似乎不太好理解,其實 JNDI 本質上是一個讓配置引數和程式碼解耦的一種規範和思想。
比如,JDBC來連線資料庫,我們可以選擇在程式碼中直接寫入資料庫的連線引數,旦如果資料來源發生更改,就必須要改動程式碼後重新編譯才能連線。如果將連線引數改成外部配置的方式,就實現了配置和程式碼之間的解耦。
JNDI命名和目錄服務
- Naming Service
命名服務是將名稱和物件進行關聯,提供透過名稱找到物件的操作,稱為"繫結"。
- Directory Service
目錄服務是命名服務的擴充套件,除了提供名稱和物件的關聯,還允許物件具有屬性。目錄容器環境中儲存的是物件的屬性資訊。
JNDI工作原理
-
上下文(Context):JNDI 中一個上下文是一系列名稱和物件的繫結的集合,應用程式透過上下文來查詢和訪問物件。
-
命名服務提供者:JNDI 使用命名服務提供者來實現對不同命名服務的訪問,不同的命名服務有對應的命名服務提供者。
-
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() 方法。
參考文章:
《Java程式碼審計入門篇》
若有錯誤,歡迎指正!o( ̄▽ ̄)ブ