跳到主要内容

JVM 安全机制

JVM提供了多层次的安全机制,确保Java程序在受控环境中安全运行。这些机制包括类加载安全、字节码验证等。

重要提示:Security Manager 已弃用并移除

Security Manager 在 JDK 17 中被标记为弃用(for removal),在 JDK 24 中已被永久禁用。

  • JDK 17:Security Manager 被标记为 @Deprecated(forRemoval=true),启用时会发出警告
  • JDK 18-23:继续发出弃用警告,默认禁用
  • JDK 24+:Security Manager 被永久禁用,无法启用,API将在未来版本中完全移除

替代方案

  • 使用容器(Docker、Kubernetes)进行隔离
  • 使用操作系统级别的安全机制(如 Linux seccomp、macOS App Sandbox)
  • 使用虚拟机或沙箱技术
  • 对于API拦截需求,可使用 Java Agent 进行字节码转换

本教程保留 Security Manager 相关内容供理解历史背景和维护旧系统参考,但不建议在新项目中使用。

为什么需要JVM安全机制?

安全威胁

Java安全模型

Java安全模型基于沙箱(Sandbox)概念,限制不可信代码的行为:

类加载安全

双亲委派模型的安全作用

双亲委派模型防止核心类库被篡改:

// 恶意代码尝试替换核心类
package java.lang;

public class String {
// 恶意实现
static {
// 执行恶意代码
Runtime.getRuntime().exec("rm -rf /");
}
}

防护机制

  • 启动类加载器优先加载核心类库
  • 自定义的java.lang.String永远不会被加载

命名空间隔离

不同的类加载器加载的类属于不同的命名空间:

public class NamespaceIsolation {
public static void main(String[] args) throws Exception {
// 自定义类加载器
ClassLoader loader1 = new CustomClassLoader("loader1");
ClassLoader loader2 = new CustomClassLoader("loader2");

Class<?> class1 = loader1.loadClass("com.example.MyClass");
Class<?> class2 = loader2.loadClass("com.example.MyClass");

// class1和class2是不同的类
System.out.println(class1 == class2); // false
System.out.println(class1.equals(class2)); // false
}
}

字节码验证

验证器的作用

字节码验证器确保.class文件符合JVM规范,不会危害虚拟机安全:

验证阶段

  1. 文件格式验证

    • 魔数检查(0xCAFEBABE)
    • 版本号检查
    • 常量池有效性
  2. 元数据验证

    • 类继承关系
    • 抽象方法实现
    • 字段和方法合法性
  3. 字节码验证

    • 操作数栈平衡
    • 类型转换合法性
    • 跳转指令合法性
  4. 符号引用验证

    • 类是否存在
    • 字段和方法是否存在
    • 访问权限检查

关闭字节码验证

# 开发环境可以关闭验证以加快启动速度(不推荐生产环境)
-Xverify:none
# 或
-noverify

安全管理器(已弃用)

已弃用并移除

Security Manager 在 JDK 17 中弃用,JDK 24 中永久禁用。本节内容仅供理解历史背景和维护旧系统参考。

弃用时间线

JDK版本状态说明
JDK 17弃用标记为 @Deprecated(forRemoval=true),启用时发出警告
JDK 18默认禁用java.security.manager 默认值为 disallow
JDK 21继续弃用仍可启用但有警告
JDK 24永久禁用无法通过任何方式启用,System.setSecurityManager() 抛出 UnsupportedOperationException
未来版本完全移除API 将从 JDK 中删除

为什么移除 Security Manager?

根据 OpenJDK 官方说明(JEP 486):

  1. 使用率极低:在实际生产环境中几乎不被使用
  2. 维护成本高:超过 1000 个方法需要进行权限检查,超过 1200 个方法需要提升权限
  3. 复杂性过高:权限模型过于复杂,大多数启用它的应用都是授予所有权限
  4. 安全价值有限:现代安全威胁主要是恶意数据,Security Manager 对此防御能力有限

现代替代方案

容器隔离

# 使用 Docker 运行 Java 应用
docker run --rm \
--read-only \ # 只读文件系统
--cap-drop ALL \ # 移除所有 Linux capabilities
--network none \ # 禁用网络
my-java-app

操作系统级安全

# Linux seccomp 限制系统调用
# Kubernetes Security Context
securityContext:
readOnlyRootFilesystem: true
runAsNonRoot: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]

API 拦截替代方案

如果需要拦截 System.exit() 等调用,可以使用 Java Agent:

