2026-09-11
java代码审计
00
请注意,本文编写于 64 天前,最后修改于 64 天前,其中某些信息可能已经过时。

目录

一、Servlet生命周期与路由
1.1 什么是Servlet?(一句话理解)
1.2 Servlet的生命周期(三个阶段)
1.3 路由是怎么匹配的?(两种配置方式)
方式一:注解配置(现代项目常用,简单直观)
方式二:web.xml配置(老项目常见,审计时经常遇到)
1.4 HttpServletRequest 和 HttpServletResponse
1.5 ServletContext:全局"储物柜"
1.6 原理速览(简单版)
两个 service 的区别
二、JDBC与SQL执行流程
2.1 什么是JDBC?(一句话理解)
2.2 JDBC执行SQL的完整流程
2.3 Statement vs PreparedStatement(安全分水岭)
❌ 危险写法(SQL注入漏洞)
✅ 安全写法(预编译)
2.4 审计时怎么找SQL注入?
2.5 JDBC中的其他重要对象
三、MyBatis #{与$的区别
3.1 什么是MyBatis?(一句话理解)
3.2 MyBatis的基本用法
3.3 #{ } 与 ${ } 的区别(审计核心!)
✅ #{ } 安全示例
❌ ${ } 危险示例
3.4 为什么有些地方必须用 ${ }?
3.5 安全的 ${ } 用法(白名单校验)
3.6 MyBatis注解方式(SpringBoot常用)
3.7 MyBatis审计Checklist
四、Filter过滤器链
4.1 什么是Filter?(一句话理解)
4.2 Filter的生命周期
4.3 Filter的基本写法
4.4 多个Filter组成"过滤器链"
4.5 登录鉴权Filter(审计重点)
4.6 Filter中的常见漏洞(审计必看)
❌ 漏洞1:白名单绕过
❌ 漏洞2:过滤逻辑顺序错误
❌ 漏洞3:Filter映射范围不对
4.7 审计Filter的步骤
五、Listener监听器
5.1 什么是Listener?(一句话理解)
5.2 Listener的三种类型
5.3 ServletContextListener
5.4 HttpSessionListener(统计在线人数)
5.5 ServletRequestListener(请求日志)
5.6 Listener在审计中的关注点
六、总结:一张图看懂Java Web基础
七、学习建议与下一步

一个请求是怎么被Java后端处理的,sql注入代码,filter和listener机制


一、Servlet生命周期与路由

1.1 什么是Servlet?(一句话理解)

Servlet是Java里专门用来处理HTTP请求的"小工人"。浏览器发来一个请求,Tomcat(服务器)找到对应的Servlet,让它干活(处理请求),然后把结果返回给浏览器。

💡 类比:Servlet就像餐厅里的服务员。顾客(浏览器)点菜(发请求),服务员(Servlet)把菜单交给后厨(业务逻辑),再把做好的菜(响应)端给顾客。

1.2 Servlet的生命周期(三个阶段)

https://blog.csdn.net/qq_52173163/article/details/121110753

一个Servlet从生到死,只会经历三个阶段,非常重要,审计时经常利用生命周期做手脚:

图片.png

┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ 出生 │ → │ 干活 │ → │ 死亡 │ │ init() │ │ service() │ │ destroy() │ │ 初始化 │ │ 处理请求 │ │ 销毁 │ │ 只执行1次 │ │ 每次请求都执行│ │ 只执行1次 │ └─────────────┘ └─────────────┘ └─────────────┘

代码示例:

java
package 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重新加载项目

图片.png

这里我们可以看到servlet出生 图片.png

图片.png

这里应该是servlet去世,编码问题,

Run → Edit Configurations → 你的 Tomcat 配置 → Server 标签页,找到 VM options(在 JRE 选择框下面,可能需要展开),填入:-Dfile.encoding=UTF-8 图片.png 访问方式:浏览器输入 http://localhost:8080/项目名/hello?name=小白


1.3 路由是怎么匹配的?(两种配置方式)

路由就是"URL怎么找到对应的Servlet"。有两种配置方式:

方式一:注解配置(现代项目常用,简单直观)

java
@WebServlet("/login") // 精确匹配 @WebServlet("/user/*") // 通配匹配,/user/add、/user/delete都能进来 @WebServlet("*.do") // 后缀匹配,所有以.do结尾的请求

图片.png

图片.png

方式二:web.xml配置(老项目常见,审计时经常遇到)

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

