【java安全】java安全基础

反射

什么是反射?

Java 中,每一个类被加载到 JVM 后,都会生成一个唯一的Class对象(可以理解为这个类的 "全量信息说明书"):

  • 这个Class对象里存着该类的所有信息:类名、父类、接口、属性(Field)、方法(Method)、构造器(Constructor);
  • 反射的本质,就是通过这个Class对象,"拆解" 出里面的信息,再动态操作。

反射的核心类(java.lang.reflect 包)

Class 表示类 / 接口的字节码对象 类名.class/对象.getClass()/Class.forName("全类名")
Field 表示类的属性 Class.getField()(公有)/getDeclaredField()(所有)
Method 表示类的方法 Class.getMethod()(公有)/getDeclaredMethod()(所有)
Constructor 表示类的构造器 Class.getConstructor()(公有)/getDeclaredConstructor()(所有)

反射的基本操作

1、定义测试类

首先定义一个基本的存在公/私有属性、方法、构造器的测试类User

java 复制代码
package com.qdy;

/**
 * 测试反射的普通类(包含公有/私有属性、方法、构造器)
 */
public class User {
    // 公有属性
    public String username;
    // 私有属性
    private Integer age;
    // 无参构造(公有)
    public User() {}
    // 有参构造(私有)
    private User(String username, Integer age) {
        this.username = username;
        this.age = age;
    }
    // 公有方法
    public void sayHello() {
        System.out.println("Hello, " + username + "! 年龄:" + age);
    }
    // 私有方法
    private String getSecret(String key) {
        return "私有方法返回:" + key + "-" + age;
    }

    public String getUsername() {
        return username;
    }

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

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{username='" + username + "', age=" + age + "}";
    }
}

2、反射核心操作代码

java 复制代码
package com.qdy;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class Main {
    public static void main(String[] args) throws Exception {
        // ===================== 1. 获取Class对象(三种方式) =====================
        // 方式1:类名.class(编译期确定,最安全)
        Class<User> clazz1 = User.class;
        // 方式2:对象.getClass()(运行时获取,需先实例化)
        User user = new User();
        Class<? extends User> clazz2 = user.getClass();
        // 方式3:Class.forName("全类名")(动态加载,最灵活,需处理ClassNotFoundException)
        Class<?> clazz3 = Class.forName("com.qdy.User");

        // 验证三个Class对象是同一个(JVM中一个类只有一个Class实例)
        System.out.println(clazz1 == clazz2 && clazz2 == clazz3); // 输出true

        // ===================== 2. 动态创建对象(两种方式) =====================
        // 方式1:通过无参构造器(公有)
        User user1 = clazz1.newInstance(); // JDK9后标记为过时,推荐用getConstructor().newInstance()
        User user2 = clazz1.getConstructor().newInstance(); // 推荐方式

        // 方式2:通过私有构造器(需打破封装)
        Constructor<?> privateConstructor = clazz1.getDeclaredConstructor(String.class, Integer.class);
        privateConstructor.setAccessible(true); // 关闭访问检查(核心!否则无法访问私有成员)
        User user3 = (User) privateConstructor.newInstance("张三", 20);
        System.out.println("私有构造器创建的对象:" + user3); // 输出User{username='张三', age=20}

        // ===================== 3. 操作属性(公有/私有) =====================
        // 3.1 操作公有属性
        Field usernameField = clazz1.getField("username");
        usernameField.set(user2, "李四"); // 给user2的username赋值
        System.out.println("公有属性赋值后:" + user2.getUsername()); // 输出李四

        // 3.2 操作私有属性
        Field ageField = clazz1.getDeclaredField("age");
        ageField.setAccessible(true); // 关闭访问检查
        ageField.set(user2, 25); // 给user2的私有属性age赋值
        System.out.println("私有属性赋值后:" + user2); // 输出User{username='李四', age=25}

        // ===================== 4. 调用方法(公有/私有) =====================
        // 4.1 调用公有方法
        Method sayHelloMethod = clazz1.getMethod("sayHello");
        sayHelloMethod.invoke(user2); // 调用user2的sayHello(),输出Hello, 李四! 年龄:25

        // 4.2 调用私有方法
        Method getSecretMethod = clazz1.getDeclaredMethod("getSecret", String.class);
        getSecretMethod.setAccessible(true); // 关闭访问检查
        String secret = (String) getSecretMethod.invoke(user2, "abc123"); // 调用私有方法并传参
        System.out.println("私有方法返回值:" + secret); // 输出私有方法返回:abc123-25
    }
}

快速查阅索引:

反射功能 核心代码关键步骤
获取 Class 对象 类名.class / 对象.getClass() / Class.forName()
公有构造创建对象 clazz.getConstructor().newInstance()
私有构造创建对象 getDeclaredConstructor(参数类型) + setAccessible(true) + newInstance(参数)
操作公有属性 getField(属性名) + set(对象, 值)
操作私有属性 getDeclaredField(属性名) + setAccessible(true) + set(对象, 值)
调用公有方法 getMethod(方法名, 参数类型) + invoke(对象, 参数)
调用私有方法 getDeclaredMethod(方法名, 参数类型) + setAccessible(true) + invoke(对象, 参数)

