一个请求是怎么被Java后端处理的,sql注入代码,filter和listener机制
Servlet是Java里专门用来处理HTTP请求的"小工人"。浏览器发来一个请求,Tomcat(服务器)找到对应的Servlet,让它干活(处理请求),然后把结果返回给浏览器。
💡 类比:Servlet就像餐厅里的服务员。顾客(浏览器)点菜(发请求),服务员(Servlet)把菜单交给后厨(业务逻辑),再把做好的菜(响应)端给顾客。
https://blog.csdn.net/qq_52173163/article/details/121110753
一个Servlet从生到死,只会经历三个阶段,非常重要,审计时经常利用生命周期做手脚:

┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ 出生 │ → │ 干活 │ → │ 死亡 │ │ init() │ │ service() │ │ destroy() │ │ 初始化 │ │ 处理请求 │ │ 销毁 │ │ 只执行1次 │ │ 每次请求都执行│ │ 只执行1次 │ └─────────────┘ └─────────────┘ └─────────────┘
代码示例:
javapackage com.example.daimashenjilearn1;
import java.io.*;
import javax.servlet.ServletException; // 改这里
import javax.servlet.http.*; // 改这里
import javax.servlet.annotation.*; // 改这里
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
@Override
public void init() throws ServletException{
System.out.println("Servlet出生");
}
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String name=request.getParameter("name"); //这里主要是后面的name参数,我们要给后面的name来进行传参
response.setContentType("text/html;charset=utf-8");
response.getWriter().write("<h1>Hello "+name+"</h1>");
}
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
@Override
public void destroy() {
System.out.println("Servlet去世");
}
}
xml<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
这里由于tomcat版本因素所以需要更改为javax,然后使用Maven重新加载项目

这里我们可以看到servlet出生


这里应该是servlet去世,编码问题,
Run → Edit Configurations → 你的 Tomcat 配置 → Server 标签页,找到 VM options(在 JRE 选择框下面,可能需要展开),填入:-Dfile.encoding=UTF-8
访问方式:浏览器输入 http://localhost:8080/项目名/hello?name=小白
路由就是"URL怎么找到对应的Servlet"。有两种配置方式:
java@WebServlet("/login") // 精确匹配
@WebServlet("/user/*") // 通配匹配,/user/add、/user/delete都能进来
@WebServlet("*.do") // 后缀匹配,所有以.do结尾的请求


在 WEB-INF/web.xml 中配置:
xml<?xml version="1.0" encoding="UTF-8"?>
<web-app>
<!-- 定义Servlet的名字和类 -->
<servlet>
<servlet-name>LoginServlet</servlet-name>
<servlet-class>com.example.LoginServlet</servlet-class>
</servlet>
<!-- 定义URL和Servlet的映射关系 -->
<servlet-mapping>
<servlet-name>LoginServlet</servlet-name>
<url-pattern>/login</url-pattern>
</servlet-mapping>
</web-app>


🔍 审计技巧:审计一个老项目时,先看
web.xml,里面列出了所有Servlet和对应的URL,这是你梳理攻击面的第一步。
这是Servlet的两个"法宝",审计时天天打交道:
| 对象 | 作用 | 审计关注点 |
|---|---|---|
| HttpServletRequest | 封装了浏览器发来的所有请求信息 | getParameter()获取的参数是否可控?是否做了过滤? |
| HttpServletResponse | 封装了要返回给浏览器的响应信息 | getWriter().write()输出的内容是否包含用户输入?(XSS) |
Request常用方法:
java// 获取用户输入(审计时的"Source"源头)
String username = req.getParameter("username"); // GET/POST参数
String header = req.getHeader("User-Agent"); // 请求头
Cookie[] cookies = req.getCookies(); // Cookie
String method = req.getMethod(); // GET or POST
// 获取请求路径(审计时看路由匹配)
String uri = req.getRequestURI(); // /项目名/login
String url = req.getRequestURL().toString(); // 完整URL
javapackage com.example.daimashenjilearn1;
import java.io.*;
import javax.servlet.ServletException; // 改这里
import javax.servlet.http.*; // 改这里
import javax.servlet.annotation.*; // 改这里
@WebServlet("*.do")
public class HelloServlet extends HttpServlet {
@Override
public void init() throws ServletException{
System.out.println("Servlet出生");
}
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String name=request.getParameter("fuck");
response.setContentType("text/html;charset=utf-8");
PrintWriter out = response.getWriter();
String fuck = request.getParameter("fuck");
String ua = request.getHeader("User-Agent");
String referer = request.getHeader("Referer");
String xff = request.getHeader("X-Forwarded-For");
Cookie[] cookies = request.getCookies();
String cookieInfo= "没有cookie";
if(cookies != null){
StringBuilder sb = new StringBuilder();
for(Cookie c : cookies){
sb.append(c.getName()).append("=").append(c.getValue()).append(";");
}
cookieInfo = sb.toString();
}
String method=request.getMethod();
String uri=request.getRequestURI();
String url=request.getRequestURL().toString();
String path=request.getServletPath();
String ip=request.getRemoteAddr();
String server=request.getServerName();
int port=request.getServerPort();
out.println("<h1>🔍 请求信息收集器</h1>");
out.println("<hr>");
out.println("<p><b>你的输入(fuck参数):</b> " + (fuck != null ? fuck : "空") + "</p>");
out.println("<p><b>请求方法:</b> " + method + "</p>");
out.println("<p><b>请求URI:</b> " + uri + "</p>");
out.println("<p><b>完整URL:</b> " + url + "</p>");
out.println("<p><b>Servlet路径:</b> " + path + "</p>");
out.println("<p><b>你的IP:</b> " + ip + "</p>");
out.println("<p><b>User-Agent:</b> " + (ua != null ? ua : "空") + "</p>");
out.println("<p><b>Referer:</b> " + (referer != null ? referer : "空") + "</p>");
out.println("<p><b>X-Forwarded-For:</b> " + (xff != null ? xff : "空") + "</p>");
out.println("<p><b>Cookies:</b> " + cookieInfo + "</p>");
}
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
@Override
public void destroy() {
System.out.println("Servlet去世");
}
}

