在线 OJ 项目(三) · 处理项目异常 · UUID · 校验代码的安全性 · 阶段性总结

news2024/11/15 4:15:55

  • 一、处理异常
  • 二、区分不同请求的工作目录
    • UUID
    • 对 Task 类进行重构
  • 三、校验代码的安全性
  • 四、阶段性总结

书接上回,我们自己测试没问题,是因为使用了正常数据;万一用户输入的是非法的请求,该咋办?

我们需要处理异常请求,修改整个代码框架。

一、处理异常

为了防止用户输入异常 ID,我们创建 ProblemNotFoundException 异常类来处理。

为了防止用户提交有问题的代码,我们创建 CodeInValidException 异常类来处理。

统一在 catch 处理异常代码。

整理整体代码结构,去除冗余代码,最后 CompileServlet 类代码如下:

@WebServlet("/compile")
public class CompileServlet extends HttpServlet {

    static class CompileRequest {
        public int id;
        public String code;
    }
    static class CompileResponse {
        // 0 表示没问题,1 表示编译出错,2 表示运行异常,3 表示其它错误
        public int error;
        public String reason;
        public String stdout;
    }

    private ObjectMapper objectMapper = new ObjectMapper();

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    
        CompileRequest compileRequest = new CompileRequest();
        CompileResponse compileResponse = new CompileResponse();

        try {
            resp.setStatus(200);
            resp.setContentType("application/json;charset=utf8");

            // 1. 读取请求的正文
            String body = readBody(req);
            // 类对象,获取类的信息
            compileRequest = objectMapper.readValue(body, CompileRequest.class);

            // 2. 根据 id 从数据库中查找到题目的详情 - 得到测试用例代码
            ProblemDAO problemDAO = new ProblemDAO();
            Problem problem = problemDAO.selectOne(compileRequest.id);

            // 处理用户输入异常 id,导致查不到题目
            if (problem == null) {
                // 为了统一处理错误,在这个地方抛出一个异常
                throw new ProblemNotFoundException();
            }

            // testCode 是测试用例的代码
            String testCode = problem.getTestCode();
            // requestCode 是用户提交的代码
            String requestCode = compileRequest.code;

            // 3. 把用户提交的代码和测试用例代码,拼接成一个完整的代码
            String finalCode = mergeCode(requestCode, testCode);
            // 处理用户提交有问题的代码
            if (finalCode == null) {
                throw new CodeInValidException();
            }
//            System.out.println(finalCode);

            // 4. 创建一个 Task 实例,调用里面的 compileAndRun 来解析编译运行
            Task task = new Task();
            Question question = new Question();
            question.setCode(finalCode);
            Answer answer = task.compileAndRun(question);

            // 5. 根据 Task 运行的结果,包装成一个 HTTP 响应
            compileResponse.error = answer.getError();
            compileResponse.reason = answer.getReason();
            compileResponse.stdout = answer.getStdout();

        } catch (ProblemNotFoundException e) {
            // 处理题目没有找到异常
            compileResponse.error = 3;
            compileResponse.reason = "没有找到指定题目!id = " + compileRequest.id;
        } catch (CodeInValidException e) {
            // 处理用户提交的代码有问题
            compileResponse.error = 3;
            compileResponse.reason = "提交的代码不符合要求!";
        } finally {
            String respString = objectMapper.writeValueAsString(compileResponse);
            resp.getWriter().write(respString);
        }
    }



    // 拼接代码
    private static String mergeCode(String requestCode, String testCode) {
        // 1. 查找 requestCode 最后一个 }
        int pos = requestCode.lastIndexOf("}");
        if (pos == -1) {
            return null;
        }
        // 2. 截取字符串
        String substring = requestCode.substring(0, pos);
        // 3. 拼接字符串并返回
        return substring + testCode + "\n}";
    }



    // 通过请求头获取数据,转换成String 返回
    private static String readBody(HttpServletRequest req) throws UnsupportedEncodingException {
        // 1. 根据请求头里面的 ContentLength 获取到 body 的长度(单位是字节)
        int contentLength = req.getContentLength();
        // 2. 按照这个长度准备好一个 byte[]
        byte[] buffer = new byte[contentLength];
        // 3. 通过 req 里面的 getInputStream 方法,获取到 body 的流对象
        try (InputStream inputStream = req.getInputStream()) {
            // 4. 基于这个流对象,读取内容,然后把内容放到 byte[] 数字中即可
            inputStream.read(buffer);
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 5. 把这个 byte[] 的内容构造成一个 String,同时设置转换字符集格式
        return new String(buffer, "utf8");
    }
}

