前端如何防止使用者修改水印?

劉凱里發表於2021-10-21

寫在前面

大家好,我是凱里,歡迎關注我的「部落格」,全是好東西。

這篇文章聊到的「前端水印防篡改」方法,本來是想作為一份專利提交給公司的,但我下筆之前去國內的專利檢索網站上搜了下,發現一模一樣的方案已經被申請了,是一位位元組跳動的哥們在今年年初遞交的。

既然坑已經被佔了,那我就不捲了。

但考慮到這個方案又簡單又管用,在這裡分享給大家。

水印的實現方案

前端水印這個需求其實是一直存在的,最高頻的場景是公司內部系統防止資訊洩漏。如果實現放在前端,主流的實現方法分為以下兩類:

  • SVG / PNG 等圖片結合 CSS background 屬性
  • Canvas

比如開啟一個知名的線上素材編輯網站,檢視其水印的實現方式,就是第一種。

截圖2021-10-10_11.50.42.png

好傢伙,這麼多 important,足以看出來這位寫樣式的前端對這個水印非常重視了。

這裡我只截圖了 CSS,對應的 HTML 就是一個指定了背景圖片的 div 元素。

另外一種基於 Canvas 實現的水印,也不難理解,直接畫就完事了,這裡不再贅述。

水印的破解

前端歲月靜好,直到被人開啟了 Devtools...

一旦開啟了瀏覽器的開發者工具,通過直接修改元素的 CSS 屬性,頁面上的水印直接破防。

如果是基於 Canvas 畫的水印,看起來比較難搞,但實際上,直接在控制檯的元素區域把整個 Canvas 元素暴力刪除掉,水印也就不復存在了。

水印防破解

回憶以下剛才講到的水印破解方案,要麼是通過修改元素的 CSS 屬性要麼是直接修改 DOM 結構,所以能不能嘗試用愛感化使用者讓他不要去修改呢?當然不行。人機互動學中有一個原則就是「要用最大的惡意去揣測你的使用者」。

但仔細一想,使用者不管是修改 CSS 還是修改 DOM,都必須要開啟瀏覽器的 devtools,能否不讓使用者開啟 devtools 呢?

當然可以。

禁止使用者開啟 devtools

拿 windows 舉例,使用者如果要開啟控制檯無外乎兩種途徑:

  1. 鍵盤 F12
  2. 滑鼠右鍵後選擇檢查元素

這就好辦了:

// 阻止 F12 事件
document.addEventListener('keydown', event => {
    return 123 !== event.keyCode || event.returnValue = false;
});

// 阻止滑鼠右鍵事件
document.addEventListener('contextmenu', event => {
    return event.returnValue = false;
});

這下確實能把企圖想要開啟 devtools 的使用者徹底攔住,但有點簡單粗暴了,畢竟滑鼠右鍵以後除了「檢查元素」還有一些別的瀏覽器功能,太“一刀切”會傷害到使用者體驗。

監聽 devtools 的開啟事件

遺憾的是,瀏覽器並沒有提供原生的 devtools 開啟事件,但我們可以曲線救國:通關檢查瀏覽器可視區域和瀏覽器視窗的差值來判斷使用者是否開啟了 devtools。實際上,在 Github 上坐擁 1.5k+ star 的開源解決方案 devtools-detect 就是這麼做的。

核心實現也很簡單:

const resize = () => {
    const threshold = 200;
    const width = window.outerWidth - window.innerWidth > threshold;
    const height = window.outerHeight - window.innerHeight > threshold;
    if (width || height) {
        console.log('控制檯開啟了,使用者準備破解水印了!!!');
    }
}

resize();
window.addEventListener('resize', resize);

不過,這個方案有一個很大的漏洞:它只能用來檢測 devtools 在瀏覽器頁面中內嵌開啟時的情況,但是現在的瀏覽器幾乎都提供了新視窗開啟 devtools 的功能,所以這個檢測很容易被繞過。

截圖2021-10-10_13.08.05.png

MutationObserver

事已至此,是時候祭出最屌的方案了(我開頭提到的專利方案):基於 MutationObserver 的元素屬性變化監測。

The MutationObserver interface provides the ability to watch for changes being made to the DOM tree. It is designed as a replacement for the older Mutation Events feature, which was part of the DOM3 Events specification.

簡言之,MutationObserver 可以監測到 DOM 元素上任何屬性的變化情況,如有需要,也可以監聽其子元素的變化情況。這不就是我們需要的嗎?

當使用者通過 devtools 修改了水印元素的屬性時,MutationObserver 可以及時地通知我們,這樣就能在第一時間恢復我們的水印。有一點需要注意的是,MutationObserver 監聽的是元素的屬性,即 attributes,所以我們的 css 樣式應當作為元素的 style 屬性內嵌在 HTML 中。

以下程式碼是本方案的具體實現:

// <h1 style="margin:100px;">別改我</h1>

const options = {
    childList: true,
    attributes: true,
    subtree: true,
    attributesOldValue: true,
    characterData: true,
    characterDataOldValue: true,
}

const reset = (expression = () => {}) => {
    setTimeout(() => {
      observer.disconnect();
    // 執行恢復方法
        expression();
    observer.observe(h1, options);
   }, 0);
}

const callback = (records) => {
    const record = records[0];
  if (record.type === 'attributes' && record.attributeName === 'style') {
      reset(() => {
        h1.setAttribute('style', 'margin:100px;');
    });
  } else if (record.type === 'characterData') {
      reset(() => {
        h1.textContent = '別改我'  
    });
  }
}

const observer = new MutationObserver(callback);
observer.observe(h1, options);

圖為禁止修改 h1 元素的 style 和 textContent,可以直接複製到 IDE 裡玩一下。

這裡可以直接把 style 的值抽取為一個常量,但凡使用者修改了元素的 style 屬性,這段程式碼會自動用剛才的固定常量覆蓋使用者修改後的值,從而就實現了前端水印的防篡改。

最後

歡迎各位加我的微信 「K-I2ving」 交個朋友,同時我也可以幫忙內推(深圳範圍內的任何知名公司都可)。
建議關注我的「部落格」,全是好東西。

相關文章