OJ在线评测系统 将代码沙箱开放为API 跑通前端后端整个项目 请求对接口
代码沙箱开放API
这一步非常简单
就是提供公共方法
引入代码沙箱的具体实现
/*** 执行代码** @param executeCodeRequest* @return*/@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);}
现在我们想想看我们之前的后端是怎么做的
我们写了一个示例
用了个策略模式
只是为了跑通流程
现在我们要把接口放进来
我们要补全远程代码沙箱里面的代码
定义新的错误枚举类型
API_REQUEST_ERROR( 50010, "接口调用失败");
以便于我们抛出异常
if(StringUtils.isBlank(responseStr)){throw new BusinessException(API_REQUEST_ERROR, "executeCode remoteSandbox error, message = {}"+responseStr);}
我们用hutool包进行数据封装
进入配置里面 此时我们就能修改
接下来我们进行测试
此时这个API是不安全的
接口调用是不安全的
调用安全性
如果将服务不做任何权限校验 直接发到公网 是不安全的
怎么提高安全性呢
调用方和服务提供方之间约定一个字符串 只是在服务器内部传递
这个字符串最好是加密的字符串
约定好一个字符串
如果调用方的请求头和请求秘钥和接口定义的不一致 就不给予调用
优点:简单 比较适合内部系统之间的相互调用 相对可行的环境内部调用
缺点:不够灵活 如果key泄露或者是变更 得去修改代码
调用方 在调用的时候补充请求头
package com.yupi.yuojcodesandbox.controller;import com.yupi.yuojcodesandbox.JavaNativeCodeSandbox;
import com.yupi.yuojcodesandbox.model.ExecuteCodeRequest;
import com.yupi.yuojcodesandbox.model.ExecuteCodeResponse;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;@RestController("/")
public class MainController {// 定义鉴权请求头和密钥private static final String AUTH_REQUEST_HEADER = "auth";private static final String AUTH_REQUEST_SECRET = "secretKey";@Resourceprivate JavaNativeCodeSandbox javaNativeCodeSandbox;@GetMapping("/health")public String healthCheck() {return "ok";}/*** 执行代码** @param executeCodeRequest* @return*/@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);}
}
我们也可以开放API签名认证
给允许调用的人员分配accessKey secretKey
然后校验这两组key是否匹配
跑通整个项目流程
前端后端 一起工作
远程代码沙箱
后端
前端
接下来我们就要尝试去跑通整个项目流程
写一个前端流程
写一个题目提交页面
<template><div id="questionSubmitView"><a-form :model="searchParams" layout="inline"><a-form-item field="questionId" label="题号" style="min-width: 240px"><a-input v-model="searchParams.questionId" placeholder="请输入" /></a-form-item><a-form-item field="language" label="编程语言" style="min-width: 240px"><a-selectv-model="searchParams.language":style="{ width: '320px' }"placeholder="选择编程语言"><a-option>java</a-option><a-option>cpp</a-option><a-option>go</a-option><a-option>html</a-option></a-select></a-form-item><a-form-item><a-button type="primary" @click="doSubmit">搜索</a-button></a-form-item></a-form><a-divider size="0" /><a-table:ref="tableRef":columns="columns":data="dataList":pagination="{showTotal: true,pageSize: searchParams.pageSize,current: searchParams.current,total,}"@page-change="onPageChange"><template #judgeInfo="{ record }">{{ JSON.stringify(record.judgeInfo) }}</template><template #createTime="{ record }">{{ moment(record.createTime).format("YYYY-MM-DD") }}</template></a-table></div>
</template><script setup lang="ts">
import { onMounted, ref, watchEffect } from "vue";
import {Question,QuestionControllerService,QuestionSubmitQueryRequest,
} from "../../../generated";
import message from "@arco-design/web-vue/es/message";
import { useRouter } from "vue-router";
import moment from "moment";const tableRef = ref();const dataList = ref([]);
const total = ref(0);
const searchParams = ref<QuestionSubmitQueryRequest>({questionId: undefined,language: undefined,pageSize: 10,current: 1,
});const loadData = async () => {const res = await QuestionControllerService.listQuestionSubmitByPageUsingPost({...searchParams.value,sortField: "createTime",sortOrder: "descend",});if (res.code === 0) {dataList.value = res.data.records;total.value = res.data.total;} else {message.error("加载失败," + res.message);}
};/*** 监听 searchParams 变量,改变时触发页面的重新加载*/
watchEffect(() => {loadData();
});/*** 页面加载时,请求数据*/
onMounted(() => {loadData();
});const columns = [{title: "提交号",dataIndex: "id",},{title: "编程语言",dataIndex: "language",},{title: "判题信息",slotName: "judgeInfo",},{title: "判题状态",dataIndex: "status",},{title: "题目 id",dataIndex: "questionId",},{title: "提交者 id",dataIndex: "userId",},{title: "创建时间",slotName: "createTime",},
];const onPageChange = (page: number) => {searchParams.value = {...searchParams.value,current: page,};
};const router = useRouter();/*** 跳转到做题页面* @param question*/
const toQuestionPage = (question: Question) => {router.push({path: `/view/question/${question.id}`,});
};/*** 确认搜索,重新加载数据*/
const doSubmit = () => {// 这里需要重置搜索页号searchParams.value = {...searchParams.value,current: 1,};
};
</script><style scoped>
#questionSubmitView {max-width: 1280px;margin: 0 auto;
}
</style>
这段代码定义了一个异步函数 loadData
,用于加载分页问题提交的数据。它调用 QuestionControllerService
的 listQuestionSubmitByPageUsingPost
方法,传入的参数是 searchParams.value
的内容,并添加了排序字段和顺序。如果返回结果的 code
为 0,说明请求成功,则将返回的记录赋值给 dataList
,同时更新总数 total
;如果请求失败,则显示错误信息。
这是对象展开运算符(spread operator)的写法,...searchParams.value
将 searchParams.value
对象的所有属性展开,并与后面的 sortField
和 sortOrder
一起创建一个新对象。这种写法可以方便地合并对象属性。