测试一波~

输入错误 id 能够捕捉异常。


二、区分不同请求的工作目录

问题引入

每次有一个请求过来,都需要生成一组临时文件。
如果同一时刻,有 N 个请求一起过来,这些临时文件和目录都是一样的。
此时多个请求之间就会出现 “相互干扰” 的情况(非常类似于线程安全问题)。

这三个请求,里面的题目和提交的代码都是一样的吗?都是不一样的!
因为这是来自三个不同用户的请求。
如果我们使用同一份目录里面的同一份文件,就会出现这种相互干扰的情况!

解决方法

我们需要让每个请求,都有一个自己的目录来存放这些临时文件,不会导致相互干扰。

因此,我们需要让每个请求创建的 WORK_DIR 目录都不相同!这时候就可以使用 “唯一 ID” 来作为目录的名字~

UUID

UUID 是计算机中非常常用的一个概念,表示一个 “全世界都唯一的 id”。每次生成的一个 UUID,会根据一系列算法,来保证这个 UUID 是唯一的。

每个请求,都生成一个唯一的 UUID,进一步创建一个以 UUID 命名的临时目录。最后把生成的临时文件都放在这个临时目录中即可。

对 Task 类进行重构

把开头的一组常量修改成变量。
然后创建一个构造方法,在里面生成 UUID 即可。

完整的 Task 类

// 编译运行
public class Task {
    // 通过一组常量来约定临时文件的名字
    // 表示所有临时文件所在的目录
    private  String WORK_DIR = null;
    // 约定代码的类名
    private  String CLASS = null;
    // 约定要编译的代码文件名
    private String CODE = null;
    // 约定存放编译错误信息的文件名
    private String COMPILE_ERROR = null;
    // 约定存放运行时标准输出的文件名
    private String STDOUT = null;
    // 存放运行时标准错误的文件名
    private String STDERR = null;

    public Task() {
        // 在 Java 中使用 UUID 这个类,就能够生成一个 UUID
        WORK_DIR = "./tmp/" + UUID.randomUUID().toString() + "/";
        CLASS = "Solution";
        CODE = WORK_DIR + "Solution.java";
        COMPILE_ERROR = WORK_DIR + "compileError.txt";
        STDOUT = WORK_DIR + "stdout.txt";
        STDERR = WORK_DIR + "stderr.txt";
    }

    // 此类的核心方法。
    // 参数:要编译运行的 Java 源代码;
    // 返回值:表示编译运行结果。
    public Answer compileAndRun(Question question) {
        Answer answer = new Answer();
        // 0. 准备好用来存放临时文件的目录
        File workDir = new File(WORK_DIR);
        // 判断是否存在该目录
        if (!workDir.exists()) {
            // 不存在则创建多级目录.
            workDir.mkdirs();
        }

        // 1. 把 question 中的 code 写入到一个 Solution.java 文件中
        FileUtil.writeFile(question.getCode(), CODE);

        // 2. 创建子进程,调用 javac 进行编译。编译的时候,需要有一个 .java 文件
        //      如果编译出错,javac 就会把错误信息写入到 stderr 里,使用专门的文件来保存:compileError.txt
        String compileCmd = String.format("javac -encoding utf8 %s -d %s", CODE, WORK_DIR);
        System.out.println("编译时:" + compileCmd);
        CommandUtil.run(compileCmd, null, COMPILE_ERROR);
        // 如果编译出错,错误信息就被记录到 COMPILE_ERROR 这个文件中。如果没有编译出错,该文件为空。
        String compileError = FileUtil.readFile(COMPILE_ERROR);
        if (!compileError.equals("")) {
            System.out.println("编译出错!");
            answer.setError(1);
            answer.setReason(compileError);
            return answer;
        }

        // 3. 创建子进程,调用 java 命令执行
        //      运行程序的时候,也会把 java 子进程的标准输出和标准错误获取到. stdout.txt, stderr.txt
        String runCmd = String.format("java -classpath %s %s", WORK_DIR, CLASS);
        System.out.println("运行时:" + runCmd);
        CommandUtil.run(runCmd, STDOUT, STDERR);
        String runError = FileUtil.readFile(STDERR);
        if (!runError.equals("")) {
            System.out.println("运行时错误!");
            answer.setError(2);
            answer.setReason(runError);
            return answer;
        }

        // 4. 父进程获取到刚才的编译执行结果,并打包成 compile.Answer 对象
        //      正常编译运行的结果,就通过刚才约定的文件来进行获取
        answer.setError(0);
        answer.setStdout(FileUtil.readFile(STDOUT));

        return answer;
    }

