前端如何防止數據被異常篡改並且複原數據

来源:https://www.cnblogs.com/coco1s/archive/2023/11/08/17816734.html
-Advertisement-
Play Games

每天,我們都在和各種文檔打交道,PRD、技術方案、個人筆記等等等。 其實文檔排版有很多學問,就像我,對排版有強迫症,見不得英文與中文之間不加空格。 所以,最近在做這麼一個谷歌擴展插件 chrome-extension-text-formatting,通過谷歌擴展,快速將選中文本,格式化為符合 中文文 ...


每天,我們都在和各種文檔打交道,PRD、技術方案、個人筆記等等等。

其實文檔排版有很多學問,就像我,對排版有強迫症,見不得英文與中文之間不加空格

所以,最近在做這麼一個谷歌擴展插件 chrome-extension-text-formatting,通過谷歌擴展,快速將選中文本,格式化為符合 中文文案排版指北 的文本。

emmm,什麼是排版指南?簡單來說它的目的在於統一中文文案、排版的相關用法,降低團隊成員之間的溝通成本,增強網站氣質。

舉個例子:

中英文之間需要增加空格

正確:

在 LeanCloud 上,數據存儲是圍繞 AVObject 進行的。

錯誤:

在LeanCloud上,數據存儲是圍繞AVObject進行的。

在 LeanCloud上,數據存儲是圍繞AVObject 進行的。

完整的正確用法:

在 LeanCloud 上,數據存儲是圍繞 AVObject 進行的。每個 AVObject 都包含了與 JSON 相容的 key-value 對應的數據。數據是 schema-free 的,你不需要在每個 AVObject 上提前指定存在哪些鍵,只要直接設定對應的 key-value 即可。

例外:「豆瓣FM」等產品名詞,按照官方所定義的格式書寫。

中文與數字之間需要增加空格

正確:

今天出去買菜花了 5000 元。

錯誤:

今天出去買菜花了 5000元。

今天出去買菜花了5000元。

當然,整個排版規範不僅僅局限於此,上面只是簡單列出部分規範內容。而且,這玩意屬於建議,很難強迫推廣開來。所以,我就想著實現這麼一個谷歌插件擴展,一鍵實現選中文本的格式化。

看個示意圖:

適用於各種文本編輯框,當然 Excel 也可以:

當然,這都不是本文的重點

相容語雀文檔遇到的異常場景

因為各個文檔平臺存在一定的差異性,所以在擴展的製作過程,需要去相容不同的文檔平臺(當然,更多的是我自己比較常用的一些文檔平臺,譬如谷歌文檔、語雀、有道雲、Github 等等)。

整體來說,整個擴展的功能非常簡單,一個極簡流程如下:

需要註意的是,上面的操作,大部分都是基於插入到頁面的 JavaScript 腳本文件進行執行。

在相容語雀文檔的時候,遇到了這麼個有趣的場景。

在上面的第 4 步執行完畢後,在我們對替換後的文本進行任意操作時,譬如重新獲焦、重新編輯等,被修改的文本都會被進行替換複原,複原成修改前的狀態

什麼意思呢?看看下麵這張實際的截圖:

總結一下,語雀這裡這個操作是什麼意思呢?

在腳本手動替換掉原選取文件後,當再次獲焦文本,修改的內容再會被覆原

在一番測試後,我理清了語雀文檔的邏輯:

  1. 如果是用戶正常輸入內容,通過鍵盤敲入內容,或者正常的複製粘貼,文檔可以被正常修改,被保存;
  2. 如果文檔內容的修改是通過腳本插入、替換,或者文檔內容的修改是通過控制台手動修改 DOM,文檔的內容都將會被覆原;
  3. 利用腳本對內容進行任意修改後,即便不做任何操作,直接點擊保存按鈕,文檔仍然會被覆原為操作前的版本;

Oh,這個功能確實是非常的有意思。它的強悍之處在於,它能夠識別出內容的修改是常規正常操作,還是腳本、控制台修改等非常規操作。並且在非常規操作之後,回退到最近一次的正常操作版本

那麼,語雀它是如何做到這一點的呢?

由於線上編譯混淆後的代碼比較難以斷點調試,所以我們大膽的猜測一下,如果我們需要去實現一個類似的功能,可能從什麼方向入手。

MutationObserver 實現文檔內容堆棧存儲

首先,我們肯定需要用到 MutationObserver