提示
StringBuilder 是一个专门用来拼接字符串的工具。
java// 方式1:用 + 拼接(简单但低效)
String s = "a" + "b" + "c" + "d"; // 底层创建了很多临时对象,浪费内存
// 方式2:用 StringBuilder(高效,审计常见写法)
StringBuilder sb = new StringBuilder(); // 拿一张"草稿纸"
sb.append("a"); // 往上写a
sb.append("b"); // 往上写b
sb.append("c"); // 往上写c
String result = sb.toString(); // 最后把草稿纸内容转成正式字符串
java// 1. 从URL参数里取 "fuck" 的值
// 比如访问 ?fuck=geronimo,name 就是 "geronimo"
String name = request.getParameter("fuck");
// 2. 设置响应格式:HTML网页,UTF-8编码
response.setContentType("text/html;charset=utf-8");
// 3. 拿笔(PrintWriter),准备往浏览器写内容
PrintWriter out = response.getWriter();
// 4. 又取了一遍 fuck 参数(和第一行重复了,可以删掉第一行)
String fuck = request.getParameter("fuck");
// 5. 从请求头里取 User-Agent(浏览器自我介绍)
String ua = request.getHeader("User-Agent");
// 6. 从请求头里取 Referer(你从哪个页面点过来的)
String referer = request.getHeader("Referer");
// 7. ❌ 错误!X-Forwarded-For 是请求头,不是参数!
// 而且一般用户不会带这个头,只有经过代理服务器才会有
String xff = request.getParameter("X-Forwarded-For");
// 8. 获取浏览器带来的所有 Cookie 小纸条
Cookie[] cookies = request.getCookies();
// 9. 默认显示"没有cookie"
String cookieInfo = "没有cookie";
// 10. 如果 cookies 不是空(确实带了Cookie)
if(cookies != null){
// 11. 创建草稿纸,准备拼接Cookie信息
StringBuilder sb = new StringBuilder();
// 12. 遍历每个Cookie(for-each循环)
for(Cookie c : cookies){
// c.getName() 是Cookie名字,如 JSESSIONID
// c.getValue() 是Cookie值,如 8158587...
sb.append(c.getName()).append("=").append(c.getValue()).append(";");
}
// 13. 把草稿纸内容变成字符串,存到 cookieInfo
cookieInfo = sb.toString();
}
// 14. 获取请求方法:GET 或 POST
String method = request.getMethod();
// 15. 获取URI:/项目名/test.do(不包含域名)
String uri = request.getRequestURI();
// 16. 获取完整URL:http://localhost:8080/项目名/test.do
String url = request.getRequestURL().toString();
// 17. 获取Servlet匹配到的路径:/test.do
String path = request.getServletPath();
// 18. 获取访问者IP(服务器看到的直接连接IP)
String ip = request.getRemoteAddr();
// 19. 获取服务器域名/主机名:localhost
String server = request.getServerName();
// 20. 获取服务器端口:8080
int port = request.getServerPort();
⚠️ 审计核心思想:追踪数据流。从
req.getParameter()开始,看这个参数经过了哪些方法,最终有没有到达危险函数(如SQL执行、命令执行、文件操作)。
ServletContext 是整个Web应用的全局共享空间,所有Servlet都能访问它。
java// 存数据(全局配置、共享对象)
getServletContext().setAttribute("appName", "我的应用");
// 取数据
String appName = (String) getServletContext().getAttribute("appName");
// 获取真实路径(文件上传审计常用)
String realPath = getServletContext().getRealPath("/upload");
// 结果:C:\Tomcat\webapps\项目名\upload
javapackage com.example.daimashenjilearn1;
import java.io.*;
import javax.servlet.ServletException; // 改这里
import javax.servlet.http.*; // 改这里
import javax.servlet.annotation.*; // 改这里
@WebServlet("*.do")
public class HelloServlet extends HttpServlet {
@Override
public void init() throws ServletException{
System.out.println("Servlet出生");
// 服务器启动时,往公共储物柜放点东西
getServletContext().setAttribute("appName", "我的应用");
}
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setContentType("text/html;charset=utf-8");
PrintWriter out = response.getWriter();
// 从储物柜取东西
String name=(String)getServletContext().getAttribute("appName");
// 获取/upload在硬盘上的真实位置
String path = getServletContext().getRealPath("/upload");
out.write("应用名:"+name+"<br>");
out.write("真实路径"+path+"<br>");
}
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
@Override
public void destroy() {
System.out.println("Servlet去世");
}
}

🔍 审计注意:
ServletContext里存的东西所有用户共享,如果存了敏感信息,可能被其他用户读取。
当浏览器访问 http://localhost:8080/demo/hello 时:
1. 浏览器发送HTTP请求 ↓ 2. Tomcat监听8080端口,收到请求 ↓ 3. Tomcat解析URL,找到对应的Web应用(demo) ↓ 4. Tomcat根据路由(/hello)找到对应的Servlet类 ↓ 5. 如果是第一次访问,调用init()创建Servlet实例(只一次) ↓ 6. 创建线程,调用service() → doGet()/doPost()处理请求 ↓ 7. Servlet通过Response对象把结果写回浏览器
📎 深入学习:
service 的区别
| 第一个 | 第二个 | |
|---|---|---|
| 来自哪 | Servlet 接口(老祖宗) | HttpServlet 类(子类) |
| 参数 | ServletRequest / ServletResponse | HttpServletRequest / HttpServletResponse |
| 权限 | public(对外公开的) | protected(只给子类用的) |
| 你平时用哪个 | ❌ 几乎不用 | ✅ 重写这个 |
执行顺序为
textTomcat: 调用 service(ServletRequest, ServletResponse) ↓ 你的代码: System.out.println("1. 这是祖宗接口的 service") ↓ super.service(req, res) → 跳到 HttpServlet 的 public service ↓ HttpServlet 内部: 把 req/res 强转成 HttpServletRequest/HttpServletResponse ↓ HttpServlet 内部: 调用 protected service(HttpServletRequest, HttpServletResponse) ↓ 你的代码: System.out.println("2. 这是 HTTP 专用的 service") ↓ super.service(req, resp) → 跳到 HttpServlet 的 protected service ↓ HttpServlet 内部: 判断 method 是 GET,调用 doGet()
第一个 service(ServletRequest, ServletResponse):
这是 Java 定下的"祖宗规矩",所有 Servlet 都必须有这个方法。但它太原始了,只提供最基本的"请求"和"响应"概念,连 GET 和 POST 都分不清。
就像"交通工具"这个抽象概念——知道它能跑,但不知道它是汽车还是飞机。
第二个 service(HttpServletRequest, HttpServletResponse):
这是 HttpServlet 专门给 HTTP 协议做的升级版。它内部做了这件事
java// 伪代码,大概逻辑
protected void service(HttpServletRequest req, HttpServletResponse resp) {
String method = req.getMethod(); // 看看是GET还是POST
if ("GET".equals(method)) {
doGet(req, resp); // 去调用 doGet
} else if ("POST".equals(method)) {
doPost(req, resp); // 去调用 doPost
}
// ... 还有 PUT、DELETE 等
}
就像"汽车"继承自"交通工具"——不仅知道能跑,还知道有方向盘、油门,能分轿车和卡车。
所以如果你重写第一个,Tomcat 直接走你的逻辑,跳过了 HttpServlet 的分发逻辑,doGet/doPost 永远不会被调用。
如果你重写第二个,HttpServlet 的分发逻辑被覆盖,你自己决定要不要调用 super.service()。
JDBC(Java Database Connectivity)是Java连接数据库的"翻译官"。Java代码说"我要查数据",JDBC把这句话翻译成数据库能听懂的语言(SQL),然后把结果翻译回Java能处理的数据。
💡 类比:JDBC就像你去国外餐厅吃饭时的翻译APP。你说中文(Java代码),APP翻译成英文(SQL)给服务员(数据库),再把服务员的回复翻译回中文(ResultSet)。
这里小迪是通过jar包安装的