    public static void main(String[] args) {
        Task task = new Task();
        // 待编译代码
        Question question = new Question();
        question.setCode("public class Solution {\n" +
                "    public static void main(String[] args) {\n" +
                "        System.out.println(\"hello world\");\n" +
                "    }\n" +
                "}\n");
        // 编译运行后的结果
        Answer answer = task.compileAndRun(question);
        System.out.println(answer);
    }
}

单独编译运行 Task 类,我们可以从项目目录的 tmp 文件中,发现已经生成了 UUID 命名的文件。

启动 Tomcat,发现没有生成目录

是因为相对路径的原因。
IDEA 中直接运行 Task 类,这时候的工作目录就是当前 Java 项目所在的目录。
IDEA 通过 SmartTomcat 来运行 Servlet 程序,此时的工作目录就是由 SmartTomcat 控制的。不想由 SmartTomcat 控制,就可以写绝对路径。

所以,当我们使用相对路径指定文件的时候,发现文件找不到,主要是工作目录是啥我们不知道。

我们为代码添加一端监控,查看 SmartTomcat 的工作目录。

        // 查看 SmartTomcat 的工作目录
        System.out.println("用户工作目录:" + System.getProperty("user.dir"));

重新运行 Tomcat,通过 Postman 发送请求,控制台就会输出工作目录,最后能够在 tmp 文件中找到生成的 UUID 目录。


三、校验代码的安全性

当前代码还存在一个严重的安全性问题。

在线 OJ 系统需要执行一段用户提交的代码,用户提交的代码,可能是存在安全隐患的。

大家可以试试,这段代码在 leetcode 上执行看看什么结果。

有诸多问题需要防范,目前能注意的到有这些:

  1. Runtime 能够执行一个程序指令,这个比较危险。
  2. 代码中可能存在一些 “读写操作”,黑客可能直接把一个病毒程序写到你的机器上。
  3. 代码中如果存在一些 “网络” 操作,也是比较危险的。

解决方法

一个简单粗暴的方法,就是使用一个黑名单,把有危险的代码特性,都放在黑名单中。
在获取到用户提交代码的时候,就查询一个当前是否命中黑名单,如果命中黑名单就直接报错,不去编译执行。

// 黑名单
private boolean checkCodeSafe(String code) {
    List<String> blackList = new ArrayList<>();
    // 恶意代码
    blackList.add("Runtime");
    blackList.add("exec");
    // 禁止读写文件
    blackList.add("java.io");
    // 禁止访问网络
    blackList.add("java.net");

    for (String target : blackList) {
        int pos = code.indexOf(target);
        if (pos > 0) {
            return false;
        }
    }
    return true;
}

四、阶段性总结

  1. 基于多进程编程的方式,创建了一个 CommandUtil 类,来封装创建进程完成任务的工作。
  2. 创建了 Task 类,把整个编译运行过程进行了封装。
  3. 创建了数据库和数据表,设计了题目的存储方式。
  4. 封装了数据库操作(Problem 和 ProblemDAO)。
  5. 设计了前后端交互的 API。
  6. 实现了这些前后端交互的 API。

到这里,我们 online-OJ 项目的服务器后台实现的差不多了。