// 使用 Java Agent 拦截 System.exit
public class BlockSystemExitAgent {
public static void premain(String args, Instrumentation inst) {
inst.addTransformer((loader, className, classBeingRedefined,
protectionDomain, classBytes) -> {
// 使用字节码转换将 System.exit 调用替换为抛出异常
// 详见 JDK 24 的 Class-File API
return transformClass(classBytes);
});
}
}

SecurityManager(历史参考)

安全管理器曾是Java安全模型的核心组件,控制代码对系统资源的访问:

public class SecurityManagerExample {
public static void main(String[] args) {
// ⚠️ 已弃用!JDK 24+ 会抛出 UnsupportedOperationException
System.setSecurityManager(new SecurityManager());

// 尝试执行受限操作
try {
System.exit(0); // 可能被拒绝
} catch (SecurityException e) {
System.out.println("操作被拒绝: " + e.getMessage());
}
}
}

权限检查

public class PermissionCheck {
public void readFile(String path) {
// 检查文件读取权限
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkRead(path);
}

// 执行文件读取
// ...
}

public void writeFile(String path) {
// 检查文件写入权限
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkWrite(path);
}

// 执行文件写入
// ...
}
}

访问控制器(已弃用)

已弃用

AccessController 及相关 API 已随 Security Manager 一起弃用。JDK 24+ 中 AccessController.checkPermission() 始终抛出异常,AccessController.doPrivileged() 将直接执行给定的操作。

AccessController(历史参考)

访问控制器曾提供更细粒度的权限控制:

import java.security.AccessController;
import java.security.PrivilegedAction;

public class AccessControllerExample {
public void privilegedOperation() {
// ⚠️ 已弃用!JDK 24+ 中 doPrivileged 直接执行,无特权提升效果
AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
// 这段代码曾具有调用者的权限
System.setProperty("user.name", "admin");
return null;
});
}
}

迁移指南

如果你的代码使用了 AccessController.doPrivileged()

// 旧代码(已弃用)
AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
// 特权操作
return null;
});

// 迁移方案:直接移除 doPrivileged 包装
// 在 JDK 24+ 中,doPrivileged 已经没有特权效果
() -> {
// 原来的操作
};

安全策略文件(已弃用)

已弃用

安全策略文件是 Security Manager 的一部分,已随 Security Manager 一起弃用。JDK 24+ 中策略文件被忽略。

策略文件格式(历史参考)

// 授予所有代码所有权限
grant {
permission java.security.AllPermission;
};

// 授予特定代码源权限
grant codeBase "file:/home/user/app/" {
permission java.io.FilePermission "/home/user/app/*", "read,write";
permission java.net.SocketPermission "localhost:8080", "connect";
};

// 授予特定签名者权限
grant signedBy "MyCompany" {
permission java.lang.RuntimePermission "exitVM";
};

// 授予特定主体权限
grant principal com.example.User "admin" {
permission java.io.FilePermission "/admin/*", "read,write";
};

常用权限

权限类用途示例
FilePermission文件访问FilePermission "/tmp/*", "read,write"
SocketPermission网络访问SocketPermission "*.example.com:80", "connect"
RuntimePermission运行时操作RuntimePermission "exitVM"
PropertyPermission系统属性PropertyPermission "user.home", "read"
ReflectPermission反射ReflectPermission "suppressAccessChecks"
SecurityPermission安全管理SecurityPermission "getPolicy"
AllPermission所有权限AllPermission

加载策略文件

# 命令行指定策略文件
java -Djava.security.manager \
-Djava.security.policy=my.policy \
MyApp

# 追加策略文件
java -Djava.security.manager \
-Djava.security.policy==my.policy \
MyApp

代码签名与验证

签名JAR文件

# 生成密钥对
keytool -genkeypair -alias mykey -keyalg RSA -keystore mykeystore.jks

# 签名JAR文件
jarsigner -keystore mykeystore.jks myapp.jar mykey

# 验证签名
jarsigner -verify -verbose -certs myapp.jar

验证签名

import java.security.cert.*;
import java.util.jar.*;

public class SignatureVerification {
public boolean verifyJar(String jarPath) throws Exception {
JarFile jarFile = new JarFile(jarPath, true);

// 验证所有条目
Enumeration<JarEntry> entries = jarFile.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();

// 读取条目内容以触发验证
try (InputStream is = jarFile.getInputStream(entry)) {
byte[] buffer = new byte[8192];
while (is.read(buffer) != -1) {
// 读取数据
}
}

// 检查签名
Certificate[] certs = entry.getCertificates();
if (certs != null) {
for (Certificate cert : certs) {
// 验证证书
cert.verify(cert.getPublicKey());
}
}
}

return true;
}
}

沙箱模型

历史背景:Applet沙箱(已移除)