我们这里直接使用maven
xml<!-- 在 pom.xml 里写这么一段 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
Maven 帮你做的:
1. 读取 pom.xml,发现你需要 mysql-connector-java 8.0.33 2. 自动去 Maven 中央仓库/阿里云镜像下载 jar 包 3. 存到你电脑的本地仓库:C:\Users\你的用户名\.m2\repository\mysql\... 4. 编译时自动把这个 jar 包加进项目
然后这里我们通过小皮面板的phpadmin来新建一个demo数据库并且创建一个user表,添加两行数据admin和guest

javapackage com.example.daimashenjilearn1;
import java.sql.*;
public class TestJDBC {
public static void main(String[] args) {
String url = "jdbc:mysql://localhost:3306/demo01?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true";
String user = "admin";
String password = "admin123";
try{
Class.forName("com.mysql.cj.jdbc.Driver");
Connection conn = DriverManager.getConnection(url,user,password);
System.out.println(conn);
System.out.println("连接成功");
Statement stmt = conn.createStatement();
** ResultSet rs = stmt.executeQuery("select * from users");**
while(rs.next()){
String username = rs.getString("username");
String pass = rs.getString("password");
System.out.println("用户"+username+"|"+"密码"+pass);
}
rs.close();
stmt.close();
conn.close();
}catch (Exception e) {
System.out.println("❌ 错误: " + e.getMessage());
e.printStackTrace();
}
}
}
提示
Statement 是 Java JDBC 中用于执行静态 SQL 语句的接口。它的作用就是把写好的 SQL 字符串直接发送给数据库执行。代码中:
Statement stmt = conn.createStatement(); // 创建一个 Statement 对象 String sql = "select * from users"; // 静态 SQL ResultSet rs = stmt.executeQuery(sql); // 执行并返回结果集
conn.createStatement() 从连接对象产生一个 Statement。executeQuery(sql) 用于执行 SELECT 查询。executeUpdate(sql) 用于 INSERT/UPDATE/DELETE。Statement 的特点:它只是把 SQL 当作普通字符串,不做任何参数处理。如果 SQL 中包含外部输入,比如用 + 拼起来,就会造成 SQL 注入。
注意,这里一定要提前开启sql服务
可以看到,这里成功运行了sql语句并获得了我们表中的数据

这是SQL注入审计最核心的知识点,必须理解:
| 对比项 | Statement | PreparedStatement |
|---|---|---|
| 写法 | 直接拼接SQL字符串 | 用?占位,预编译 |
| 安全性 | ❌ 危险! 用户输入直接拼进SQL | ✅ 安全! 用户输入作为参数传入 |
| 性能 | 每次执行都编译SQL | 预编译,重复执行更快 |
| 适用场景 | 静态SQL(无用户输入) | 动态SQL(有用户输入) |
java// 用户输入:name = admin' OR '1'='1
String name = request.getParameter("name");
String sql = "SELECT * FROM users WHERE username = '" + name + "'";
// 最终SQL:SELECT * FROM users WHERE username = 'admin' OR '1'='1'
// 结果:返回所有用户数据!
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);
实例代码
javapackage com.example.daimashenjilearn1;
import java.io.*;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import javax.servlet.ServletException;
import javax.servlet.http.*;
import javax.servlet.annotation.*;
@WebServlet("*.do")
public class HelloServlet extends HttpServlet {
@Override
public void init() throws ServletException{
System.out.println("Servlet出生");
getServletContext().setAttribute("appName", "我的应用");
}
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setContentType("text/html;charset=utf-8");
PrintWriter out = response.getWriter();
String name=(String)getServletContext().getAttribute("appName");
String path = getServletContext().getRealPath("/upload");
String username=request.getParameter("username");
String sql="select * from users where username='"+username+"'";
out.write("执行的 SQL:" + sql + "<br><hr>");
// 使用 Statement 执行
Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
try {
// 加载驱动(如果 web 项目已包含依赖,可省略 Class.forName)
Class.forName("com.mysql.cj.jdbc.Driver");
String url = "jdbc:mysql://localhost:3306/demo01?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true";
String user = "admin";
String password = "admin123";
conn = DriverManager.getConnection(url, user, password);
stmt = conn.createStatement();
rs = stmt.executeQuery(sql);
while (rs.next()) {
String dbUsername = rs.getString("username");
String dbPassword = rs.getString("password");
out.write("用户:" + dbUsername + " | 密码:" + dbPassword + "<br>");
}
} catch (Exception e) {
out.write("❌ 错误:" + e.getMessage());
e.printStackTrace();
} finally {
// 关闭资源(实际可以用 try-with-resources)
try { if (rs != null) rs.close(); } catch (Exception e) {}
try { if (stmt != null) stmt.close(); } catch (Exception e) {}
try { if (conn != null) conn.close(); } catch (Exception e) {}
}
out.write("应用名:"+name+"<br>");
out.write("真实路径"+path+"<br>");
}
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
@Override
public void destroy() {
System.out.println("Servlet去世");
}
}
所以这里需要你直接传参一个username,然后这里是直接拼接的