反序列化

什么是反序列化?

序列化(Serialization):将 Java 对象转化为字节序列(二进制数据),便于存储 / 网络传输;

反序列化(Deserialization):将字节序列还原为 Java 对象的过程。

核心 API(java.io 包)

类 / 接口 作用 核心方法
Serializable 标记接口(无方法) 实现该接口的类才能被序列化
ObjectOutputStream 序列化对象 writeObject(Object obj)
ObjectInputStream 反序列化对象 readObject()

反序列化的基本操作

创建一个普通的类,继承Serializable接口,不过不需要重新方法,本来就是空接口

java 复制代码
package com.qdy;

import java.io.Serializable;

// 实现Serializable标记接口(否则序列化时报NotSerializableException)
public class User implements Serializable {
    // 序列化版本号(避免类结构变化导致反序列化失败)
    private static final long serialVersionUID = 1L;

    public String username;
    private Integer age;

    public User() {}
    public User(String username, Integer age) {
        this.username = username;
        this.age = age;
    }

    // 自定义readObject(反序列化时会被反射调用!核心安全点)
    private void readObject(java.io.ObjectInputStream in) throws Exception {
        // 先执行默认反序列化逻辑
        in.defaultReadObject();
        // 可自定义校验/逻辑(若被恶意篡改,此处可能执行危险代码)
        System.out.println("反序列化调用readObject:" + username);
    }

    @Override
    public String toString() {
        return "User{username='" + username + "', age=" + age + "}";
    }

}

然后创建main方法尝试序列化和反序列化操作

java 复制代码
package com.qdy;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class Main {
    public static void main(String[] args) throws Exception {
        // ===================== 1. 序列化:对象 → 字节流 =====================
        User user = new User("张三", 20);
        // 写入文件(也可写入字节数组、网络流)
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.ser"));
        oos.writeObject(user); // 序列化核心方法
        System.out.println("序列化完成:" + user);

        // ===================== 2. 反序列化:字节流 → 对象 =====================
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.ser"));
        // 反序列化核心方法(底层通过反射创建对象、调用readObject)
        User deserializeUser = (User) ois.readObject();
        System.out.println("反序列化结果:" + deserializeUser);
    }
}

运行发现自定义的readObject方法在反序列化时被调用

假如readObject方法存在恶意代码,就会产生安全问题

java 复制代码
private void readObject(java.io.ObjectInputStream in) throws Exception {
    // 先执行默认反序列化逻辑
    in.defaultReadObject();
    Runtime.getRuntime().exec("calc");
}

运行Main类发现反序列化时成功执行代码,弹出计算器

类加载机制

类加载的核心生命周期(5 个核心阶段)

类从加载到 JVM 内存到可用,核心经历 5 步(加载→验证→准备→解析→初始化),最终生成反射的核心Class对象:

