瀏覽代碼

feat(hooks): useWatermark添加水印防篡改功能(#3395) (#3397)

Co-authored-by: jackhoo_98 <jackhoo_98@foxmail.com>
jackhoo(胡彪) 1 年之前
父節點
當前提交
0a1a5ffedc
共有 1 個文件被更改,包括 77 次插入26 次删除
  1. 77 26
      src/hooks/web/useWatermark.ts

+ 77 - 26
src/hooks/web/useWatermark.ts

@@ -4,15 +4,74 @@ import { addResizeListener, removeResizeListener } from '@/utils/event';
 import { isDef } from '@/utils/is';
 
 const watermarkSymbol = 'watermark-dom';
+const updateWatermarkText = ref<string | null>(null);
 
 type UseWatermarkRes = {
   setWatermark: (str: string) => void;
   clear: () => void;
   clearAll: () => void;
+  obInstance?: MutationObserver;
+  targetElement?: HTMLElement;
+  parentElement?: HTMLElement;
 };
 
 const sourceMap = new Map<Symbol, Omit<UseWatermarkRes, 'clearAll'>>();
 
+function createBase64(str: string) {
+  const can = document.createElement('canvas');
+  const width = 300;
+  const height = 240;
+  Object.assign(can, { width, height });
+
+  const cans = can.getContext('2d');
+  if (cans) {
+    cans.rotate((-20 * Math.PI) / 180);
+    cans.font = '15px Vedana';
+    cans.fillStyle = 'rgba(0, 0, 0, 0.15)';
+    cans.textAlign = 'left';
+    cans.textBaseline = 'middle';
+    cans.fillText(str, width / 20, height);
+    // todo 自定义水印样式
+  }
+  return can.toDataURL('image/png');
+}
+const resetWatermarkStyle = (element: HTMLElement, watermarkText: string) => {
+  element.className = '__' + watermarkSymbol;
+  element.style.pointerEvents = 'none';
+  element.style.top = '0px';
+  element.style.left = '0px';
+  element.style.position = 'absolute';
+  element.style.zIndex = '100000';
+  element.style.height = '100%';
+  element.style.width = '100%';
+  element.style.background = `url(${createBase64(
+    unref(updateWatermarkText) || watermarkText,
+  )}) left top repeat`;
+};
+
+const obFn = () => {
+  const obInstance = new MutationObserver((mutationRecords) => {
+    for (const mutation of mutationRecords) {
+      for (const node of Array.from(mutation.removedNodes)) {
+        const target = Array.from(sourceMap.values()).find((item) => item.targetElement === node);
+        if (!target) return;
+        const { targetElement, parentElement } = target;
+        // 父元素的子元素水印如果被删除 重新插入被删除的水印(防篡改,插入通过控制台删除的水印)
+        if (!parentElement?.contains(targetElement as Node | null)) {
+          target?.parentElement?.appendChild(node as HTMLElement);
+        }
+      }
+      if (mutation.attributeName === 'style' && mutation.target) {
+        const _target = mutation.target as HTMLElement;
+        if (_target.className === '__' + watermarkSymbol) {
+          resetWatermarkStyle(_target as HTMLElement, _target?.['data-watermark-text']);
+        }
+      }
+    }
+  });
+  return obInstance;
+};
+
 export function useWatermark(
   appendEl: Ref<HTMLElement | null> = ref(document.body) as Ref<HTMLElement>,
 ): UseWatermarkRes {
@@ -29,35 +88,17 @@ export function useWatermark(
     updateWatermark({ height, width });
   });
   const watermarkEl = shallowRef<HTMLElement>();
-
   const clear = () => {
     const domId = unref(watermarkEl);
     watermarkEl.value = undefined;
     const el = unref(appendEl);
+    sourceMap.has(domSymbol) && sourceMap.get(domSymbol)?.obInstance?.disconnect();
     sourceMap.delete(domSymbol);
     if (!el) return;
     domId && el.removeChild(domId);
     removeResizeListener(el, func);
   };
 
-  function createBase64(str: string) {
-    const can = document.createElement('canvas');
-    const width = 300;
-    const height = 240;
-    Object.assign(can, { width, height });
-
-    const cans = can.getContext('2d');
-    if (cans) {
-      cans.rotate((-20 * Math.PI) / 120);
-      cans.font = '15px Vedana';
-      cans.fillStyle = 'rgba(0, 0, 0, 0.15)';
-      cans.textAlign = 'left';
-      cans.textBaseline = 'middle';
-      cans.fillText(str, width / 20, height);
-    }
-    return can.toDataURL('image/png');
-  }
-
   function updateWatermark(
     options: {
       width?: number;
@@ -80,22 +121,32 @@ export function useWatermark(
 
   const createWatermark = (str: string) => {
     if (unref(watermarkEl) && sourceMap.has(domSymbol)) {
+      updateWatermarkText.value = str;
       updateWatermark({ str });
       return;
     }
     const div = document.createElement('div');
+    div['data-watermark-text'] = str; //自定义属性 用于恢复水印
+    updateWatermarkText.value = str;
     watermarkEl.value = div;
-    div.style.pointerEvents = 'none';
-    div.style.top = '0px';
-    div.style.left = '0px';
-    div.style.position = 'absolute';
-    div.style.zIndex = '100000';
+    resetWatermarkStyle(div, str);
     const el = unref(appendEl);
     if (!el) return;
     const { clientHeight: height, clientWidth: width } = el;
     updateWatermark({ str, width, height });
     el.appendChild(div);
-    sourceMap.set(domSymbol, { setWatermark, clear });
+    sourceMap.set(domSymbol, {
+      setWatermark,
+      clear,
+      parentElement: el,
+      targetElement: div,
+      obInstance: obFn(),
+    });
+    sourceMap.get(domSymbol)?.obInstance?.observe(el, {
+      childList: true, // 子节点的变动(指新增,删除或者更改)
+      subtree: true, // 该观察器应用于该节点的所有后代节点
+      attributes: true, // 属性的变动
+    });
   };
 
   function setWatermark(str: string) {
@@ -108,12 +159,12 @@ export function useWatermark(
       });
     }
   }
-
   return { setWatermark, clear, clearAll };
 }
 
 function clearAll() {
   Array.from(sourceMap.values()).forEach((item) => {
+    item?.obInstance?.disconnect();
     item.clear();
   });
 }