可以看到返回全部数据,剩下的就和前面说过的sql注入一样了

java// 用户输入:name = admin' OR '1'='1
String name = request.getParameter("name");
// ? 是占位符,数据库会把它当成"纯数据"处理,不会解析成SQL代码
String sql = "SELECT * FROM users WHERE username = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, name); // 把name作为第1个参数传入
ResultSet rs = pstmt.executeQuery();
// 最终SQL逻辑:查找username = "admin' OR '1'='1" 这个字符串
// 结果:找不到匹配用户(因为没人叫这个名),注入失败!
示例代码
这里需要更改这几个地方

如图,这样是不对的,我的错误就是在executeQuery() 的括号里传了 sql 参数,Java 就会去调用 Statement 父类的 executeQuery(String sql) 方法。这个方法会直接把你写的字符串(包含 ?)原封不动地发给数据库,完全绕过了预编译机制。


然后这里就是没有反应,因为他预编译了

只有严格匹配才可以

💡 原理大白话:
Statement是把SQL语句和用户输入混在一起发给数据库,数据库分不清哪是代码哪是数据。PreparedStatement是先把SQL语句编译好(带占位符),然后把用户输入单独作为数据传进去。数据库知道"这部分是代码,这部分是数据",不会把数据当代码执行。
Step 1:搜索危险函数
在IDEA中全局搜索(Ctrl+Shift+F): - executeQuery( - executeUpdate( - execute(
Step 2:追溯SQL来源
java// 找到这行:
ResultSet rs = stmt.executeQuery(sql);
// 按Ctrl+Alt+B(查看定义),找到sql变量在哪赋值的:
String sql = "SELECT * FROM users WHERE id = " + id; // ❌ 拼接 = 危险
String sql = "SELECT * FROM users WHERE id = ?"; // ✅ 占位符 = 安全
Step 3:追溯参数来源
java// 继续往上找,id从哪来的?
int id = Integer.parseInt(request.getParameter("id"));
// 如果来自request.getParameter()且没有过滤,就是注入点!
Step 4:确认漏洞
在浏览器输入 ?id=1 OR 1=1,看是否返回了额外数据。
| 对象 | 作用 | 审计关注点 |
|---|---|---|
| Connection | 数据库连接 | 连接字符串里是否有硬编码密码? |
| ResultSet | 查询结果集 | 结果是否直接输出到页面?(XSS) |
| ResultSetMetaData | 结果集元数据(列名、类型等) | 信息泄漏 |
连接字符串示例:
java// 审计时检查是否有硬编码密码
String url = "jdbc:mysql://localhost:3306/mydb";
String user = "root";
String password = "123456"; // ❌ 硬编码密码!
扩展
https://blog.csdn.net/m0_37761437/article/details/110468944
https://www.runoob.com/w3cnote/jdbc-use-guide.html
MyBatis是JDBC的"升级版"。JDBC写SQL太繁琐(连接、创建Statement、处理结果集都要手写),MyBatis帮你把这些重复工作自动化了。你只需要写SQL,剩下的交给它。
💡 类比:JDBC像手动写信(自己买纸、写字、贴邮票、去邮局),MyBatis像微信发消息(你只管打字,发送流程自动处理)。
1. 定义Mapper接口(写方法签名):
javapublic interface UserMapper {
// 根据ID查询用户
User findById(int id);
// 根据用户名查询用户
User findByName(String username);
// 根据用户名和密码查询(登录)
User login(String username, String password);
}
2. 写XML映射文件(写SQL):
xml<!-- UserMapper.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.UserMapper">
<!-- 根据ID查询 -->
<select id="findById" resultType="com.example.entity.User">
SELECT * FROM users WHERE id = #{id}
</select>
<!-- 根据用户名查询 -->
<select id="findByName" resultType="com.example.entity.User">
SELECT * FROM users WHERE username = #{username}
</select>
</mapper>
3. Java代码中调用:
java// 获取Mapper
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
// 调用方法,MyBatis自动执行SQL并封装结果
User user = mapper.findById(1); // 执行:SELECT * FROM users WHERE id = ?
这是MyBatis审计最重要的知识点,没有之一:
| 符号 | 原理 | 安全性 | 使用场景 |
|---|---|---|---|
| #{ } | 预编译,用?占位 | ✅ 安全 | 所有用户输入的地方 |
| ${ } | 字符串直接拼接 | ❌ 危险 | 表名、列名、排序字段等不能预编译的地方 |
xml<select id="findByName" resultType="User">
SELECT * FROM users WHERE username = #{username}
</select>
实际执行的SQL:
sqlSELECT * FROM users WHERE username = ? -- 预编译
-- 参数:'admin' OR '1'='1' 被当成纯字符串处理
xml<select id="findByNameUnsafe" resultType="User">
SELECT * FROM users WHERE username = '${username}'
</select>
实际执行的SQL:
sql-- 如果username = admin' OR '1'='1
SELECT * FROM users WHERE username = 'admin' OR '1'='1'
-- 注入成功!返回所有用户!
有些SQL语法是无法预编译的,数据库不支持用?占位:
xml<!-- 1. 动态表名(表名不能用?占位) -->
<select id="findByTable" resultType="User">
SELECT * FROM ${tableName}
</select>
<!-- 2. 动态列名(列名不能用?占位) -->
<select id="findByColumn" resultType="User">
SELECT ${columnName} FROM users
</select>
<!-- 3. 动态排序(ORDER BY后面不能用?占位) -->
<select id="findAllOrderBy" resultType="User">
SELECT * FROM users ORDER BY ${sortField} ${sortOrder}
</select>
<!-- 4. LIKE模糊查询(虽然可以用#,但需要拼接%) -->
<select id="findByLike" resultType="User">
SELECT * FROM users WHERE username LIKE '%${keyword}%'
</select>
⚠️ 审计重点:看到
${ }就要高度警惕!追溯参数来源,看是否用户可控。如果可控且没有白名单过滤,就是SQL注入。
如果业务上确实需要 ${ },必须做白名单校验:
java// 安全的动态排序
public List<User> findAllOrderBy(String sortField, String sortOrder) {
// 白名单:只允许指定的列名
List<String> allowedFields = Arrays.asList("id", "username", "create_time");
if (!allowedFields.contains(sortField)) {
throw new IllegalArgumentException("非法排序字段!");
}
// 白名单:只允许ASC或DESC
if (!"ASC".equalsIgnoreCase(sortOrder) && !"DESC".equalsIgnoreCase(sortOrder)) {
throw new IllegalArgumentException("非法排序方式!");
}
return userMapper.findAllOrderBy(sortField, sortOrder);
}
❌ 错误做法:黑名单过滤(如过滤
or、and、select),攻击者总能绕过。 ✅ 正确做法:白名单校验(只允许指定的值),攻击者无法绕过。
现代SpringBoot项目常用注解,不用XML:
java@Mapper
public interface UserMapper {
// ✅ 安全
@Select("SELECT * FROM users WHERE id = #{id}")
User findById(@Param("id") int id);
// ❌ 危险!
@Select("SELECT * FROM users WHERE username = '${username}'")
User findByNameUnsafe(@Param("username") String username);
// ✅ 安全的LIKE写法(用# + concat)
@Select("SELECT * FROM users WHERE username LIKE CONCAT('%', #{keyword}, '%')")
List<User> findByLike(@Param("keyword") String keyword);
}
审计MyBatis项目时,按这个清单检查:
${ ,找到所有使用 ${ } 的地方${ } 中的参数来源,是否来自用户输入(request.getParameter、@RequestParam等)ORDER BY、LIMIT、GROUP BY 后的参数LIKE 语句是否错误使用了 ${ }📎 深入学习:
Filter是Java Web的"安检门"。请求进入Servlet之前,要先经过Filter检查;Servlet处理完后,返回响应时也要经过Filter。你可以在Filter里做身份验证、编码设置、日志记录、XSS过滤等。
💡 类比:Filter就像机场的安检。你(请求)要上飞机(Servlet)前,必须先过安检(Filter),检查行李、验证身份。下飞机时也可能要再过一次检查。
Filter和Servlet一样,也有生命周期:
┌─────────────┐ ┌─────────────────────────────┐ ┌─────────────┐ │ init() │ → │ doFilter(req, resp, chain) │ → │ destroy() │ │ 初始化 │ │ 每次请求都执行 │ │ 销毁 │ │ 只执行1次 │ │ 可以拦截/放行请求 │ │ 只执行1次 │ └─────────────┘ └─────────────────────────────┘ └─────────────┘
这里先创建一个规范的写法,

这里导入要导入对

然后使用Alt+Enter导入三个方法

java@WebFilter("/*") // 过滤所有请求
public class EncodingFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("Filter初始化");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
// 1. 请求进来时的处理(在Servlet之前执行)
request.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=UTF-8");
System.out.println("请求经过了Filter...");
// 2. 放行!让请求继续往下走(如果不写这行,请求就被拦截了)
chain.doFilter(request, response);
// 3. 响应返回时的处理(在Servlet之后执行)
System.out.println("响应经过了Filter...");
}
@Override
public void destroy() {
System.out.println("Filter销毁");
}
}
代码实例 HelloServlet
javapackage com.example.diamshenjilearn2;
import java.io.*;
import javax.servlet.ServletException; // 改这里
import javax.servlet.http.*; // 改这里
import javax.servlet.annotation.*; // 改这里
@WebServlet("/fuck")
public class HelloServlet extends HttpServlet {
private String message;
public void init() {
message = "Hello World!";
System.out.println("出生");
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html;charset=UTF-8");
String woca=req.getParameter("woca");
PrintWriter out = resp.getWriter();
out.println("woca: "+woca);
}
public void destroy() {
}
}
FuckFliter
javapackage com.example.diamshenjilearn2.fliter;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
import java.io.PrintWriter;
@WebFilter("/fuck")
public class FuckFliter implements Filter{
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("过滤器开始工作");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
servletResponse.setContentType("text/html;charset=UTF-8");
System.out.println("请求进入过滤器");
String woca = servletRequest.getParameter("woca");
if(woca.equals("admin")){
PrintWriter out = servletResponse.getWriter();
out.println("禁止访问");
System.out.println("拦截请求:参数为 admin");
}
else{
System.out.println("放行请求,参数:" + woca);
filterChain.doFilter(servletRequest, servletResponse);
}
}
@Override
public void destroy() {
System.out.println("过滤器结束工作");
}
}



执行顺序:
浏览器请求 ↓ Filter.doFilter() 前半部分 ↓ chain.doFilter() 放行 ↓ Servlet处理请求 ↓ Filter.doFilter() 后半部分 ↓ 浏览器收到响应
一个项目可以有多个Filter,它们按配置顺序执行,形成过滤器链:
java@WebFilter("/*") // 第1个:编码过滤器
public class EncodingFilter implements Filter { ... }
@WebFilter("/*") // 第2个:登录验证过滤器
public class AuthFilter implements Filter { ... }
@WebFilter("/*") // 第3个:XSS过滤过滤器
public class XssFilter implements Filter { ... }
执行顺序(web.xml中按配置顺序,注解中按类名字母顺序):
请求 → EncodingFilter → AuthFilter → XssFilter → Servlet 响应 ← EncodingFilter ← AuthFilter ← XssFilter ← Servlet
💡 形象理解:像洋葱一样,一层层剥进去,再一层层剥出来。
这是审计时最常见的Filter,很多权限绕过漏洞都是因为Filter写错了:
java@WebFilter("/*")
public class LoginFilter implements Filter {
// 白名单:这些URL不需要登录就能访问
private static final List<String> WHITE_LIST = Arrays.asList(
"/login", "/register", "/css/", "/js/", "/images/"
);
@Override
public void doFilter(ServletRequest req, ServletResponse resp,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) resp;
String uri = request.getRequestURI();
// 1. 白名单放行
for (String white : WHITE_LIST) {
if (uri.contains(white)) { // ❌ 危险!后面讲为什么
chain.doFilter(req, resp);
return;
}
}
// 2. 检查登录状态
HttpSession session = request.getSession();
Object user = session.getAttribute("user");
if (user == null) {
// 未登录,重定向到登录页
response.sendRedirect("/login");
return;
}
// 3. 已登录,放行
chain.doFilter(req, resp);
}
}
然后这里是示例代码
LoginServlet.java
javapackage com.example.diamshenjilearn2;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
/**
* 登录处理Servlet。
* 接收 GET 请求(亦可处理 POST,但本示例只重写了 doGet),
* 从请求参数中获取 username 和 password,硬编码校验为 admin/admin。
* 校验成功则创建Session并写入 user 属性,返回成功提示;
* 失败则返回错误提示。
*/
@WebServlet("/login") // 映射到 /login 路径
public class LoginServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// 1. 设置响应内容类型和字符编码,防止中文乱码
resp.setContentType("text/html;charset=UTF-8");
// 2. 获取请求参数
String username = req.getParameter("username");
String password = req.getParameter("password");
// 3. 硬编码校验(仅为演示,实际生产应查询数据库)
if ("admin".equals(username) && "admin".equals(password)) {
// 3a. 登录成功:创建Session并保存用户信息
HttpSession session = req.getSession(); // 若不存在则自动创建
session.setAttribute("user", username); // 将用户名存入Session
// 3b. 返回成功响应,并提供访问受保护资源(/fuck)的链接
PrintWriter out = resp.getWriter();
out.println("<h1>登录成狗,session写入,你可以访问<a href='fuck'>/fuck</a> 了。 " + username + "</h1>");
} else {
// 4. 登录失败:返回错误信息
PrintWriter out = resp.getWriter();
out.println("<h1>账号密码错误,请使用admin,admin</h1>");
}
}
}
LoginFilter.java
javapackage com.example.diamshenjilearn2.fliter;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
/**
* 登录鉴权过滤器。
* 拦截所有请求("/*"),对白名单内的路径直接放行,其他路径检查用户是否已登录。
* 若未登录则重定向到登录页(/login)。
*
* 注意:此过滤器使用 contains 判断白名单,存在路径遍历绕过风险(如 /login/../fuck),
* 仅为演示用途,生产环境应改为精确匹配或前缀匹配。
*/
@WebFilter("/*") // 拦截所有请求
public class LoginFilter implements Filter {
/**
* 白名单列表:这些路径开头的请求将跳过登录校验,直接放行。
* 注意:当前使用 contains 匹配,存在安全风险。
*/
public static final List<String> WHITE_LIST = Arrays.asList(
"/login", // 登录接口本身需要公开
"/css", // 静态样式资源
"/js/" // 静态脚本资源
);
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {
// 1. 转型为HTTP专用请求/响应对象,以便获取URI、Session、重定向等功能
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
// 2. 获取当前请求的URI(如:/demo/login 或 /demo/fuck)
String uri = request.getRequestURI();
System.out.println("Filter拦截当前请求api:" + uri);
// 3. 判断当前URI是否命中白名单(contains匹配,有风险)
boolean isWhite = false;
for (String white : WHITE_LIST) {
if (uri.contains(white)) { // 只要URI中包含白名单字符串即放行(漏洞点)
isWhite = true;
System.out.println("filter匹配白名单" + white + "直接放行");
break;
}
}
// 4. 若命中白名单,直接放行(不检查登录状态)
if (isWhite) {
filterChain.doFilter(request, response);
return;
}
// 5. 检查Session中是否有用户信息(登录标志)
// 注意:getSession(false) 不会主动创建Session,若当前无Session则返回null。
// 但此处用了 getSession() 会强制创建,可能造成不必要的Session开销。
HttpSession session = request.getSession(); // 建议改为 request.getSession(false)
Object user = (session != null) ? session.getAttribute("user") : null;
// 6. 若未登录(user == null),则重定向到登录页面
if (user == null) {
System.out.println("filter未登录,拦截" + uri);
// 注意:重定向地址需要写绝对路径或包含上下文路径
response.sendRedirect("/diamshenjilearn2_war_exploded/login");
return; // 必须return,确保后续代码不执行
}
// 7. 已登录,放行请求
System.out.println("filter已登录用户" + user + "放行");
filterChain.doFilter(request, response);
}
}
这里访问fuck,也会直接跳转到login