阶段 核心作用
加载 读取.class字节码文件,生成Class对象(反射的入口)
验证 校验字节码合法性(防恶意字节码)
准备 为静态变量分配内存,设默认值(如int默认 0,引用默认null
解析 把类名 / 方法名等符号引用转为内存地址(直接引用)
初始化 执行静态代码块、给静态变量赋初始值(真正的 "初始化")

核心原则: 双亲委派机制

逻辑:子类加载器先委托父加载器加载类,父加载不到时,子类才自己加载。

目的 :避免核心类(如java.lang.String)被篡改,保证类加载唯一性。

默认类加载器层级

启动类加载器(Bootstrap,C++ 实现,加载 JRE 核心类)→ 扩展类加载器(Ext,加载 JRE 扩展类)→ 应用类加载器(App,加载 classpath 下的类)。

类加载器分类

加载器类型 加载路径 特点
启动类加载器 JRE/lib/rt.jar(如java.lang.* C++ 实现,无 Java 对象
扩展类加载器 JRE/lib/ext/*.jar Java 实现,父加载器是 Bootstrap
应用类加载器 项目 classpath 目录 日常开发默认使用的加载器

自定义类加载器

1. 自定义原因

  • 加载非 classpath 下的字节码(如磁盘指定路径、网络传输的字节码);
  • 实现字节码加密 / 解密(加载前解密,防反编译);
  • 热部署(动态替换类的字节码)。

2.实现核心

继承ClassLoader重写findClass方法 (而非loadClass,避免破坏双亲委派),核心两步:

1、 读取字节码文件为字节数组

2、调用defineClass生成Class对象。

3.实现自定义类加载器

随便找个目录写下如下测试类

java 复制代码
public class TestClass {
	public void hello() {
		System.out.println("自定义类加载器加载成功!")
	}	
}

使用javac命令编译该测试类,生成.class文件

自定义类加载器

java 复制代码
package com.qdy;

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.InputStream;

// 自定义类加载器(核心:重写findClass)
public class CustomClassLoader extends ClassLoader {
    private String rootPath; // 字节码文件根路径(如D:/test/)

    // 构造器:指定字节码根路径
    public CustomClassLoader(String rootPath) {
        this.rootPath = rootPath;
    }

    // 核心方法:父加载器加载不到时,自定义加载逻辑
    @Override
    protected Class<?> findClass(String className) throws ClassNotFoundException {
        try {
            // 1. 类名转文件路径(com.qdy.TestClass → com/qdy/TestClass.class)
            String classFile = className.replace(".", "/") + ".class";
            String filePath = rootPath + classFile;

            // 2. 读取字节码文件为字节数组
            InputStream is = new FileInputStream(filePath);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024];
            int len;
            while ((len = is.read(buffer)) != -1) {
                baos.write(buffer, 0, len);
            }
            byte[] classBytes = baos.toByteArray();
            is.close();
            baos.close();

            // 3. 核心:将字节数组转为Class对象(JVM底层生成Class)
            return defineClass(className, classBytes, 0, classBytes.length);
        } catch (Exception e) {
            throw new ClassNotFoundException("加载类失败:" + className, e);
        }
    }
}

在Main类中的main方法使用该类加载尝试调用类

java 复制代码
package com.qdy;

public class Main {
    // 测试入口
    public static void main(String[] args) throws Exception {
        // 1. 创建自定义类加载器,指定字节码根路径
        CustomClassLoader loader = new CustomClassLoader("E:\\java_project\\test\\");
        // 2. 加载类(触发双亲委派:父加载器先加载,加载不到则调用findClass)
        Class<?> clazz = loader.loadClass("TestClass");
        // 3. 反射调用方法(验证类加载成功)
        Object obj = clazz.getConstructor().newInstance();
        clazz.getMethod("hello").invoke(obj);
        // 验证类加载器(输出自定义加载器,而非默认应用类加载器)
        System.out.println("当前类的加载器:" + clazz.getClassLoader().getClass().getName());
    }
}

运行后成功使用我们自定义的类加载器以类反射的方式调用了测试类

当然也可使用http方式加载类

修改自定义类加载器CustomClassLoader

java 复制代码
package com.qdy;

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.InputStream;
import java.net.URL;

// 自定义类加载器(核心:重写findClass)
public class CustomClassLoader extends ClassLoader {
    private String urlPath;

    // 构造器:指定字节码根路径
    public CustomClassLoader(String urlPath) {
        this.urlPath = urlPath;
    }

    // 重写findClass,使得从HTTP下载字节码
    @Override
    protected Class<?> findClass(String className) throws ClassNotFoundException {
        try {
            // 1. 类名转文件路径(com.qdy.TestClass → com/qdy/TestClass.class)
            String classFile = className.replace(".", "/") + ".class";
            String filePath = urlPath + classFile;

            // 2. 发送HTTP请求获取下载字节码
            URL url = new URL(filePath);
            InputStream is = url.openStream();
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024];
            int length;
            while ((length = is.read(buffer)) != -1) {
                bos.write(buffer, 0, length);
            }
            byte[] bytes = bos.toByteArray();

            // 3. 核心:将字节数组转为Class对象(JVM底层生成Class)
            return defineClass(className, bytes, 0, bytes.length);
        } catch (Exception e) {
            throw new ClassNotFoundException("加载类失败:" + className, e);
        }
    }
}

测试main方法如下

java 复制代码
package com.qdy;

public class Main {
    // 测试入口
    public static void main(String[] args) throws Exception {
        // 1. 创建自定义类加载器,指定远程服务器地址
        CustomClassLoader loader = new CustomClassLoader("http://127.0.0.1:5200/");
        // 2. 加载远程类
        Class<?> clazz = loader.loadClass("TestClass");
        // 3. 反射调用方法(验证类加载成功)
        Object obj = clazz.getConstructor().newInstance();
        clazz.getMethod("hello").invoke(obj);
        // 验证类加载器(输出自定义加载器,而非默认应用类加载器)
        System.out.println("当前类的加载器:" + clazz.getClassLoader().getClass().getName());
    }
}

在本地使用python直接在存在.class文件启动http服务

shell 复制代码
python -m http.server 5200

运行main方法

成功使用http远程加载.class文件

相关推荐
白帽黑客-晨哥2 小时前
2026网络安全学习攻略:从入门到专家之路
web安全·网络安全·零基础·渗透测试·湖南省网安基地
白帽黑客-晨哥2 小时前
如何从零基础成为白帽黑客
web安全·渗透测试·白帽黑客
Data_agent2 小时前
OOPBUY模式淘宝1688代购系统搭建指南
开发语言·爬虫·python
Ashley_Amanda2 小时前
JavaScript 中数组的常用处理方法
开发语言·javascript·网络
报错小能手2 小时前
C++ STL bitset 位图
开发语言·c++
钓鱼的肝2 小时前
GESP系列(3级)小杨的储蓄
开发语言·数据结构·c++·笔记·算法·gesp
float_六七2 小时前
行级与块级元素:核心区别与应用场景
开发语言·前端·javascript
ZePingPingZe2 小时前
不使用Spring事务的管理—原生JDBC实现事务管理
java·数据库·spring
唐装鼠2 小时前
Rust Cow(deepseek)
开发语言·后端·rust