Applet曾是Java在浏览器中运行的技术,运行在严格的沙箱中:

注意:Applet已在Java 9中标记为弃用,Java 17中移除。Applet沙箱模型已成为历史。

现代沙箱实现

现代Java应用通常使用以下技术实现隔离和安全:

1. 容器化隔离

# Kubernetes Pod Security Context
apiVersion: v1
kind: Pod
metadata:
name: java-app
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: my-java-app:latest
securityContext:
capabilities:
drop: ["ALL"]
resources:
limits:
memory: "512Mi"
cpu: "500m"

2. 进程级隔离

# 使用 firejail 沙箱运行 Java 应用
firejail --noprofile --private --net=none java -jar app.jar

# 使用 bubblewrap
bwrap --ro-bind /usr /usr \
--ro-bind /lib /lib \
--dev /dev \
--proc /proc \
--unshare-net \
java -jar app.jar

3. JVM进程隔离

对于多租户场景,可以使用:

  • GraalVM Native Image:编译为独立可执行文件
  • JDK 21+ Virtual Threads:轻量级线程隔离
  • OSGi:模块化隔离(同一JVM内)

自定义安全管理器(历史参考)

已弃用

以下代码仅供理解历史实现参考,JDK 24+ 中将无法使用。

public class SandboxSecurityManager extends SecurityManager {
private final Set<String> allowedPackages;
private final Set<String> allowedPaths;

public SandboxSecurityManager() {
this.allowedPackages = Set.of("java.lang", "java.util");
this.allowedPaths = Set.of("/tmp/sandbox");
}

@Override
public void checkPackageAccess(String pkg) {
if (!allowedPackages.contains(pkg)) {
throw new SecurityException("Package access denied: " + pkg);
}
}

@Override
public void checkRead(String file) {
if (allowedPaths.stream().noneMatch(file::startsWith)) {
throw new SecurityException("File read denied: " + file);
}
}

@Override
public void checkExec(String cmd) {
throw new SecurityException("Process execution denied");
}
}

安全最佳实践

1. 使用容器和操作系统级安全

# Kubernetes 安全配置示例
securityContext:
runAsNonRoot: true
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
seccompProfile:
type: RuntimeDefault

2. 输入验证(防止注入攻击)

public class InputValidation {
public void processUserInput(String input) {
// 验证输入
if (input == null || input.length() > 1000) {
throw new IllegalArgumentException("Invalid input");
}

// 防止注入攻击
if (input.contains(";") || input.contains("--")) {
throw new SecurityException("Potential injection attack");
}

// 处理输入
// ...
}
}

3. 安全编码实践

public class SecureCoding {
// 防止反序列化漏洞
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException {
// 验证对象状态
in.defaultReadObject();

if (this.sensitiveField == null) {
throw new InvalidObjectException("Invalid object state");
}
}

// 防止反射攻击
private final String sensitiveData;

public SecureCoding(String data) {
// 防御性拷贝
this.sensitiveData = Objects.requireNonNull(data);
}

public String getSensitiveData() {
// 返回不可变副本
return sensitiveData;
}
}

4. 从 Security Manager 迁移

如果你的应用仍在使用 Security Manager,以下是迁移建议:

检测是否使用 Security Manager

# 检查命令行参数
grep -r "java.security.manager" scripts/

# 使用 jdeprscan 扫描弃用 API
jdeprscan --release 21 your-app.jar

# 在 JDK 17-23 上测试禁用效果
java -Djava.security.manager=disallow -jar app.jar

常见使用场景的替代方案

原用途现代替代方案
限制文件访问容器只读文件系统、chroot
限制网络访问容器网络策略、防火墙规则
限制系统调用seccomp、AppArmor
拦截 System.exit()Java Agent 字节码转换
多租户隔离容器、进程隔离
代码签名验证容器镜像签名、SBOM

小结

JVM安全机制的发展反映了Java生态系统的演进:

传统安全机制(已弃用/移除)

  1. Security Manager:JDK 17 弃用,JDK 24 永久禁用
  2. 策略文件:随 Security Manager 一起弃用
  3. Applet 沙箱:JDK 17 移除

现代安全实践

  1. 容器隔离:Docker、Kubernetes 安全上下文
  2. 操作系统安全:seccomp、AppArmor、SELinux
  3. 安全编码:输入验证、防御性编程
  4. 依赖安全:使用安全的库、定期更新依赖

仍然有效的安全机制

  1. 类加载安全:双亲委派模型、命名空间隔离
  2. 字节码验证:确保代码符合规范

理解这些安全机制有助于构建安全的Java应用,但应优先使用现代的安全技术和工具。

参考资料