我们输入账密登录

这次就可以了

让我们清除缓存

java// 危险代码
if (uri.contains("/login")) { // 包含/login就放行
chain.doFilter(req, resp);
return;
}
// 绕过方式:访问 /xxx/login.jsp 或 /login.html/anything
// 因为contains只要包含子串就匹配!
修复:
java// ✅ 正确做法:精确匹配或前缀匹配
if (uri.equals("/login") || uri.startsWith("/login")) {
chain.doFilter(req, resp);
return;
}
java// 危险:先执行chain.doFilter(),后做鉴权!
chain.doFilter(req, resp); // 已经放行了!
// 检查登录...(这里已经晚了,请求已经到Servlet了)
xml<!-- 只过滤了 /user/*,但后台管理接口是 /admin/*,没保护到! -->
<filter-mapping>
<filter-name>AuthFilter</filter-name>
<url-pattern>/user/*</url-pattern>
</filter-mapping>
web.xml 或搜索 @WebFilterurl-pattern 是什么?有没有遗漏的接口?chain.doFilter() 的位置对不对?return 还是继续执行?📎 深入学习:
Listener是Java Web的"监控摄像头"。它监听Web应用中的各种事件(应用启动、会话创建、请求到来等),事件发生时自动执行你写的代码。
💡 类比:Listener就像你家的智能门铃。有人按门铃(事件发生),门铃自动响(执行代码),不需要你手动去检查。
| 类型 | 监听对象 | 常用场景 |
|---|---|---|
| ServletContextListener | 应用生命周期 | 应用启动时加载配置、初始化数据库连接池 |
| HttpSessionListener | 会话生命周期 | 统计在线人数、记录用户登录时长 |
| ServletRequestListener | 请求生命周期 | 记录请求日志、统计API访问次数 |
java@WebListener // 声明这是一个监听器
public class AppStartupListener implements ServletContextListener {
// 应用启动时执行(Tomcat启动时)
@Override
public void contextInitialized(ServletContextEvent sce) {
ServletContext context = sce.getServletContext();
System.out.println("🚀 应用启动了!");
// 常见操作1:加载全局配置
String appName = context.getInitParameter("appName");
context.setAttribute("APP_NAME", appName);
// 常见操作2:初始化数据库连接池
// DataSource ds = createDataSource();
// context.setAttribute("DATA_SOURCE", ds);
// 常见操作3:创建全局缓存
Map<String, Object> cache = new ConcurrentHashMap<>();
context.setAttribute("CACHE", cache);
}
// 应用关闭时执行(Tomcat关闭时)
@Override
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("🛑 应用关闭了...");
// 常见操作:释放资源、关闭连接池
}
}
代码实例
Fucklistener.java
javapackage com.example.diamshenjilearn2.listener; // 包名,可改,需与文件夹结构一致
import javax.servlet.*; // 导入 Servlet API(包含 ServletContext、ServletContextEvent、ServletContextListener 等)
import javax.servlet.annotation.WebListener; // 导入 @WebListener 注解,用于自动注册监听器
import java.util.Map; // 导入 Map 接口
import java.util.concurrent.ConcurrentHashMap; // 导入线程安全的 ConcurrentHashMap
// 下面两个导入实际未使用,可以删除:
// import java.net.http.WebSocket; // 未使用,且包名不对(WebSocket 应该在 javax.websocket 下),建议删除
// import java.util.HashMap; // 未使用,建议删除
@WebListener // 声明这是一个监听器,Tomcat 启动时自动注册,不可改
public class FuckListener implements ServletContextListener { // 类名可改,必须实现 ServletContextListener 接口
/**
* 应用启动时自动调用一次(Tomcat 启动时触发)。
* 方法名、参数类型固定,不能修改。
*/
@Override
public void contextInitialized(ServletContextEvent sce) {
// 获取全局上下文对象(整个 Web 应用唯一)
ServletContext context = sce.getServletContext(); // 变量名 context 可改,方法名固定
System.out.println("Listener 启动了"); // 控制台日志,内容可改
// 从 web.xml 的 <context-param> 中读取名为 "appName" 的参数值
// 这里的 "appName" 必须与 web.xml 中 <param-name> 完全一致
String appName = context.getInitParameter("appName"); // 变量名 appName 可改
// 将读取到的值存入全局上下文,键为 "APP_NAME"
// 键名 "APP_NAME" 可改,但后续读取时要用同样的键
context.setAttribute("APP_NAME", appName);
// 创建一个线程安全的 Map 作为全局缓存,泛型为 <String, Object>
Map<String, Object> cache = new ConcurrentHashMap<>(); // 变量名 cache 可改,泛型可改
// 将缓存存入全局上下文,键为 "CACHE"
// 键名 "CACHE" 可改,后续读取时对应即可
context.setAttribute("CACHE", cache);
System.out.println("全局缓存已创建"); // 控制台日志,内容可改
}
/**
* 应用关闭时自动调用一次(Tomcat 关闭时触发)。
* 方法名、参数类型固定,不能修改。
*/
@Override
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("Listener 断掉了"); // 控制台日志,内容可改
// 实际项目中可在此释放资源,如关闭数据库连接池、停止后台线程等
}
}
为了展示效果可以在前面的LoginServlet.java中加上几行