图片.png

图片.png

🔍 审计技巧:审计一个老项目时,先看 web.xml,里面列出了所有Servlet和对应的URL,这是你梳理攻击面的第一步。


1.4 HttpServletRequest 和 HttpServletResponse

这是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
java
package 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去世"); } }

图片.png

提示

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执行、命令执行、文件操作)。


1.5 ServletContext:全局"储物柜"

ServletContext 是整个Web应用的全局共享空间,所有Servlet都能访问它。

java
// 存数据(全局配置、共享对象) getServletContext().setAttribute("appName", "我的应用"); // 取数据 String appName = (String) getServletContext().getAttribute("appName"); // 获取真实路径(文件上传审计常用) String realPath = getServletContext().getRealPath("/upload"); // 结果:C:\Tomcat\webapps\项目名\upload
java
package 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去世"); } }

图片.png

🔍 审计注意:ServletContext 里存的东西所有用户共享,如果存了敏感信息,可能被其他用户读取。


1.6 原理速览(简单版)

当浏览器访问 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 的区别

图片.png

第一个第二个
来自哪Servlet 接口(老祖宗)HttpServlet 类(子类)
参数ServletRequest / ServletResponseHttpServletRequest / HttpServletResponse
权限public(对外公开的)protected(只给子类用的)
你平时用哪个❌ 几乎不用✅ 重写这个

执行顺序为

text
Tomcat: 调用 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与SQL执行流程

2.1 什么是JDBC?(一句话理解)

JDBC(Java Database Connectivity)是Java连接数据库的"翻译官"。Java代码说"我要查数据",JDBC把这句话翻译成数据库能听懂的语言(SQL),然后把结果翻译回Java能处理的数据。

💡 类比:JDBC就像你去国外餐厅吃饭时的翻译APP。你说中文(Java代码),APP翻译成英文(SQL)给服务员(数据库),再把服务员的回复翻译回中文(ResultSet)。

这里小迪是通过jar包安装的

图片.png

我们这里直接使用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 图片.png

2.2 JDBC执行SQL的完整流程

java
package 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服务

图片.png 可以看到,这里成功运行了sql语句并获得了我们表中的数据

图片.png

2.3 Statement vs PreparedStatement(安全分水岭)

这是SQL注入审计最核心的知识点,必须理解:

对比项StatementPreparedStatement
写法直接拼接SQL字符串用?占位,预编译
安全性❌ 危险! 用户输入直接拼进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);

实例代码

java
package 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,然后这里是直接拼接的 图片.png

图片.png

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

图片.png

✅ 安全写法(预编译)

JDBC预编译-CSDN

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" 这个字符串 // 结果:找不到匹配用户(因为没人叫这个名),注入失败!

示例代码

这里需要更改这几个地方

图片.png

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

图片.png

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

只有严格匹配才可以

图片.png

💡 原理大白话:

  • Statement 是把SQL语句和用户输入混在一起发给数据库,数据库分不清哪是代码哪是数据。
  • PreparedStatement 是先把SQL语句编译好(带占位符),然后把用户输入单独作为数据传进去。数据库知道"这部分是代码,这部分是数据",不会把数据当代码执行。

2.4 审计时怎么找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,看是否返回了额外数据。


2.5 JDBC中的其他重要对象

对象作用审计关注点
Connection数据库连接连接字符串里是否有硬编码密码?
ResultSet查询结果集结果是否直接输出到页面?(XSS)
ResultSetMetaData结果集元数据(列名、类型等)信息泄漏

连接字符串示例:

java
// 审计时检查是否有硬编码密码 String url = "jdbc:mysql://localhost:3306/mydb"; String user = "root"; String password = "123456"; // ❌ 硬编码密码!

扩展

https://www.jianshu.com/p/ed1a59750127

https://blog.csdn.net/m0_37761437/article/details/110468944

https://www.runoob.com/w3cnote/jdbc-use-guide.html


三、MyBatis #{与$的区别

3.1 什么是MyBatis?(一句话理解)

MyBatis是JDBC的"升级版"。JDBC写SQL太繁琐(连接、创建Statement、处理结果集都要手写),MyBatis帮你把这些重复工作自动化了。你只需要写SQL,剩下的交给它。

💡 类比:JDBC像手动写信(自己买纸、写字、贴邮票、去邮局),MyBatis像微信发消息(你只管打字,发送流程自动处理)。

3.2 MyBatis的基本用法