我们继续实现前端部分,实现 online-OJ 项目的界面。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/188146.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

内网服务器结合vxlan和iptables-snat实现内网服务器上网——筑梦之路

之前通过其他方式实现 CentOS搭建NAT和DHCP服务&#xff0c;实现共享上网_筑梦之路的博客-CSDN博客_vsphere 创建dhcp CentOS 7 firewalld实现共享上网和搭建本地yum仓库——筑梦之路_筑梦之路的博客-CSDN博客 如上图&#xff0c;有这样一种场景&#xff0c;我们经常遇到&…

【以音视频开发进阶指南为基础】音视频基础概念

一、数字音频 数字音频可以粗糙的理解为将本质是模拟信号的声音音频数字化&#xff0c;那么如何数字化呢&#xff0c;需要经过三个步骤&#xff1a;采样、量化和编码。 采样&#xff1a;将声音的振动信息转化为电信号&#xff0c;再对电信号进行放大处理得到声音的模拟信号&a…

POE交换机常见问题解答

POE交换机通过网线供电的方式为标准的POE终端设备供电&#xff0c;可以免去额外的电源布线&#xff0c;在为一些基于IP的终端&#xff08;如IP电话机、无线局域网接入点AP、网络摄像机等&#xff09;传输数据信号的同时&#xff0c;还能为此类设备提供直流供电的技术。POE技术能…

Windows10安装java环境

Windows10安装java环境 文章目录Windows10安装java环境下载解压配置下载 Java8 https://www.oracle.com/java/technologies/downloads/#java8-windows Java11 https://www.oracle.com/java/technologies/downloads/#java11-windows Java17 https://www.oracle.com/java/techno…

12.日期与时间

目录 一.Date类 1.1 什么是Date类 1.2 Date类的构造器和常用方法 1.2.1 常用构造器 1.2.2 常用方法 二.SimpleDateFormat 2.1 什么是SimpleDateFormat 2.2 SimpleDateFormat的作用 2.3 SimpleDateFormat的构造器和方法 2.3.1 构造器 2.3.2 方法 2.3.3 例子 2.4 Sim…

[论文笔记] XRP: In-Kernel Storage Functions with eBPF

XRP: In-Kernel Storage Functions with eBPF XRP: 利用 eBPF 的核内存储函数 [Paper] [Slides] [Code] OSDI’22 摘要 提出了 XRP, 一个允许应用程序从 NVMe 驱动程序中的 eBPF 钩子(hook)执行用户定义的存储函数(例如索引查找或聚合)的框架, 可以安全地绕过大部分内核的存…

中国电子学会2022年09月份青少年软件编程Python等级考试试卷一级真题(含答案)

分数&#xff1a;100 题数&#xff1a;37 测试时长&#xff1a;60min 一、单选题(共25题&#xff0c;共50分) 1.表达式len("学史明理增信 &#xff0c;读史终生受益") > len(" reading history will benefit you ")的结果是&#xff1f;&#xff08;…

vue3全局组件自动注册

前言&#xff1a;本文主要讲述vue3的全局公共组件的自动注册第一步&#xff1a;建文件需要在src/components 下创建一个文件夹用于存放封装的公共组件&#xff08;这里我起名叫coms&#xff09;需要在src/components 下创建一个js文件用于存放引入组件并注册&#xff08;这里我…

前端经典面试500题【下】

面试 一、vue 说说vue动态权限绑定渲染列表&#xff08;权限列表渲染&#xff09; 首先请求服务器,获取当前用户的权限数据,比如请求 this.$http.get(“rights/list”); 获取到权限数据之后,在列表中使用v-if v-if-else的组合来展示不同的内容 <template><div>&…

JavaScript基础复盘3

流程控制 在一个程序执行的过程中&#xff0c;各条代码的执行顺序怼程序的结果是有直接影响的。很多时候我们药通过控制代码的执行顺序来实现我们药完成的功能。 简单理解&#xff1a;流程控制就是来控制我们的代码按照什么结构顺序来执行 JavaScript作为一门程序语言&#x…

JDBC-Mysql数据库连接池

