跳至主要內容

OJ 单机版沙箱

holic-x...大约 4 分钟项目oj-platform

08-单机版沙箱

Docker沙箱改造

1.模板方法优化代码沙箱

先确认设计模式的应用场景和现有的业务场景是否匹配

模板方法概念

模版方法:定义一套通用的执行流程,让子类负责每个执行步骤的具体实现

模版方法的适用场景:适用于有规范的流程,且执行流程可以复用

作用:大幅节省重复代码量,便于项目扩展、更好维护

对比JavaNativeCodeSandbox、JavaDockerCodeSandbox的方法实现流程,基本都是遵循同样的一套思路,只不过可能每个步骤的实现细节可能有所不同

  • 抽离一套通用的执行流程
  • 能复用的尽量复用,不能复用的让子类自行进行扩展

定义一个模板方法抽象类:JavaCodeSandboxTemplate(定义一套通用的执行流程),先复制具体的实现类,把代码从完整的方法中抽离成一个子写法

1.抽象出具体的流程

public ExecuteCodeResponse executeCode(ExecuteCodeRequest executeCodeRequest) {
        List<String> inputList = executeCodeRequest.getInputList();
        String code = executeCodeRequest.getCode();
        String language = executeCodeRequest.getLanguage();

		// 1. 把用户的代码保存为文件
        File userCodeFile = saveCodeToFile(code);

		// 2. 编译代码,得到 class 文件
        ExecuteMessage compileFileExecuteMessage = compileFile(userCodeFile);
        System.out.println(compileFileExecuteMessage);

        // 3.执行代码,得到输出结果
        List<ExecuteMessage> executeMessageList = runFile(userCodeFile, inputList);

		// 4. 收集整理输出结果
        ExecuteCodeResponse outputResponse = getOutputResponse(executeMessageList);

		// 5. 文件清理
        boolean b = deleteFile(userCodeFile);
        if (!b) {
            log.error("deleteFile error, userCodeFilePath = {}", userCodeFile.getAbsolutePath());
        }
        return outputResponse;
    }

2.定义子类的具体实现

​ Java 原生代码沙箱实现,直接复用模板方法定义好的方法实现

/**
 * Java 原生代码沙箱实现(直接复用模板方法)
 */
public class JavaNativeCodeSandbox extends JavaCodeSandboxTemplate {

    @Override
    public ExecuteCodeResponse executeCode(ExecuteCodeRequest executeCodeRequest) {
        return super.executeCode(executeCodeRequest);
    }
}

​ Docker 代码沙箱实现,需要自行重写 RunFile(因为RunFile的执行方式不一样,需要重写)

public class JavaDockerCodeSandbox extends JavaCodeSandboxTemplate{


    private static final long TIME_OUT = 5000L;

    private static final Boolean FIRST_INIT = true;

    /**
     * 3、创建容器,把文件复制到容器内
     * @param userCodeFile
     * @param inputList
     * @return
     */
    @Override
    public List<ExecuteMessage> runFile(File userCodeFile, List<String> inputList) {
        ...... 业务逻辑 ......
    }
}

2.给代码沙箱提供API

​ 直接在controller层暴露接口(CondeSandbox定义的接口实现)

@PostMapping("/executeCode")
ExecuteCodeResponse executeCode(@RequestBody ExecuteCodeRequest executeCodeRequest) {
    if (executeCodeRequest == null) {
        throw new RuntimeException("请求参数为空");
    }
    return javaNativeCodeSandbox.executeCode(executeCodeRequest);
}

​ oj-platform-backend:改造远程调用代码沙箱的调用RemoteCodeSandbox

/**
 * 远程代码沙箱(实际调用接口的沙箱)
 */
public class RemoteCodeSandbox implements CodeSandbox {

    @Override
    public ExecuteCodeResponse executeCode(ExecuteCodeRequest executeCodeRequest) {
       // ----- 修改为调用远程代码沙箱接口进行响应 ------
//        System.out.println("远程代码沙箱");
//        return null;
    }
}

调用安全性改造:如果将服务不做任何的权限校验,直接发到公网,是不安全的。

1)调用方与服务提供方之间约定一个字符串**(最好加密)**

  • 优点:实现最简单,比较适合内部系统之间相互调用(相对可信的环境内部调用)
  • 缺点:不够灵活,如果 key 泄露或变更,需要重启代码

代码沙箱服务,先定义确定的字符串:

// 定义鉴权请求头和密钥
private static final String AUTH_REQUEST_HEADER = "auth";
private static final String AUTH_REQUEST_SECRET = "secretKey";

接口提供方改造:改造请求,从请求头中获取认证信息并进行校验:

@PostMapping("/executeCode")
ExecuteCodeResponse executeCode(@RequestBody ExecuteCodeRequest executeCodeRequest, HttpServletRequest request,
                                HttpServletResponse response) {
    // 基本的认证
    String authHeader = request.getHeader(AUTH_REQUEST_HEADER);
    if (!AUTH_REQUEST_SECRET.equals(authHeader)) {
        response.setStatus(403);
        return null;
    }
    if (executeCodeRequest == null) {
        throw new RuntimeException("请求参数为空");
    }
    return javaNativeCodeSandbox.executeCode(executeCodeRequest);
}

接口调用方改造:在调用的时候补充请求头信息

@Override
public ExecuteCodeResponse executeCode(ExecuteCodeRequest executeCodeRequest) {
    System.out.println("远程代码沙箱");
    String url = "http://localhost:8090/executeCode";
    String json = JSONUtil.toJsonStr(executeCodeRequest);
    String responseStr = HttpUtil.createPost(url)
            .header(AUTH_REQUEST_HEADER, AUTH_REQUEST_SECRET)
            .body(json)
            .execute()
            .body();
    if (StringUtils.isBlank(responseStr)) {
        throw new BusinessException(ErrorCode.API_REQUEST_ERROR, "executeCode remoteSandbox error, message = " + responseStr);
    }
    return JSONUtil.toBean(responseStr, ExecuteCodeResponse.class);
}

2)API签名认证

​ 给允许调用的角色分配AK、SK,然后分别校验这两组key是否匹配

3.跑通业务流程

代码沙箱测试

代码沙箱测试:通过CodeSandboxTest进行测试,注意调整application.yml中的配置(codesandbox.type)为remote配置

页面测试

​ 需确保题目设定的正确性(测试用例的正确性以及输出结果校验正确性),如果题目设定异常,则在编译执行代码的时候就会出现错误(例如数组越界异常等情况)

​ 以Main.java的测试案例为例子,输入两个数字,输出两数之和,则输入的内容应该为1 2,输出为3,然后在代码沙箱中设置断点查看数据是否正常

public class Main {
    public static void main(String[] args) {
        int a = Integer.parseInt(args[0]);
        int b = Integer.parseInt(args[1]);
        System.out.println("结果:" + (a + b));
    }
}

​ 如果此处输入的测试用例不满足规则限定,例如多输入了一些内容或者其他,则在实际代码处理的时候就会报错(例如数组越界等)

image-20240505172019935

image-20240505171242243

评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v3.1.3