1. 定义Mapper接口(写方法签名):

java
public 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 = ?

3.3 #{ } 与 ${ } 的区别(审计核心!)

这是MyBatis审计最重要的知识点,没有之一:

符号原理安全性使用场景
#{ }预编译,用?占位✅ 安全所有用户输入的地方
${ }字符串直接拼接❌ 危险表名、列名、排序字段等不能预编译的地方

✅ #{ } 安全示例

xml
<select id="findByName" resultType="User"> SELECT * FROM users WHERE username = #{username} </select>

实际执行的SQL:

sql
SELECT * 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' -- 注入成功!返回所有用户!

3.4 为什么有些地方必须用 ${ }?

有些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注入。


3.5 安全的 ${ } 用法(白名单校验)

如果业务上确实需要 ${ },必须做白名单校验:

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),攻击者总能绕过。 ✅ 正确做法:白名单校验(只允许指定的值),攻击者无法绕过。


3.6 MyBatis注解方式(SpringBoot常用)

现代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); }

3.7 MyBatis审计Checklist

审计MyBatis项目时,按这个清单检查:

  • 全局搜索 ${ ,找到所有使用 ${ } 的地方
  • 追溯 ${ } 中的参数来源,是否来自用户输入(request.getParameter、@RequestParam等)
  • 如果参数可控,检查是否有白名单校验
  • 检查 ORDER BY、LIMIT、GROUP BY 后的参数
  • 检查动态表名/列名场景
  • 检查 LIKE 语句是否错误使用了 ${ }

📎 深入学习:


四、Filter过滤器链

4.1 什么是Filter?(一句话理解)

Filter是Java Web的"安检门"。请求进入Servlet之前,要先经过Filter检查;Servlet处理完后,返回响应时也要经过Filter。你可以在Filter里做身份验证、编码设置、日志记录、XSS过滤等。

💡 类比:Filter就像机场的安检。你(请求)要上飞机(Servlet)前,必须先过安检(Filter),检查行李、验证身份。下飞机时也可能要再过一次检查。

4.2 Filter的生命周期

Filter和Servlet一样,也有生命周期:

┌─────────────┐ ┌─────────────────────────────┐ ┌─────────────┐ │ init() │ → │ doFilter(req, resp, chain) │ → │ destroy() │ │ 初始化 │ │ 每次请求都执行 │ │ 销毁 │ │ 只执行1次 │ │ 可以拦截/放行请求 │ │ 只执行1次 │ └─────────────┘ └─────────────────────────────┘ └─────────────┘

4.3 Filter的基本写法

这里先创建一个规范的写法, 图片.png

这里导入要导入对 图片.png

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

图片.png

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

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

java
package 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("过滤器结束工作"); } }

图片.png

图片.png

图片.png

执行顺序:

浏览器请求 ↓ Filter.doFilter() 前半部分 ↓ chain.doFilter() 放行 ↓ Servlet处理请求 ↓ Filter.doFilter() 后半部分 ↓ 浏览器收到响应

4.4 多个Filter组成"过滤器链"

一个项目可以有多个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

💡 形象理解:像洋葱一样,一层层剥进去,再一层层剥出来。


4.5 登录鉴权Filter(审计重点)

这是审计时最常见的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

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

java
package 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 图片.png

图片.png

我们输入账密登录 图片.png

这次就可以了 图片.png

让我们清除缓存

图片.png


4.6 Filter中的常见漏洞(审计必看)

❌ 漏洞1:白名单绕过

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

❌ 漏洞2:过滤逻辑顺序错误

java
// 危险:先执行chain.doFilter(),后做鉴权! chain.doFilter(req, resp); // 已经放行了! // 检查登录...(这里已经晚了,请求已经到Servlet了)

❌ 漏洞3:Filter映射范围不对