MutationObserver 是一個 JavaScript API,用於監視 DOM 的變化。它提供了一種非同步觀察 DOM 樹的能力,併在發生變化時觸發回調函數。

我們來構建一個線上文檔的最小化場景:

<div id="g-container" contenteditable>
    這是 Web 雲文檔的一段內容,如果直接編輯,可以編輯成功。如果使用控制台修改,數據將會被恢復。
</div>
#g-container {
    width: 400px;
    padding: 20px;
    line-height: 2;
    border: 2px dashed #999;
}

這裡,我們利用 HTML 的 contenteditable 屬性,實現了一個可編輯的 DIV 框:

接下來,我們就可以利用 MutationObserver,實現對這個 DOM 元素的監聽,實現每當此元素的內容發生改變,就觸發 MutationObserver 的事件回調,並且通過一個數組,記錄下每一次元素改動的結果。

其大致代碼如下:

const targetElement = document.getElementById("g-container");
// 記錄初始數據
let cacheInitData = '';

function observeElementChanges(element) {
    const changes = []; // 存儲變化的數組
    const targetElementCache = element.innerText;

    // 緩存每次的初始數據
    cacheInitData = targetElementCache;
    
    // 創建 MutationObserver 實例
    const observer = new MutationObserver((mutationsList, observer) => {
        // 檢查當前是否存在焦點
        mutationsList.forEach((mutation) => {
            console.log('observer', observer);
            const { type, target, addedNodes, removedNodes } = mutation;
            let realtimeText = "";
            
            const change = {
                type,
                target,
                addedNodes: [...addedNodes],
                removedNodes: [...removedNodes],
                realtimeText,
            };
            
            changes.push(change);
        });
        
        console.log("changes", changes);
    });

    // 配置 MutationObserver
    const config = { childList: true, subtree: true, characterData: true };

    // 開始觀察元素的變化
    observer.observe(element, config);
}

observeElementChanges(targetElement);

上面的代碼,閱讀起來需要一點點時間。但是其本質是非常好理解的,我大致將其核心步驟列舉一下:

  1. 創建一個 MutationObserver 實例來觀察指定 DOM 元素的變化

  2. 定義一個配置對象 config,用於指定觀察的選項。在這個例子中,配置對象中設置了

    1. childList: true 表示觀察子節點的變化
    2. subtree: true 表示觀察所有後代節點的變化
    3. characterData: true 表示觀察節點文本內容的變化
  3. 將變化的信息存儲在 changes 數組中

  4. changes 數組中的每個元素記錄了一次 DOM 變化的信息。每個變化對象包含以下屬性:

    1. type:表示變化的類型,可以是 "attributes"(屬性變化)、"characterData"(文本內容變化)或 "childList"(子節點變化)。
    2. target:表示發生變化的目標元素。
    3. addedNodes:一個包含新增節點的數組,表示在變化中添加的節點。
    4. removedNodes:一個包含移除節點的數組,表示在變化中移除的節點。
    5. realtimeText:實時文本內容,可以根據具體需求進行設置。

如此一來,我們嘗試編輯 DOM 元素,打開控制台,看看每次 changes 輸出了什麼內容:

可以發現,每一次當 DIV 內的內容被更新,都會觸發一次 MutationObserver 的回調。

我們詳細展開數組中的兩處進行說明:

其中 type 表示這次觸發的是 MutationObserver 配置的 config 中的哪一類變化,命中了 characterData,也就是上面提到的文本內容的變化。而 addedNodesremoveDNodes 都為空,說明沒有結構上的變化。

兩組數據唯一的變化在於 realtimeText 我們利用了這個值記錄了可編輯 DOM 元素內文本值內容。

  • 第一次刪除了一個句號 ,所以 realtimeText 文本相比初始文本少了個句號
  • 二次操作刪除了一個 字,所以 realtimeText 文本相比初始文本少了 復。

後面的數據依次類推。可以看到,有了這個信息,其實我們相當於能夠實現整個 DOM 結構的操作堆棧

在此基礎上,我們可以在整個監聽之前,在 changes 數組中首先壓入最開始未經過任何操作的數據。這也就意味著我們有能力將數據恢復到用戶的操作過程中的任意一步

利用特征狀態,識別用戶是否是手動輸入

有了上面的changes 數組,我們相當於有了用戶操作的每一步的堆棧信息。

接下的核心就在於我們應該如何去運用它們

在語雀這個例子中,它的核心點在於:

它能夠識別出內容的修改是常規正常操作,還是腳本、控制台修改等非常規操作。並且在非常規操作之後,回退到最近一次的正常操作版本

因此,我們接下來探索的問題就變成瞭如何識別一個可輸入編輯框,它的內容修改是正常輸入修改,還是非正常輸入修改。

譬如,思考一下,當用戶正常輸入或者複製粘貼內容到編輯框,應該會有什麼特征信息:

  1. 可以通過 document.activeElement 拿到當前頁面獲焦的元素,因此可以在每次觸發 Mutation 變化的時,多存儲一份當前的獲焦元素信息,對比內容被修改時的頁面獲焦元素是否是當前輸入框
  2. 嘗試判斷輸入框的獲焦狀態,可以通過監聽 foucsblur 獲焦及失焦等事件進行判斷
  3. 用戶當文本內容改變時,是否有經過觸發過鍵盤事件,譬如 keydown 事件
  4. 用戶當文本內容改變時,是否有經過觸發過鍵盤事件的粘貼 paste 事件
  5. 對於直接修改控制台,則可能是除了文本內容外,有 DOM 子樹的其他變化,也就是會觸發 Mutation 的 childList 變化事件

有了上面的思路,下麵我們嘗試一下,為了儘可能讓 DEMO 好理解,我們稍微簡化需求,實現:

  1. 一個輸入框,用戶正常輸入可以改變內容
  2. 當輸入框內容通過控制台進行修改,則當元素再次獲焦時,恢復到最近一次的手動修改記錄
  3. 如果(2)找不到最近一次的手動修改記錄,將數據恢復到初始狀態

基於此,下麵我給出大致的偽代碼:

<div id="g-container" contenteditable>這是 Web 雲文檔的一段內容,如果直接編輯,可以編輯成功。如果使用控制台修改,數據將會被恢復。</div>
const targetElement = document.getElementById("g-container");
// 記錄初始數據
let cacheInitData = '';
// 數據複位標誌位
let data_fixed_flag = false; 
// 複位緩存對象
let cacheObservingObject = null;
let cacheContainer = null;
let cacheData = '';

function eventBind() {
    targetElement.addEventListener('focus', (e) => {        
        if (data_fixed_flag) {
            cacheContainer.innerText = cacheData;
            cacheObservingObject.disconnect();
            observeElementChanges(targetElement);
            
            data_fixed_flag = false;
        }
    });
}

function observeElementChanges(element) {
    const changes = []; // 存儲變化的數組
    const targetElementCache = element.innerText;

    // 緩存每次的初始數據
    cacheInitData = targetElementCache;
    
    // 創建 MutationObserver 實例
    const observer = new MutationObserver((mutationsList, observer) => {
        mutationsList.forEach((mutation) => {
            // console.log('observer', observer);
            const { type, target, addedNodes, removedNodes } = mutation;
            let realtimeText = "";
            
            if (type === "characterData") {
                realtimeText = target.data;
            }
            
            const change = {
                type,
                target,
                addedNodes: [...addedNodes],
                removedNodes: [...removedNodes],
                realtimeText,
                activeElement: document.activeElement
            };
            changes.push(change);
        });
        
        let isFixed = false;
        let container = null;
        
        for (let i = changes.length - 1; i >= 0; i--) {
            const item = changes[i];
            // console.log('i', i);
            if (item.activeElement === element) {
                if (isFixed) {
                    cacheData = item.realtimeText;
                }
                break;
            } else {
                if (!isFixed) {
                    isFixed = true;
                    container = item.target.nodeType === 3 ? item.target.parentElement : item.target;
                    cacheContainer = container;
                    data_fixed_flag = true;
                }
            }
        }
        
        if (data_fixed_flag && cacheData === '') {
            cacheData = cacheInitData;
        }
        
        cacheObservingObject = observer;
    });

    // 配置 MutationObserver
    const config = { childList: true, subtree: true, characterData: true };

    // 開始觀察元素的變化
    observer.observe(element, config);
    eventBind();
    
    // 返回停止觀察並返回變化數組的函數
    return () => {
        observer.disconnect();
        return changes;
    };
}

observeElementChanges(targetElement);