web.xml配置方式:
xml<listener>
<listener-class>com.example.listener.AppStartupListener</listener-class>
</listener>
<!-- 初始化参数 -->
<context-param>
<param-name>appName</param-name>
<param-value>我的应用</param-value>
</context-param>
效果

java@WebListener
public class OnlineUserListener implements HttpSessionListener {
// 全局计数器(所有用户共享)
private static AtomicInteger onlineCount = new AtomicInteger(0);
// 会话创建时(用户打开浏览器访问网站)
@Override
public void sessionCreated(HttpSessionEvent se) {
int count = onlineCount.incrementAndGet();
System.out.println("👤 有人上线了!当前在线:" + count + "人");
// 把在线人数存到全局上下文
se.getSession().getServletContext()
.setAttribute("onlineCount", count);
}
// 会话销毁时(用户关闭浏览器或超时)
@Override
public void sessionDestroyed(HttpSessionEvent se) {
int count = onlineCount.decrementAndGet();
System.out.println("👋 有人下线了!当前在线:" + count + "人");
se.getSession().getServletContext()
.setAttribute("onlineCount", count);
}
}
代码实例
onlineListener.java
javapackage com.example.diamshenjilearn2.listener;
import javax.servlet.annotation.WebListener; // 声明监听器,Tomcat 启动时自动注册
import javax.servlet.http.HttpSessionListener; // 实现 HttpSessionListener 接口,监听 Session 创建/销毁
import javax.servlet.http.*; // 包含 HttpSessionEvent 等类
import java.util.concurrent.atomic.AtomicInteger; // 线程安全的计数器,用于统计在线人数
@WebListener // 标记为监听器,无需 web.xml 配置
public class OnlineListener implements HttpSessionListener {
// 全局静态计数器,所有用户共享。AtomicInteger 保证多线程并发时计数准确。
private static final AtomicInteger onlineCount = new AtomicInteger(0);
/**
* 当一个新的 Session 被创建时自动调用。
* 触发时机:用户第一次调用 request.getSession() 时(例如登录成功写入 Session)。
* 方法名、参数类型固定,不能修改。
*/
@Override
public void sessionCreated(HttpSessionEvent event) {
int count = onlineCount.incrementAndGet(); // 在线人数 +1,返回增加后的值
System.out.println("当前在线人数" + count); // 控制台打印,便于观察
// 将最新在线人数存入全局 ServletContext,键为 "onlineCount"
// 这样所有 Servlet 都能通过 getServletContext().getAttribute("onlineCount") 读取
event.getSession().getServletContext().setAttribute("onlineCount", count);
}
/**
* 当 Session 被销毁时自动调用。
* 触发时机:Session 超时(默认30分钟)、调用 session.invalidate()、Tomcat 正常关闭等。
* 方法名、参数类型固定,不能修改。
*/
@Override
public void sessionDestroyed(HttpSessionEvent event) {
int count = onlineCount.decrementAndGet(); // 在线人数 -1,返回减少后的值
System.out.println("有fucker下线了,当前人数" + count);
// 同样将最新人数更新到全局 ServletContext,保证其他用户读取到的是最新值
event.getSession().getServletContext().setAttribute("onlineCount", count);
}
}
可以在HelloServlet中改一点