xml
<!-- 只过滤了 /user/*,但后台管理接口是 /admin/*,没保护到! --> <filter-mapping> <filter-name>AuthFilter</filter-name> <url-pattern>/user/*</url-pattern> </filter-mapping>

4.7 审计Filter的步骤

  1. 找到所有Filter:看 web.xml 或搜索 @WebFilter
  2. 看过滤范围:url-pattern 是什么?有没有遗漏的接口?
  3. 看过滤逻辑:白名单怎么写的?有没有绕过可能?
  4. 看执行顺序:chain.doFilter() 的位置对不对?
  5. 看错误处理:鉴权失败是 return 还是继续执行?

📎 深入学习:


五、Listener监听器

5.1 什么是Listener?(一句话理解)

Listener是Java Web的"监控摄像头"。它监听Web应用中的各种事件(应用启动、会话创建、请求到来等),事件发生时自动执行你写的代码。

💡 类比:Listener就像你家的智能门铃。有人按门铃(事件发生),门铃自动响(执行代码),不需要你手动去检查。

5.2 Listener的三种类型

类型监听对象常用场景
ServletContextListener应用生命周期应用启动时加载配置、初始化数据库连接池
HttpSessionListener会话生命周期统计在线人数、记录用户登录时长
ServletRequestListener请求生命周期记录请求日志、统计API访问次数

5.3 ServletContextListener

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

java
package 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中加上几行 图片.png

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>

效果

图片.png


5.4 HttpSessionListener(统计在线人数)

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

java
package 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中改一点

图片.png

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

图片.png

为什么呢,因为在LoginServlet中会有创建session,所以

登录成功 → 执行 req.getSession() → 创建 Session → 触发 sessionCreated → 在线人数 +1。

登录失败 → 直接返回错误,没有调用 getSession() → 不创建 Session → 不触发。 图片.png


5.5 ServletRequestListener(请求日志)

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"); } } }

图片.png


5.6 Listener在审计中的关注点

Listener本身不直接产生漏洞,但审计时要注意:

关注点说明
初始化敏感操作contextInitialized 中是否加载了硬编码密码、密钥?
全局属性安全setAttribute 存入ServletContext的数据是否敏感?
监听器顺序多个Listener的执行顺序是否影响安全逻辑?
内存泄漏contextDestroyed 是否正确释放了资源?

审计示例:

java
// 在Listener中发现硬编码密码! public void contextInitialized(ServletContextEvent sce) { String dbPassword = "Admin@123456"; // ❌ 硬编码! sce.getServletContext().setAttribute("DB_PASSWORD", dbPassword); }

📎 深入学习:


六、总结:一张图看懂Java Web基础

┌─────────────────────────────────────────────────────────────┐ │ 浏览器 │ └─────────────────────────────────────────────────────────────┘ ↓ HTTP请求 ┌─────────────────────────────────────────────────────────────┐ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ Listener │ │ Filter │ │ Filter │ │ │ │(请求监听) │ │(编码设置) │ │(登录鉴权) │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ 不通过 → 拦截返回 │ │ 通过 ↓ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Servlet │ │ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │ │ │ doGet() │ │doPost() │ │service()│ │ │ │ │ └─────────┘ └─────────┘ └─────────┘ │ │ │ │ ↓ │ │ │ │ 调用业务逻辑 → MyBatis → JDBC → 数据库 │ │ │ │ ↓ │ │ │ │ 返回响应 │ │ │ └─────────────────────────────────────────────────────┘ │ │ ↓ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ Filter │ │ Filter │ │ Listener │ │ │ │(XSS过滤) │ │(日志记录) │ │(请求完成) │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ │ └─────────────────────────────────────────────────────────────┘ ↓ HTTP响应 ┌─────────────────────────────────────────────────────────────┐ │ 浏览器 │ └─────────────────────────────────────────────────────────────┘

七、学习建议与下一步

  1. 不要只看不练:把上面的每个代码示例都在IDEA里敲一遍,跑起来,打断点跟一遍。

  2. 画流程图:学完这章后,自己手动画一张"请求从浏览器到数据库再返回"的完整流程图,能画出来才算真懂了。

  3. 审计思维培养:看任何代码时,问自己三个问题:

    • 用户输入从哪来?(getParameter、getHeader等)
    • 输入经过了哪些处理?(过滤?转义?拼接?)
    • 最终到了哪里?(数据库?文件系统?页面输出?)
  4. 下一步:当你能熟练画出上面的流程图后,就可以进入**第34天(反射机制)**的学习了。反射是Java反序列化漏洞的基础,也是Java安全中最"魔法"的部分。


附:本章核心概念速查表

概念一句话解释审计关键词
Servlet处理HTTP请求的工人@WebServlet、doGet、doPost
JDBCJava连接数据库的翻译官Statement、PreparedStatement、executeQuery
MyBatisJDBC的自动化升级版#{ }、${ }、@Select、Mapper.xml
Filter请求的安检门@WebFilter、doFilter、chain.doFilter()
ListenerWeb应用的监控摄像头@WebListener、ServletContextListener