耗时一个月开发的OJ在线判题系统,文末有项目地址,目前还在更新代码~
针对上篇提到的异常情况,分别有如下方案,可以提高程序安全性
1、超时控制
2、限制给用户程序分配的资源
3、限制代码
文章目录
1、超时控制
通过创建一个守护线程,超时后自动中断 Process 实现
// 超时控制
new Thread(() -> {
try {
Thread.sleep(TIME_OUT);
System.out.println("超时了,中断");
runProcess.destroy();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}).start();
2、限制资源分配
我们不能让每个 java 进程的执行占用的 JVM 最大堆内存空间都和系统默认的一致(正常默认8 G),实际上应该更小(执行用户的题目代码也不需要这么多),比如说256MB
在启动 java 程序时,可以指定 JVM 的参数: -Xmx256m (最大堆空间大小为256MB)
java -Xmx256m
注意,-Xmx参数,JVM的堆内存限制,不等同于系统实际占用的最大资源,可能会溢出
如果需要更严格的内存限制,要在系统层面去限制,而不是 JVM 层面的限制。
如果是 Linux 系统,可以使用 cgroup 来实现对某个进程的 CPU、内存等资源的分配
小知识-什么是 cgroup ?
**cgroup**
是 Linux 内核提供的一种机制,可以用来限制进程组(包括子进程)的资源使用,例如内存、CPU 、磁盘 I/O 等。通过将 Java 进程放置在特定的 cgroup
中,你可以实现限制其使用的内存和 CPU 数。
小知识-常用 JVM 启动参数
- 内存相关参数:
○ -Xms: 设置 JVM 的初始堆内存大小。1680129124930162690_0.785241782385159
○ -Xmx: 设置 JVM 的最大堆内存大小。
○ -Xss: 设置线程的栈大小。
○ -XX:MaxMetaspaceSize: 设置 Metaspace(元空间)的最大大小。1680129124930162690_0.5789460697462636
○ -XX:MaxDirectMemorySize: 设置直接内存(Direct Memory)的最大大小。
- 垃圾回收相关参数:
○ -XX:+UseSerialGC: 使用串行垃圾回收器。
○ -XX:+UseParallelGC: 使用并行垃圾回收器。1680129124930162690_0.8186470925040801
○ -XX:+UseConcMarkSweepGC: 使用 CMS 垃圾回收器。
○ -XX:+UseG1GC: 使用 G1 垃圾回收器。 - 线程相关参数:
○ -XX:ParallelGCThreads: 设置并行垃圾回收的线程数。1680129124930162690_0.8722533709020122
○ -XX:ConcGCThreads: 设置并发垃圾回收的线程数。
○ -XX:ThreadStackSize: 设置线程的栈大小。 - JIT 编译器相关参数:
○ -XX:TieredStopAtLevel: 设置 JIT 编译器停止编译的层次。 - 其他资源限制参数:1680129124930162690_0.45881515540130735
○ -XX:MaxRAM: 设置 JVM 使用的最大内存。
3、限制代码 - 黑白名单
实现
先定义一个黑白名单,比如哪些操作是禁止的,可以就是一个列表:
private static final List<String> blackList = Arrays.asList("Files", "exec");
还可以使用字典树代替列表存储单词,用更少的空间存储更多的敏感词汇,并且实现更高效的敏感词查找。
字典树原理:
此处使用 HuTool 工具库的字典树工具类: WordTree ,不用自己写字典树!
1)先初始化字典树,插入禁用词
private static final WordTree WORD_TREE;
static {
// 初始化字典树
WORD_TREE = new WordTree();
WORD_TREE.addWords(blackList);
}
2)校验用户代码是否包含禁用词:
FoundWord foundWord = WORD_TREE.matchWord(code);
if(foundWord != null){
System.out.println("包含禁止词" + foundWord.getFoundWord());
return null;
}
本方案的缺点
1)无法遍历所有黑名单
2)不同的编程语言,对应的领域,关键词都不一样,限制人工成本很大
4、限制权限- java System.setSecurityManager(new MySecurityManager());安全管理器
目标:限制用户对文件,内存,CPU,网络等资源的操作和访问
java 安全管理器基本使用
java 安全管理器(Security Manager)是 Java 提供的保护 JVM、JAVA 安全的机制,可以实现更严格的资源和操作权限
编写安全管理器,只需要继承 Security Manager
1)所有权限放开:
package com.yupi.yuojcodesandbox.security;
import java.security.Permission;
/**
* 默认安全管理器
*/
public class DefaultSecurityManager extends SecurityManager {
// 检查所有的权限
@Override
public void checkPermission(Permission perm) {
System.out.println("默认不做任何限制");
System.out.println(perm);
// super.checkPermission(perm);
}
}
2)所有权限拒绝
package com.yupi.yuojcodesandbox.security;
import java.security.Permission;
/**
* 禁用所有权限安全管理器
*/
public class DenySecurityManager extends SecurityManager {
// 检查所有的权限
@Override
public void checkPermission(Permission perm) {
throw new SecurityException("权限异常:" + perm.toString());
}
}
3)限制读权限:
@Override
public void checkRead(String file) {
throw new SecurityException("checkRead 权限异常:" + file);
}
4)限制写文件权限
@Override
public void checkWrite(String file) {
throw new SecurityException("checkWrite 权限异常:" + file);
}
5)限制执行文件权限
@Override
public void checkExec(String cmd) {
throw new SecurityException("checkExec 权限异常:" + cmd);
}
6)限制网络连接权限
@Override
public void checkConnect(String host, int port) {
throw new SecurityException("checkConnect 权限异常:" + host + ":" + port);
}
结合项目使用
在程序中启用安全管理器:
System.setSecurityManager(new MySecurityManager());
实际情况下,不应该在主类(开发者自己写的程序)中做限制,只需要限制子程序的权限即可。
启动子进程执行命令时,设置安全管理器,而不是在外层设置(会限制住测试用例的读写和子命令的执行)
具体操作如下:
1)根据需要开发自定义的安全管理器(MySecurityManager)
2)复制MySecurityManager 类到 resources/security 目录下,移除类的包名
3)手动输入命令编译 MySecurityManager 类,得到 class 文件
4)在运行 java 程序时,指定安全管理器 class 文件的路径,安全管理器的名称。
命令:
三个%s 分别是要运行的代码所在文件的父目录,安全管理器 class 文件的路径,输入参数
java -Xmx256m -Dfile.encoding=UTF-8 -cp %s;%s -Djava.security.manager=MySecurityManager Main %s
重新执行程序,发现资源成功被限制
安全管理器的优点
1)权限控制很灵活
2)实现简单
安全管理器的缺点
1)如果要做比较严格的权限限制,需要自己去判断哪些文件,包名需要允许读写,粒度太细,难以精细化控制
2)安全管理器本身也是 JAVA代码,也有可能存在漏洞,本质上还是程序层面的控制,没深入系统的层面
5、运行环境隔离
原理:操作系统层面上,把用户程序封装到沙箱里,和宿主机(我们的电脑 / 服务器) 隔离开,使得用户的程序无法影响宿主机。
实现方式: Docker 容器技术 (底层是用 cgroup,namespace 等方式实现的),也可以直接用 cgroup 实现
项目地址
(求求大佬们赏个star~)
前端:https://github.com/IMZHEYA/yoj-frontend
后端:https://github.com/IMZHEYA/yoj-backend
代码沙箱:https://github.com/IMZHEYA/yoj-code-sandbox