然后,登录后就可以触发了

为什么呢,因为在LoginServlet中会有创建session,所以
登录成功 → 执行 req.getSession() → 创建 Session → 触发 sessionCreated → 在线人数 +1。
登录失败 → 直接返回错误,没有调用 getSession() → 不创建 Session → 不触发。

java@WebListener
public class RequestLogListener implements ServletRequestListener {
// 请求开始时
@Override
public void requestInitialized(ServletRequestEvent sre) {
HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
String uri = request.getRequestURI();
String ip = request.getRemoteAddr();
String method = request.getMethod();
// 记录请求开始时间
request.setAttribute("startTime", System.currentTimeMillis());
System.out.println("📨 [" + method + "] " + uri + " from " + ip);
}
// 请求结束时
@Override
public void requestDestroyed(ServletRequestEvent sre) {
HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
Long startTime = (Long) request.getAttribute("startTime");
long duration = System.currentTimeMillis() - startTime;
System.out.println("✅ 请求处理完成,耗时:" + duration + "ms");
}
}
代码实例
RequestLogListener.java
java
package com.example.diamshenjilearn2.listener;
import javax.servlet.ServletRequestListener; // 监听请求生命周期的接口
import javax.servlet.annotation.WebListener; // 自动注册监听器
import javax.servlet.*; // ServletRequestEvent、ServletRequest 等
import javax.servlet.http.HttpServletRequest; // 转型后才能获取 URI、IP、方法
@WebListener // Tomcat 启动时自动扫描并注册
public class RequestLogListener implements ServletRequestListener {
/**
* 请求开始时触发(任何请求,包括静态资源、404、被 Filter 拦截的请求)。
* 执行时机:在 Filter 之前。
*/
@Override
public void requestInitialized(ServletRequestEvent sre) {
// 转成 HttpServletRequest,才能调用 HTTP 专有方法
HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
String uri = request.getRequestURI(); // 请求路径,如 /项目名/fuck
String ip = request.getRemoteAddr(); // 客户端 IP(可能为 IPv4 或 IPv6)
String method = request.getMethod(); // GET / POST / PUT 等
// 把“请求开始时间”存入当前 request 作用域
// 每个请求独立,请求结束自动销毁
request.setAttribute("startTime", System.currentTimeMillis());
// 打印日志:方法、URI、来源 IP
System.out.println("[" + method + "]" + uri + "from" + ip);
}
/**
* 请求结束时触发(响应已完成,即将返回给浏览器)。
* 执行时机:在 Filter 的“响应返回”逻辑之后。
*/
@Override
public void requestDestroyed(ServletRequestEvent sre) {
HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
// 取出开始时存的时间
Object time = request.getAttribute("startTime");
// 判空 + 类型校验,防止 requestInitialized 抛异常导致 NPE
if (time instanceof Long) {
long duration = System.currentTimeMillis() - (Long) time;
System.out.println("请求处理完成耗时:" + duration + "ms");
}
}
}

