Selaa lähdekoodia

feat: 虚拟摄像头和远程桌面程序黑名单配置功能。

chenhao 2 vuotta sitten
vanhempi
commit
9be1f1a847

+ 2 - 4
src/constants/constants.ts

@@ -12,10 +12,8 @@ export const WEEKDAY_NAMES: Record<number, string> = {
 const env = import.meta.env;
 export const VITE_SLS_STORE_NAME = env.VITE_SLS_STORE_NAME as string;
 // export const VITE_CONFIG_FILE_SEVER_URL = env.VITE_CONFIG_FILE_SEVER_URL;
-// 将config server与域名绑定
-export const VITE_CONFIG_FILE_SEVER_URL = window.location.hostname.includes(
-  "exam-cloud.cn"
-)
+// 将config server与域名绑定 (为了让构建出来的包在测试和生产环境通用, 弃用环境变量配置)
+export const VITE_CONFIG_FILE_SEVER_URL = window.location.hostname.includes("exam-cloud.cn")
   ? "https://ecs-static.qmth.com.cn"
   : "https://ecs-test-static.qmth.com.cn";
 const modeStr = env.MODE !== "production" ? env.MODE + "-" : "";

+ 1 - 0
src/features/OnlineExam/Examing/ExamingHome.vue

@@ -132,6 +132,7 @@ function onCompareResult({ hasError, fileName }: CompareResult) {
 //#endregion 人脸抓拍与活体检测
 
 onUnmounted(() => {
+  /** TODO: 摄像头关闭条件需要跟考试信息解耦 */
   if (store.exam.faceCheckEnabled) {
     // debug级别备选。因为初期上线,摄像头比较容易出错,所以保留此日志
     logger({

+ 2 - 0
src/features/OnlineExam/StartExamModal.vue

@@ -218,6 +218,7 @@ function disagreeCommittment() {
 let passedAllChecks = false;
 async function enterExam() {
   let hstate = { examStudentId: course.examStudentId };
+  /** 为了代码加密混淆效果更好,隐藏在线考试开始考试的启动逻辑,将在线考试启动接口调用放在该组件内部 */
   if (course.examType === "ONLINE") {
     const timestamp = Date.now();
     const rawStr = JSON.stringify({
@@ -294,6 +295,7 @@ async function enterExam() {
 }
 
 onUnmounted(() => {
+  /** TODO: 摄像头关闭时机,需要完善 */
   if (!passedAllChecks && course.faceEnable) {
     // debug级别备选。因为初期上线,摄像头比较容易出错,所以保留此日志
     logger({

+ 3 - 2
src/features/UserLogin/useRemoteAppChecker.tsx

@@ -1,13 +1,14 @@
-import { REMOTE_APP_NAME } from "@/constants/constant-namelist";
 import { store } from "@/store/store";
 import {
   execLocal,
   fileExists,
   nodeCheckRemoteDesktop,
 } from "@/utils/nativeMethods";
+import { getBlackAppConfig } from "@/utils/common";
 import { watch } from "vue";
 
 export function useRemoteAppChecker() {
+  void getBlackAppConfig();
   /** 检测出错则提示;检测通过则将禁用登录按钮的flag置为false */
   async function checkRemoteAppTxt() {
     if (
@@ -133,7 +134,7 @@ export function useRemoteAppChecker() {
       let exe = "Project1.exe";
       try {
         if (fileExists("Project2.exe")) {
-          const remoteAppName = REMOTE_APP_NAME;
+          const { remoteApp: remoteAppName } = await getBlackAppConfig();
           exe = `Project2.exe "${remoteAppName}" `;
         }
 

+ 3 - 1
src/features/UserLogin/useVCamChecker.tsx

@@ -1,9 +1,10 @@
-import { VCAM_LIST } from "@/constants/constant-namelist";
 import { store } from "@/store/store";
 import { execLocal, fileExists } from "@/utils/nativeMethods";
+import { getBlackAppConfig } from "@/utils/common";
 import { watch } from "vue";
 
 export function useVCamChecker() {
+  void getBlackAppConfig();
   /** 检测出错则提示;检测通过则将禁用登录按钮的flag置为false */
   async function checkVCamTxt() {
     let cameraInfo;
@@ -37,6 +38,7 @@ export function useVCamChecker() {
       return;
     }
     let found = false;
+    const { vCamList: VCAM_LIST } = await getBlackAppConfig();
     if (cameraInfo && cameraInfo.trim()) {
       for (const vc of VCAM_LIST) {
         if (cameraInfo.toUpperCase().includes(vc.toUpperCase())) {

+ 1 - 1
src/types/student-client.d.ts

@@ -253,7 +253,7 @@ export type OnlineExam = BaseExam &
     undertaking: string;
     /** 是否启用人脸比对 */
     faceEnable: boolean;
-    /** 是否启用人脸比对的强制或非强制 */
+    /** 是否强制启用人脸比对 */
     faceCheck: boolean;
     /** 剩余考试次数 */
     allowExamCount: number;

+ 125 - 0
src/utils/common.ts

@@ -0,0 +1,125 @@
+import { httpNoAuth } from "@/plugins/axiosNoAuth";
+import { VITE_CONFIG_FILE_SEVER_URL } from "@/constants/constants";
+import { VCAM_LIST, REMOTE_APP_NAME } from "@/constants/constant-namelist";
+import { decryptB } from "@/utils/utils";
+
+interface ParsedBlackAppConfig {
+  vCamList: string[];
+  remoteApp: string[];
+}
+
+interface RawBlackAppConfig {
+  vCamList: string;
+  remoteApp: string;
+}
+
+interface MergedBlackAppConfig {
+  vCamList: string[];
+  remoteApp: string;
+}
+
+interface GetBlackAppConfig {
+  (): Promise<MergedBlackAppConfig>;
+  fetch?: Promise<MergedBlackAppConfig>;
+}
+
+/**
+ * @description 获取配置的虚拟摄像头和远程软件黑名单
+ */
+export let getBlackAppConfig: GetBlackAppConfig = () => {
+  if (getBlackAppConfig.fetch) {
+    return getBlackAppConfig.fetch;
+  }
+  const fetch = httpNoAuth
+    .get<string>(`${VITE_CONFIG_FILE_SEVER_URL}/oe_client/software.json`, {
+      "axios-retry": { retries: 3 },
+      noErrorMessage: true,
+    })
+    .then((response) => response.data)
+    .catch(() => {
+      getBlackAppConfig.fetch = void 0;
+      return "";
+    })
+    .then(parseBlackAppConfig)
+    .then(mergeBlackAppConfig)
+    .then((result) => {
+      if (getBlackAppConfig.fetch) {
+        getBlackAppConfig = () => Promise.resolve(result);
+      }
+      return result;
+    });
+  getBlackAppConfig.fetch = fetch;
+  return fetch;
+};
+
+function parseBlackAppConfig(str: string): ParsedBlackAppConfig {
+  const result: ParsedBlackAppConfig = {
+    vCamList: [],
+    remoteApp: [],
+  };
+  try {
+    if (typeof str === "string") {
+      const s = str.slice(0, -32).split("").reverse().join("");
+      if (s.length) {
+        try {
+          const ret = decryptB(s);
+          if (ret) {
+            try {
+              const parsed: RawBlackAppConfig = JSON.parse(ret);
+              result.vCamList = processEditText(parsed.vCamList);
+              result.remoteApp = processEditText(parsed.remoteApp);
+            } catch (error) {
+              console.warn("config json parse error!", error);
+            }
+          }
+        } catch (error) {
+          console.warn("base64 decode error!", error);
+        }
+      }
+    } else {
+      console.warn("参数类型错误!");
+    }
+  } catch (error) {
+    console.warn("parseBlackAppConfig error", error);
+  }
+  return result;
+}
+
+function isString(s: any): s is string {
+  return typeof s === "string";
+}
+
+/** 换行符分割 */
+function processEditText(text: string) {
+  return isString(text)
+    ? text
+        .split(/\r|\n/g)
+        .map((s) => s.trim())
+        .filter(Boolean)
+    : [];
+}
+
+function mergeBlackAppConfig(
+  remoteConfig: ParsedBlackAppConfig
+): MergedBlackAppConfig {
+  const result: MergedBlackAppConfig = {
+    vCamList: [],
+    remoteApp: "",
+  };
+  try {
+    if (
+      remoteConfig &&
+      typeof remoteConfig === "object" &&
+      remoteConfig.vCamList
+    ) {
+      const { vCamList, remoteApp } = remoteConfig;
+      result.vCamList = [...new Set([...vCamList, ...VCAM_LIST])];
+      result.remoteApp = [
+        ...new Set(REMOTE_APP_NAME.split(";").concat(...remoteApp)),
+      ].join(";");
+    }
+  } catch (error) {
+    console.warn("processBlackAppConfig error", error);
+  }
+  return result;
+}