Просмотр исходного кода

登录页-加密日志&补充日志&屏幕截图

Michael Wang 3 лет назад
Родитель
Сommit
b9c65089cc

+ 7 - 1
src/App.vue

@@ -6,9 +6,10 @@ import {
   useMessage,
 } from "naive-ui";
 import { zhCN, dateZhCN } from "naive-ui";
-import { defineComponent, watchEffect } from "vue";
+import { defineComponent, onMounted, watchEffect } from "vue";
 import { useStore, setStore, store } from "./store/store";
 import themeOverrides from "./themeOverrides";
+import { initScreenShot, registerOnResize } from "./utils/nativeMethods";
 
 setStore(useStore());
 
@@ -26,6 +27,11 @@ const DummyComp = defineComponent({
   },
 });
 
+onMounted(() => {
+  registerOnResize();
+  initScreenShot();
+});
+
 watchEffect(() => {
   const bodyScrollProp = spinning ? "hidden" : "auto";
   document.body.style.overflow = bodyScrollProp;

+ 29 - 0
src/api/login.ts

@@ -1,4 +1,5 @@
 import { httpApp } from "@/plugins/axiosApp";
+import { httpNoAuth } from "@/plugins/axiosNoAuth";
 
 /** 用户登录 */
 export async function loginApi(
@@ -40,3 +41,31 @@ export async function getStudentSpecialtyNameListApi() {
     { setGlobalMask: true, noErrorMessage: true, "axios-retry": { retries: 3 } }
   );
 }
+
+/** 获得云存储地址签名 */
+export async function getCapturePhotoYunSign(signParams: object) {
+  return httpApp.get<{
+    formUrl: string;
+    accessUrl: string;
+    formParams: object;
+  }>("/api/ecs_oe_student/examControl/getCapturePhotoYunSign", {
+    params: signParams,
+  });
+}
+
+/** 保存图片到云存储 */
+export async function saveCapturePhoto(
+  formUrl: string,
+  formParams: object,
+  payload: object
+) {
+  const myFormData = new FormData();
+  for (const [k, v] of Object.entries(formParams)) {
+    myFormData.append(k, v as string);
+  }
+  for (const [k, v] of Object.entries(payload)) {
+    myFormData.append(k, v as Blob);
+  }
+
+  return httpNoAuth.post(formUrl, myFormData);
+}

+ 74 - 4
src/features/UserLogin/UserLogin.vue

@@ -11,7 +11,9 @@ import {
 } from "@/constants/constants";
 import { useTimers } from "@/setups/useTimers";
 import { store } from "@/store/store";
-import { createUserDetailLog } from "@/utils/logger";
+import { createEncryptLog, createUserDetailLog } from "@/utils/logger";
+import { getScreenShot, isElectron } from "@/utils/nativeMethods";
+import ua from "@/utils/ua";
 import { CloseCircleOutline, LockClosed, Person } from "@vicons/ionicons5";
 import { FormItemInst, FormRules } from "naive-ui";
 import { onMounted, watch } from "vue";
@@ -28,13 +30,45 @@ import { useNewVersion } from "./useNewVersion";
 import { useRemoteAppChecker } from "./useRemoteAppChecker";
 import { useVCamChecker } from "./useVCamChecker";
 
+const { addTimeout, addInterval } = useTimers();
+
+//#region 登录日志处理
 logger({
   cnl: ["console", "local", "server"],
   pgn: "登录页面",
   act: "首次渲染",
 });
 
+if (isElectron()) {
+  logger({
+    cnl: ["server"],
+    pgn: "登录页面",
+    act: "versonstats",
+    ext: {
+      packageVersion: "ua-" + ua.getBrowser().version,
+      file: eval(`process.env.PORTABLE_EXECUTABLE_FILE`),
+      uaGood:
+        "uagood-" + (eval(`process.env.PORTABLE_EXECUTABLE_FILE`) ? 1 : 0),
+    },
+  });
+}
+
+// @ts-expect-error rtt不应该存在chrome 61以下,此处是陷阱代码
+if (navigator.connection && navigator.connection.rtt) {
+  logger({
+    cnl: ["local", "server"],
+    pgn: "登录页面",
+    // 故意用特殊的空格字符
+    act: "page created",
+    ext: { UA: navigator.userAgent },
+  });
+}
+
+// 上传本机加密日志
+addInterval(createEncryptLog, 5 * 1000);
+
 useExamShellStats();
+//#endregion
 
 //#region cache faceapi json
 {
@@ -58,8 +92,6 @@ useExamShellStats();
 }
 //#endregion
 
-const { addTimeout } = useTimers();
-
 let isGeeTestEnabled = $ref(false);
 onMounted(async () => {
   const conf = await getElectronConfig();
@@ -102,6 +134,9 @@ let disableLoginBtn = $computed(
 
 //#region form校验
 const domain = DOMAIN;
+if (!domain?.includes(".ecs.qmth.com.cn")) {
+  $message.warning("学校域名不正确", { closable: false, duration: 360000 });
+}
 type FormModel = {
   accountType: "STUDENT_CODE" | "IDENTITY_NUMBER";
   accountValue: string;
@@ -133,6 +168,13 @@ const fromRules: FormRules = {
 
 let errorInfo = $ref("");
 watch([formValue], () => (errorInfo = ""));
+watch(
+  () => formValue.accountType,
+  () => {
+    formValue.accountValue = localStorage.getItem(formValue.accountType) || "";
+  },
+  { immediate: true }
+);
 //#endregion
 
 //#region 极验
@@ -158,8 +200,8 @@ watch(
 );
 //#endregion
 
+//#region 登录处理
 const router = useRouter();
-
 let loginBtnLoading = $ref(false);
 let disableLoginBtnBecauseNotTimeout = $ref(false);
 async function loginForuser() {
@@ -215,6 +257,18 @@ async function loginForuser() {
       geeParams
     );
 
+    logger({
+      cnl: ["server"],
+      pgu: "AUTO",
+      act: "login api success",
+      ext: {
+        accountType: formValue.accountType,
+        accountValue: formValue.accountValue,
+        domain,
+        ...geeParams,
+      },
+    });
+
     if (res.data.code === "200") {
       errorInfo = "";
       // 准备下面的登录token
@@ -240,6 +294,8 @@ async function loginForuser() {
 
 /** 登录成功后,取学生信息和跳转 */
 async function afterLogin(loginRes: any) {
+  // 存储登录成功的用户名
+  localStorage.setItem(formValue.accountType, formValue.accountValue);
   try {
     const [{ data: student }, { data: specialty }] = await Promise.all([
       getStudentInfoBySessionApi(),
@@ -255,6 +311,19 @@ async function afterLogin(loginRes: any) {
     store.user = user;
     createUserDetailLog();
 
+    getScreenShot({ cause: "ss-login" }).catch((e) => {
+      logger({
+        pgu: "AUTO",
+        cnl: ["server"],
+        dtl: "桌面抓拍失败-electron问题",
+        ejn: JSON.stringify(e),
+      });
+    });
+
+    let userIds: number[] = JSON.parse(localStorage.getItem("userIds") || "[]");
+    userIds = [...new Set(userIds).add(store.user.id)];
+    localStorage.setItem("userIds", JSON.stringify(userIds));
+
     // 有断点或者异常,停止后续处理
     if (await checkExamInProgress().catch(() => true)) return;
 
@@ -271,6 +340,7 @@ async function afterLogin(loginRes: any) {
     });
   }
 }
+//#endregion
 
 function closeApp() {
   logger({

+ 9 - 0
src/features/UserLogin/useExamShellStats.ts

@@ -1,4 +1,13 @@
 export function useExamShellStats() {
+  if (
+    navigator.userAgent.indexOf("WOW64") != -1 ||
+    navigator.userAgent.indexOf("Win64") != -1
+  ) {
+    _hmt.push(["_trackEvent", "登录页面", "64Bit OS"]);
+  } else {
+    _hmt.push(["_trackEvent", "登录页面", "非64Bit OS"]);
+  }
+
   const shellVersion = window.navigator.userAgent
     .split(" ")
     .find((v) => v.startsWith("electron-exam-shell/"));

+ 10 - 3
src/features/UserLogin/useNewVersion.ts

@@ -52,11 +52,18 @@ export function useNewVersion() {
     newVersionAvailable = true;
   };
 
-  onMounted(() =>
+  onMounted(async () => {
     document.addEventListener("__newSWAvailable", checkNewVersionListener, {
       once: true,
-    })
-  );
+    });
+    if (localStorage.getItem("__swReload")) {
+      localStorage.removeItem("__swReload");
+      $message.info("正在更新版本...");
+      // disableLoginBtnBecauseRefreshServiceWorker = true;
+      await new Promise((resolve) => setTimeout(resolve, 2000));
+      location.reload();
+    }
+  });
 
   onUnmounted(() =>
     document.removeEventListener("__newSWAvailable", checkNewVersionListener)

+ 42 - 0
src/utils/logger.ts

@@ -5,6 +5,7 @@ import SlsWebLogger from "js-sls-logger";
 import { VITE_SLS_STORE_NAME } from "@/constants/constants";
 import { electronLog } from "./electronLog";
 import getDeviceInfos from "./deviceInfo";
+import { isElectron } from "./nativeMethods";
 
 const aliLogger = new SlsWebLogger({
   host: "cn-shenzhen.log.aliyuncs.com",
@@ -108,3 +109,44 @@ export function createUserDetailLog() {
 
   createLog({ cnl: ["server"], pgn: "登录页面", act: "登录成功日志", ext });
 }
+
+export function createEncryptLog() {
+  // 非 electron 返回
+  if (!isElectron()) return;
+
+  try {
+    const uuidForEcs = localStorage.getItem("uuidForEcs");
+    // 没有 uuidForEcs 日志没法查询
+    if (!uuidForEcs) return;
+
+    let log = null;
+    let lastLogIndex: number = +(localStorage.getItem("lastLogIndex") || 0);
+    log = window.nodeRequire("electron-log");
+    // const filePath = log.getFile().path;
+    // eslint-disable-next-line @typescript-eslint/no-unsafe-call
+    const filePath: string = log.transports.file.findLogPath();
+    const fs: typeof import("fs") = window.nodeRequire("fs");
+    const content = fs.readFileSync(filePath, "utf-8");
+    const ary = content.toString().split("\r\n").join("\n").split("\n");
+    // 重复上传没有时间的行:跨过非时间戳的行; 错误识别:全部重新执行
+    // let lastIndex = ary.findIndex(v => v === lastLog);
+    // console.log({ lastIndex });
+    if (ary.length < lastLogIndex) {
+      lastLogIndex = 0;
+    }
+
+    const logLen = 10;
+    const newAry = ary
+      .slice(lastLogIndex, lastLogIndex + logLen)
+      .filter((v) => v);
+    // 如果没有上传的内容,则不修改lastLog, 也不上传
+    if (!newAry.length) return;
+    lastLogIndex = lastLogIndex + newAry.length;
+
+    localStorage.setItem("lastLogIndex", lastLogIndex + "");
+    createLog({ cnl: ["server"], ext: { encryptLog: newAry.join("\n") } });
+  } catch (error) {
+    console.debug(error);
+    return;
+  }
+}

+ 125 - 1
src/utils/nativeMethods.ts

@@ -1,5 +1,6 @@
+import { getCapturePhotoYunSign, saveCapturePhoto } from "@/api/login";
 import { HOST_FILE_HASH_MAP } from "@/constants/constants";
-import { xor } from "lodash-es";
+import { throttle, xor } from "lodash-es";
 
 export function isElectron() {
   return typeof window.nodeRequire != "undefined";
@@ -211,3 +212,126 @@ export function checkMainExe(): boolean {
   }
   return false;
 }
+
+/** 初始化桌面抓拍 */
+export function initScreenShot() {
+  if (import.meta.env.DEV) return;
+  // eslint-disable-next-line @typescript-eslint/no-unsafe-call
+  window
+    .nodeRequire("electron")
+    .desktopCapturer.getSources(
+      { types: ["window", "screen"] },
+      async (e: any, sources: { id: string; name: string }[]) => {
+        console.log(e, sources);
+        for (const source of sources) {
+          console.log(source);
+          if (source.name === "Entire screen") {
+            try {
+              const stream = await navigator.mediaDevices.getUserMedia({
+                audio: false,
+                video: {
+                  // @ts-expect-error 不确定是chrome/electron标准是否不一样,需要测试
+                  mandatory: {
+                    chromeMediaSource: "desktop",
+                    chromeMediaSourceId: source.id,
+                    minWidth: 600,
+                    maxWidth: 600,
+                    minHeight: 480,
+                    maxHeight: 480,
+                  },
+                },
+              });
+              handleStream(stream);
+            } catch (err) {
+              logger({
+                cnl: ["local", "server"],
+                pgu: "AUTO",
+                act: "ss-failed",
+                ejn: JSON.stringify(err),
+              });
+            }
+            return;
+          }
+        }
+      }
+    );
+  function handleStream(stream: MediaStream) {
+    const video: HTMLVideoElement = document.querySelector("#ssVideo")!;
+    video.srcObject = stream;
+    video.onloadedmetadata = () => video.play();
+  }
+}
+
+/** 保存当前屏幕截图 */
+export async function getScreenShot({ cause = "ss-none" }) {
+  const video: HTMLVideoElement = document.querySelector("#ssVideo")!;
+  async function getSnapShot() {
+    return new Promise((resolve, reject) => {
+      if (video.readyState !== 4 || !(<MediaStream>video.srcObject)?.active) {
+        reject("desktop没有正常启用");
+      }
+      video.pause();
+      const canvas = document.createElement("canvas");
+      canvas.width = 220;
+      canvas.height = 165;
+      const context = canvas.getContext("2d");
+      context?.drawImage(video, 0, 0, 220, 165);
+      canvas.toBlob(resolve, "image/png", 0.95);
+    });
+  }
+  if (window.location.pathname.includes("/login")) return;
+  const captureBlob = await getSnapShot();
+  const res = await getCapturePhotoYunSign({ fileSuffix: "png" });
+  try {
+    await saveCapturePhoto(res.data.formUrl, res.data.formParams, {
+      file: captureBlob,
+    });
+    logger({
+      pgu: "AUTO",
+      cnl: ["server"],
+      dtl: "桌面抓拍保存成功",
+      ext: {
+        resultUrl: res.data.accessUrl,
+        cause,
+      },
+    });
+  } catch (error) {
+    console.log(error);
+    logger({
+      pgu: "AUTO",
+      cnl: ["server"],
+      dtl: "桌面抓拍失败",
+      ejn: JSON.stringify(error),
+      ext: {
+        cause,
+      },
+    });
+  } finally {
+    video && video.play();
+  }
+}
+
+/** 在app resize时触发 */
+export function registerOnResize() {
+  const throttledResizeLog = throttle(() => {
+    logger({
+      cnl: ["local", "server"],
+      pgu: "AUTO",
+      act: "registerOnResize",
+      ext: {
+        width: window.screen.width,
+        height: window.screen.height,
+        screenX: window.screen.availWidth,
+        screenY: window.screen.availHeight,
+        clientWidth: document.documentElement.clientWidth,
+        clientHeight: document.documentElement.clientHeight,
+        windowInnerWidth: window.innerWidth,
+        windowInnerHeight: window.innerHeight,
+        windowOuterWidth: window.outerWidth,
+        windowOuterHeight: window.outerHeight,
+      },
+    });
+    void getScreenShot({ cause: "ss-registerOnResize" });
+  }, 3000);
+  window.onresize = throttledResizeLog;
+}

+ 5 - 0
src/utils/utils.ts

@@ -4,3 +4,8 @@ export function setUUID() {
     localStorage.setItem("uuidForEcs", uuidForEcs);
   }
 }
+
+// 如果这台机器上登录过的学生人数小于5,那么这台电脑就是学生的
+export function isThisMachineOwnByStudent() {
+  return JSON.parse(localStorage.getItem("userIds") || "[]").length < 5;
+}