往期开发(已经完成)

  • OJ 判题项目-介绍(一)
  • OJ 判题项目-初始化(二)
  • OJ 判题项目-前后端联调(三)
  • OJ 判题项目-简易判题机(四)
  • OJ 判题项目-原生代码沙箱(五)
  • OJ 判题项目-Docker 代码沙箱(六)
  • OJ 判题项目-微服务改造(七)

  • 后端开发

    • 代码沙箱项目初始化

      代码沙箱定位:只负责接收代码和测试用例运行代码返回执行结果,不负责执行结果的正确性,主要实现 JAVA 代码沙箱,其他的相同

      创建独立的 springboot-web 项目 使用 IDEA 的 spring 初始化器

      然后编写测试接口启动项目测试即可

    • 代码沙箱简易执行流程

      1. 接收代码和测试用例以及用户的提交信息
      2. 根据语言进行编译代码,生成对应可执行文件
      3. 然后使用命令行的方式进行执行代码例如 java 的 javac 编译,java -cp 执行代码
        模拟代码沙箱的执行流程
    • 分析代码沙箱执行规范(JAVA)

      由于 java 原生执行代码需要指定类名传参
      所以要先保证类名的一致统一类名为 Main

    • 原生核心流程梳理

      主要实现思路:编写一套程序流程,当接口接收到请求之后,自动的去执行这些命令进行编译,运行,提取结果
      使用 jdk 中自带的进程类 Process 实现

      1. 将用户代码保存再进行编译得到 class 文件
      2. 执行代码得到输出结果
      3. 收集整理输出结果
      4. 文件清理,执行完成之后清除 class 文件防止磁盘占用
      5. 进行异常处理
    • 原生代码沙箱实现

      1. 保存用户代码并编译代码文件生成 class 文件

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        // 获取用户的工作目录
        String userDir = System.getProperty("user.dir");
        // 创建全局存储代码目录
        String globalCodePathName = userDir + File.separator + GLOBAL_CODE_FILE_NAME;
        // 没有的话创建目录
        if (!FileUtil.exist(globalCodePathName)) {
        FileUtil.mkdir(globalCodePathName);
        }
        // 创建用户单个目录存储代码文件 实现每个用户的代码隔离
        String userCodeFileParentPath = globalCodePathName + File.separator + UUID.randomUUID();
        // 创建用户的代码文件
        String userCodeFilePath = userCodeFileParentPath + File.separator + GLOBAL_CLASS_NAME;
        // 写入代码
        File userCodeFile = FileUtil.writeString(code, userCodeFilePath, StandardCharsets.UTF_8);
        try{
        // 1. 构建编译指令
        String compileCmd = String.format("javac -encoding utf-8 %s", userCodeFile.getAbsolutePath());
        // 2. 执行编译命令 得到结果
        Process compileProcess = Runtime.getRuntime().exec(compileCmd);
        ExecuteMessage compileExecuteMessage = ProcessUtils.runProcessAndGetMessage(compileProcess, "编译");
        System.out.println("编译的结果为:" + compileExecuteMessage);
        if (!StrUtil.isEmpty(compileExecuteMessage.getErrorMessage())) {
        return ProcessUtils.getErrorExecuteCodeResponse(compileExecuteMessage);
        }
        } catch (Exception e){
        e.printStackTrace();
        }
      2. 执行代码文件得到输出结果
        由于是多组输出结果要进行循环遍历每组用例分别执行并收集结果

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        List<ExecuteMessage> runExecuteMessageList = new ArrayList<>(inputList.size());
        long runMaxTime = 0L;
        try{
        for (String args : inputList) {
        // 1. 构建执行程序命令
        String runCmd = String.format("java -Dfile.encoding=UTF-8 -cp %s Main %s", userCodeFileParentPath, args);
        // 2. 执行运行命令 得到结果
        Process runProcess = Runtime.getRuntime().exec(runCmd);
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        ExecuteMessage runExecuteMessage = ProcessUtils.runProcessAndGetMessage(runProcess, "运行");
        stopWatch.stop();
        runMaxTime = Math.max(stopWatch.getLastTaskTimeMillis(), runMaxTime);
        System.out.println("运行结果为:" + runExecuteMessage);
        runExecuteMessageList.add(runExecuteMessage);
        }
        } catch (Exception e){
        e.printStackTrace();
        }
      3. 整理输出结果

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        List<String> outputList = new ArrayList<>();
        for (ExecuteMessage executeMessage : runExecuteMessageList) {
        if (!StrUtil.isEmpty(compileExecuteMessage.getErrorMessage())) {
        return ProcessUtils.getErrorExecuteCodeResponse(executeMessage);
        } else {
        outputList.add(executeMessage.getMessage());
        }
        }
        if (outputList.size() != runExecuteMessageList.size()) {
        executeCodeResponse.setMessage("结果数量不符合");
        return executeCodeResponse;
        }
        // 封装返回结果
        executeCodeResponse.setOutputList(outputList);
        executeCodeResponse.setMessage("运行成功");
        executeCodeResponse.setStatus(0);
        JudgeInfo judgeInfo = new JudgeInfo();
        judgeInfo.setMessage("结果正确");
        judgeInfo.setTime(runMaxTime);
        judgeInfo.setMemory(0L);
        executeCodeResponse.setJudgeInfo(new JudgeInfo());
      4. 清理文件

        1
        2
        3
        4
        5
        // 代码执行完毕之后 清除文件
        if (userCodeFile.getParentFile() != null) {
        boolean del = FileUtil.del(userCodeFileParentPath);
        System.out.println("删除" + (del ? "成功" : "失败"));
        }
      5. 全过程代码

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        36
        37
        38
        39
        40
        41
        42
        43
        44
        45
        46
        47
        48
        49
        50
        51
        52
        53
        54
        55
        56
        57
        58
        59
        60
        61
        62
        63
        64
        65
        66
        67
        68
        69
        70
        71
        72
        73
        74
         public static ExecuteCodeResponse doJudge(ExecuteCodeRequest executeCodeRequest) {
        ExecuteCodeResponse executeCodeResponse = new ExecuteCodeResponse();
        List<String> inputList = executeCodeRequest.getInputList();
        String code = executeCodeRequest.getCode();
        // 获取用户的工作目录
        String userDir = System.getProperty("user.dir");
        // 创建全局存储代码目录
        String globalCodePathName = userDir + File.separator + GLOBAL_CODE_FILE_NAME;
        // 没有的话创建目录
        if (!FileUtil.exist(globalCodePathName)) {
        FileUtil.mkdir(globalCodePathName);
        }
        // 创建用户单个目录存储代码文件 实现每个用户的代码隔离
        String userCodeFileParentPath = globalCodePathName + File.separator + UUID.randomUUID();
        // 创建用户的代码文件
        String userCodeFilePath = userCodeFileParentPath + File.separator + GLOBAL_CLASS_NAME;
        // 写入代码
        File userCodeFile = FileUtil.writeString(code, userCodeFilePath, StandardCharsets.UTF_8);
        try {
        // 1. 构建编译指令
        String compileCmd = String.format("javac -encoding utf-8 %s", userCodeFile.getAbsolutePath());
        // 2. 执行编译命令 得到结果
        Process compileProcess = Runtime.getRuntime().exec(compileCmd);
        ExecuteMessage compileExecuteMessage = ProcessUtils.runProcessAndGetMessage(compileProcess, "编译");
        System.out.println("编译的结果为:" + compileExecuteMessage);
        if (!StrUtil.isEmpty(compileExecuteMessage.getErrorMessage())) {
        return ProcessUtils.getErrorExecuteCodeResponse(compileExecuteMessage);
        }
        List<ExecuteMessage> runExecuteMessageList = new ArrayList<>(inputList.size());
        long runMaxTime = 0L;
        for (String args : inputList) {
        // 1. 构建执行程序命令
        String runCmd = String.format("java -Dfile.encoding=UTF-8 -cp %s Main %s", userCodeFileParentPath, args);
        // 2. 执行运行命令 得到结果
        Process runProcess = Runtime.getRuntime().exec(runCmd);
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        ExecuteMessage runExecuteMessage = ProcessUtils.runProcessAndGetMessage(runProcess, "运行");
        stopWatch.stop();
        runMaxTime = Math.max(stopWatch.getLastTaskTimeMillis(), runMaxTime);
        System.out.println("运行结果为:" + runExecuteMessage);
        runExecuteMessageList.add(runExecuteMessage);
        }
        List<String> outputList = new ArrayList<>();
        for (ExecuteMessage executeMessage : runExecuteMessageList) {
        if (!StrUtil.isEmpty(compileExecuteMessage.getErrorMessage())) {
        return ProcessUtils.getErrorExecuteCodeResponse(executeMessage);
        } else {
        outputList.add(executeMessage.getMessage());
        }
        }
        if (outputList.size() != runExecuteMessageList.size()) {
        executeCodeResponse.setMessage("结果数量不符合");
        return executeCodeResponse;
        }
        // 封装返回结果
        executeCodeResponse.setOutputList(outputList);
        executeCodeResponse.setMessage("运行成功");
        executeCodeResponse.setStatus(0);
        JudgeInfo judgeInfo = new JudgeInfo();
        judgeInfo.setMessage("结果正确");
        judgeInfo.setTime(runMaxTime);
        judgeInfo.setMemory(0L);
        executeCodeResponse.setJudgeInfo(new JudgeInfo());
        // 代码执行完毕之后 清除文件
        if (userCodeFile.getParentFile() != null) {
        boolean del = FileUtil.del(userCodeFileParentPath);
        System.out.println("删除" + (del ? "成功" : "失败"));
        }
        } catch (Exception e) {
        e.printStackTrace();
        }
        return executeCodeResponse;
        }
    • 执行异常情况

      1. 执行超时,设置过期时间即可,或者是守护线程,时间超时自动中断
      2. 内存占用,导致空间浪费,JVM 有保护机制来处理内存溢出情况 OOM 机制 可以使用 JConsole 工具链接到 JVM 虚拟机查看
      3. 读写文件,信息泄露,通过代码程序来获取服务的配置文件,植入木马等危险操作
      4. 运行其他程序,指定目录来运行服务中的一些代码文件
      5. 执行高位操作,编写 bat 文件执行 shell 命令
    • java 程序安全控制

      1. 超时控制

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
         // 守护线程
        new Thread(() -> {
        try {
        Thread.sleep(TIME_OUT);
        System.out.println("超时了,中断");
        runProcess.destroy();
        } catch (Exception e) {
        throw new RuntimeException(e);
        }
        });
      2. 通过命令行来指定 JVM 最大堆内存,也可以使用 cgroup

        1
        java -Xmx256m
      3. 限制某些代码不可执行(字典树)
        但是无法遍历所有的黑名单

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        private static final WordTree WORD_TREE;
        private static final List<String> BLACK_LIST = Arrays.asList("Files", "exec");

        static {
        WORD_TREE = new WordTree();
        WORD_TREE.addWord(BLACK_LIST.toString());
        }

        // 审核代码
        FoundWord foundWord = WORD_TREE.matchWord(code);
        if (foundWord != null) {
        System.out.println("包含敏感词:" + foundWord);
        return null;
        }
      4. 限制权限-java 安全管理器
        直接限制用户对文件、内存、CPU、网络等资源的权限

        • Java 安全管理器的使用
          Java 安全管理器(Security Manager)是 Java 提供的保护 JVM、Java 安全的机制,可以实现更严格的资源和操作限制。
          编写安全管理器,只需要继承 SecurityManager 即可

          1. 打开所有权限

            1
            2
            3
            4
            5
            6
            7
            8
            public class DefaultSecurityManager extends SecurityManager {

            @Override
            public void checkPermission(Permission perm) {
            // 调用父级方法限制权限 不调用则做任何限制
            super.checkPermission(perm);
            }
            }
          2. 限制读权限

            1
            2
            3
            4
            @Override
            public void checkRead(String file) {
            super.checkRead(file);
            }
          3. 限制写权限
            1
            2
            3
            4
            @Override
            public void checkWrite(String file) {
            super.checkWrite(file);
            }
          4. 限制执行文件权限
            1
            2
            3
            4
            @Override
            public void checkExec(String cmd) {
            super.checkExec(cmd);
            }
          5. 限制网络链接权限
            1
            2
            3
            4
            @Override
            public void checkConnect(String host, int port) {
            super.checkConnect(host, port);
            }
          6. 执行 java 命令时需要带上安全管理器的文件路径

            1
            java -Dfile.encoding=UTF-8 -cp %s;%s -Djava.security.manager=${路径} ${运行程序}
        • Java 安全管理器的缺点
          如果要做比较严格的权限限制,需要自己去判断那些文件、包名需要允许读写。粒度太细了,权限限制的太死了难以去控制他
          安全管理器本身也是代码,也有可能存在漏洞。本质上还是程序层面的限制,没深入系统的层面。