传统数据库连接的弊端 一个java程序多个正常要开启多个mysql连接-正常开发中次数在1w次往上&#xff08;还可能有多个java程序-并发编程&#xff09;&#xff0c;但是如果很多歌连接的话&#xff0c;数据库可能就瘫痪了 测试一个程序连接5000次 抛出一个&#xff0c;太多连接的…

离校毕业生刚去新的陌生城市,需要提高警惕的几点

很多大学毕业生其实阅历还不是特别的多&#xff0c;从小在自己熟悉的环境长大&#xff0c;即便是读了大学&#xff0c;可能也是在自己熟悉的学校附近活动&#xff0c;同学们集体出去逛个街&#xff0c;放假了就回家了。 但一旦大学毕业要离校&#xff0c;可能你会去一个陌生的新…

论文笔记:Learning Disentangled Representations of Video with Missing Data

2020 Neurips 1 intro & abstract 视频表征的一个挑战是高维、动态、各个像素之间多模态分布 最近的一些研究通过探索视频的inductive bias&#xff0c;并将高维数据映射到低微数据中—>这种方法通过将视频的各帧分解成语义上有意义的因子&#xff0c;来获得视频的解耦…

Maven初级(二)

目录 四.第一个Maven项目&#xff08;手工制作&#xff09; 4.1 Maven工程目录结构 4.2 手工制作Maven项目的步骤 4.2.1 手工制作项目的目录结构或使用插件创建项目的目录结构 4.2.2 在项目的src同层目录下创建pom.xml文件 4.2.3 在cmd中使用Maven项目构建命令 四.第一个…

自动驾驶感知——物体检测与跟踪算法|4D毫米波雷达

文章目录1. 物体检测与跟踪算法1.1 DBSCAN1.2 卡尔曼滤波2. 毫米波雷达公开数据库的未来发展方向3. 4D毫米波雷达特点及发展趋势3.1 4D毫米波雷达特点3.1.1 FMCW雷达角度分辨率3.1.2 MIMO ( Multiple Input Multiple Output)技术3.2 4D毫米波雷达发展趋势3.2.1 芯片级联3.2.2 专…

[docker]-docker安装prometheus和grafana

导语&#xff1a;需要排查部分出问题的私有化环境。直接上一个docker的node_exporter prometheus grafana 最方便。 所用到的安装包在最下面的云盘。 解压安装自定义node_exporter # 这个abc的包名可以忽略 tar zxvf abc.tgz cd abc cp node_exporter /usr/local/ cat <…

Mybatis 动态sql的编写|开启二级缓存

❤️作者主页&#xff1a;微凉秋意 ✅作者简介&#xff1a;后端领域优质创作者&#x1f3c6;&#xff0c;CSDN内容合伙人&#x1f3c6;&#xff0c;阿里云专家博主&#x1f3c6; ✨精品专栏&#xff1a;数据结构与课程设计 &#x1f525;系列专栏&#xff1a;javaweb 文章目录前…

Linux-主要目录

/: 根目录&#xff0c;一般根目录下只存放目录&#xff0c;在Linux下有且只有一个根目录&#xff0c;所有的东西都是从这里开始当在终端里输入 /home,其实是在告诉电脑&#xff0c;先从/&#xff08;根目录&#xff09;开始&#xff0c;再进入到home目录/bin、/usr/bin:可执行二…

java基于ssm的旅游景点门票预订网站

通过本系统&#xff0c;能够实现用户进行登录后&#xff0c;在网站上对旅游景点信息进行门票预订&#xff0c;也可以通过在线留言系统&#xff0c;了解景点相关信息&#xff0c;为用户提供全方位的服务&#xff0c;同时为节省用户的时间&#xff0c;本系统采用分地名的方式&…

2022尚硅谷SSM框架跟学(七)Spring MVC基础二

2022尚硅谷SSM框架跟学 七 Spring MVC基础二4.SpringMVC获取请求参数4.1.通过ServletAPI获取4.2.通过控制器方法的形参获取请求参数4.3.RequestParam4.4.RequestHeader4.5.CookieValue4.6.通过POJO获取请求参数4.7.解决获取请求参数的乱码问题5.域对象共享数据5.1.使用ServletA…