Listener本身不直接产生漏洞,但审计时要注意:
| 关注点 | 说明 |
|---|---|
| 初始化敏感操作 | contextInitialized 中是否加载了硬编码密码、密钥? |
| 全局属性安全 | setAttribute 存入ServletContext的数据是否敏感? |
| 监听器顺序 | 多个Listener的执行顺序是否影响安全逻辑? |
| 内存泄漏 | contextDestroyed 是否正确释放了资源? |
审计示例:
java// 在Listener中发现硬编码密码!
public void contextInitialized(ServletContextEvent sce) {
String dbPassword = "Admin@123456"; // ❌ 硬编码!
sce.getServletContext().setAttribute("DB_PASSWORD", dbPassword);
}
📎 深入学习:
┌─────────────────────────────────────────────────────────────┐ │ 浏览器 │ └─────────────────────────────────────────────────────────────┘ ↓ HTTP请求 ┌─────────────────────────────────────────────────────────────┐ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ Listener │ │ Filter │ │ Filter │ │ │ │(请求监听) │ │(编码设置) │ │(登录鉴权) │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ 不通过 → 拦截返回 │ │ 通过 ↓ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Servlet │ │ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │ │ │ doGet() │ │doPost() │ │service()│ │ │ │ │ └─────────┘ └─────────┘ └─────────┘ │ │ │ │ ↓ │ │ │ │ 调用业务逻辑 → MyBatis → JDBC → 数据库 │ │ │ │ ↓ │ │ │ │ 返回响应 │ │ │ └─────────────────────────────────────────────────────┘ │ │ ↓ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ Filter │ │ Filter │ │ Listener │ │ │ │(XSS过滤) │ │(日志记录) │ │(请求完成) │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ │ └─────────────────────────────────────────────────────────────┘ ↓ HTTP响应 ┌─────────────────────────────────────────────────────────────┐ │ 浏览器 │ └─────────────────────────────────────────────────────────────┘
不要只看不练:把上面的每个代码示例都在IDEA里敲一遍,跑起来,打断点跟一遍。
画流程图:学完这章后,自己手动画一张"请求从浏览器到数据库再返回"的完整流程图,能画出来才算真懂了。
审计思维培养:看任何代码时,问自己三个问题:
getParameter、getHeader等)下一步:当你能熟练画出上面的流程图后,就可以进入**第34天(反射机制)**的学习了。反射是Java反序列化漏洞的基础,也是Java安全中最"魔法"的部分。
附:本章核心概念速查表
概念 一句话解释 审计关键词 Servlet 处理HTTP请求的工人 @WebServlet、doGet、doPostJDBC Java连接数据库的翻译官 Statement、PreparedStatement、executeQueryMyBatis JDBC的自动化升级版 #{ }、${ }、@Select、Mapper.xmlFilter 请求的安检门 @WebFilter、doFilter、chain.doFilter()Listener Web应用的监控摄像头 @WebListener、ServletContextListener