簡單解釋一下,大致流程如下

  1. observeElementChanges 上文已經出現過,核心在於記錄每一次 DOM 元素的變化,將變化內容記錄在 changes 數組中

    1. 多記錄了一個 activeElement,表示每次 DOM 元素髮生變化時,頁面的焦點元素
  2. 每次 changes 更新後,倒序遍歷一次 changes 數組

    1. 如果當前頁面獲焦元素與當前發生變化的 DOM 元素不是同一個元素,則認為是一次非法修改,記錄兩個標誌位 isFixeddata_fixed_flag,此時繼續向前尋找最近一次正常修改記錄
    2. isFixed 用於向前尋找最近一次正常修改記錄後,將最近一次修改的堆棧信息進行保存
  3. data_fixed_flag 標誌位用於當元素被再次獲焦時(觸發 focus 事件),根據標誌位判斷是否需要回滾恢複數據

OK,此時,我們來看看整體效果:

這樣,我們就成功的實現了識別非正常操作,並且恢復到上一次正常數據。

當然,實際場景肯定比這個複雜,並且需要考慮更多的細節,這裡為了整體的可理解性,簡化了整個 DEMO 的表達。

完整的 DEMO 效果,你可以戳這裡體驗:[CodePen Demo -- Editable Text Fixed]

一些思考

至於這個功能有什麼用?這個就見仁見智了,至少對於開發擴展插件的我而言,是一個非常棘手的問題,當然從語雀的角度而言,更多也許是從安全方面進行考量的。

當然,我們不應該局限於這個場景,思考一下,這個方案其實可以應用在非常多其它場景,舉個例子:

  1. 前端頁面水印,實現當水印 DOM 的樣式、結構、或者內容被篡改時,立即進行水印恢復

當然,破解起來也有一些方式,對於擴展插件而言,我可以通過更早的向頁面註入我的 content script,在頁面載入渲染前,對全局的 MutationObserver 對象進行劫持。

總而言之,可以通過本文提供的思路,嘗試進行更多有意思的前端交互限制。

最後

好了,本文到此結束,希望對你有幫助

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 目錄String簡單介紹常見命令應用場景Hash簡單介紹常見命令應用場景List簡單介紹常見命令應用場景Set簡單介紹常見命令應用場景Sorted Set(Zset)簡單介紹常見命令應用場景Bitmap簡單介紹常見命令應用場景附錄 Redis支持多種數據類型,比如String、hash、list、S ...
  • 在構建數據倉庫或做數據分析時,需要對原始數據的結構進行一定的處理,有時涉及到“行轉列”,有時涉及到“列轉行”,那麼這兩個轉換的方式具體是什麼,有什麼差異,怎麼實現。 ...
  • 本文主要以介紹方法為主,落地過程可以歸納為方案->收益測算->數據安全驗證->系統穩定性驗證->灰度與回滾。文中的賬單系統通過step1大表壓縮32%,step2大JSON欄位序列化12%,step3刪除無效數據10%,3個方案的順利落地,有效的減少了50.7%的磁碟空間,成本下降也非常顯著。最後,... ...
  • 本文分享自華為雲社區《GaussDB(DWS)性能調優:表掃描時過濾行數過多引起的性能瓶頸問題案例》,作者: O泡果奶~ 。 1、【問題描述】 SQL語句執行過程中,對12億數據量的大表進行掃描,過濾99%的數據僅留617行數據,性能瓶頸位於掃描該表這裡。 2、【原始語句】 set search_p ...
  • 本文采用一張簡單的架構圖說明瞭MySQL查詢中使用的組件和組件間關係。解析了一條sql語句從客戶端請求mysql伺服器到返回給客戶端的整個生命周期流程。 ...
  • 路由跳轉原理 之 Hash 一. 路由跳轉的原理 首先講講路由跳轉的原理, 其實沒有什麼神秘的, 以變數類比: // 首先定義一個變數名為 container , 賦予初始值 'index' let container = 'index'; // 監聽一個點擊事件 window.addEventLi ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 代碼片段 閑來無事寫了個有意思的東西,滑鼠放在小方塊上會放大並擠壓周圍方塊,背景顏色會動態改變。這裡沒有用一行 js 代碼,純樣式(Sass)實現。 <template> <div class="container"> <div clas ...
  • 1. 前言 唉,好想玩滋嘣。 2. 計算屬性直接傳參接收不到 表格數據某一列需要用的計算屬性時,模板中使用計算屬性 fullName 就會直接調用 fullName 函數,而在模板中 fullName(item) 相當於fullName()(item),此處為函數柯里化。 <el-table-col ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...