前端水印生成方案
前段時間做某系統審覈後臺,出現了審覈人員截圖把內容外部扭曲的情況,雖然截圖內容不是特別敏感,但是安全問題還是不能忽略。於是便在系統頁面上面加上了水印,對於審覈人員截圖等敏感操作有一定的提示作用。
網頁水印生成解決方案
通過canvas生成水印
畫布兼容性
這裏我們用canvas來生成base64圖片,通過CanIUse網站查詢兼容性,如果在移動端以及一些管理系統使用,兼容性問題可以完全忽略。
HTMLCanvasElement.toDataURL 方法返回一個包含圖片展示的數據URI。可以使用類型參數其類型,爲PNG格式。圖片的分辨率爲96dpi。
如果畫布的高度或寬度爲0,那麼會返回字符串“ data :,”。
如果初始化的類型非“ image / png”,但返回的值以“ data:image / png”開頭,那麼該必然的類型是不支持的。
鉻支持“圖像/ WEBP”類型。具體參考HTMLCanvasElement.toDataURL
具體代碼實現如下:
(function () {
// canvas 實現 watermark
function __canvasWM({
// 使用 ES6 的函數默認值方式設置參數的默認取值
// 具體參見 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions/Default_parameters
container = document.body,
width = '200px',
height = '150px',
textAlign = 'center',
textBaseline = 'middle',
font = "20px microsoft yahei",
fillStyle = 'rgba(184, 184, 184, 0.8)',
content = '請勿外傳',
rotate = '30',
zIndex = 1000
} = {}) {
var args = arguments[0];
var canvas = document.createElement('canvas');
canvas.setAttribute('width', width);
canvas.setAttribute('height', height);
var ctx = canvas.getContext("2d");
ctx.textAlign = textAlign;
ctx.textBaseline = textBaseline;
ctx.font = font;
ctx.fillStyle = fillStyle;
ctx.rotate(Math.PI / 180 * rotate);
ctx.fillText(content, parseFloat(width) / 2, parseFloat(height) / 2);
var base64Url = canvas.toDataURL();
const watermarkDiv = document.createElement("div");
watermarkDiv.setAttribute('style', `
position:absolute;
top:0;
left:0;
width:100%;
height:100%;
z-index:${zIndex};
pointer-events:none;
background-repeat:repeat;
background-image:url('${base64Url}')`);
container.style.position = 'relative';
container.insertBefore(watermarkDiv, container.firstChild);
});
window.__canvasWM = __canvasWM;
})();
// 調用
__canvasWM({
content: 'QQMusicFE'
})
效果如下:
![畫布實現網頁水印效果]
圖片
爲了使這個方法更通用,兼容不同的引用方式,我們還可以加上這段代碼:
// 爲了兼容不同的環境
if (typeof module != 'undefined' && module.exports) { //CMD
module.exports = __canvasWM;
} else if (typeof define == 'function' && define.amd) { // AMD
define(function () {
return __canvasWM;
});
} else {
window.__canvasWM = __canvasWM;
}
這樣看起來能滿足我們的需求了,但是還有一個問題,稍微懂一點瀏覽器的使用或網頁知識的用戶,可以用瀏覽器的開發者工具來動態更改DOM的屬性或者結構就可以去掉了。當時有兩個解決辦法:
監測水印div的變化,記錄剛生成的div的innerHTML,每隔幾秒就取一次新的值,一旦發生變化,則重新生成水印。
使用MutationObserver
MutationObserver給開發者們提供了一種能在某個範圍內的DOM樹發生變化時做出適當反應的能力。
MutationObserver兼容性
圖片
通過兼容性表可以看出高級瀏覽器以及移動瀏覽器支持非常不錯。
突變觀察員API用來監視DOM變動.DOM的任何變動,比如節點的增減,屬性的變動,文本內容的變動,這個API都可以得到通知。
使用MutationObserver的實例的觀察函數方法用來啓動監聽,它接受兩個參數。
第一個參數:所要觀察的DOM節點,第二個參數:一個配置對象,指定所要觀察的特定變動,有以下幾種:
屬性 | 描述 |
---|---|
childList | 如果需要觀察目標目標的子例程(添加了某個子例程,或者可移除了某個子例程),則設置爲true。 |
屬性 | 如果需要觀察目標目標的屬性變量(添加或刪除了某個屬性,以及某個屬性的屬性值發生了變化),則設置爲true。 |
characterData | 如果目標目標對象爲字符數據例程(一種抽象接口,具體可以爲文本索引,註釋索引,以及處理指令例程)時,也要觀察該例程的文本內容是否發生變化,則設置爲true。 |
子樹 | 除了目標例程,如果還需要觀察目標例程的所有後代例程,則設置爲true。 |
attributeOldValue | 在屬性屬性已經被設置爲true,則需要設置爲true。 |
characterDataOldValue | 在characterData屬性已經被設置爲true的情況下,如果需要將發生變化的characterData之前的文本內容記錄下來(記錄到下面的MutationRecord對象的oldValue屬性中),則設置爲true。 |
attributeFilter | 一個屬性名數組(不需要指定命名空間),只有該數組中包含的屬性名發生變化時纔會被觀察到,其他名稱的屬性發生變化後會被忽略。 |
MutationObserver只能監測到某種屬性改變,增減子結點等,對於自己本身被刪除,是沒有辦法的可以通過監測父結點來達到要求。因此最終改造之後的代碼爲:
(function () {
// canvas 實現 watermark
function __canvasWM({
// 使用 ES6 的函數默認值方式設置參數的默認取值
// 具體參見 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions/Default_parameters
container = document.body,
width = '300px',
height = '200px',
textAlign = 'center',
textBaseline = 'middle',
font = "20px Microsoft Yahei",
fillStyle = 'rgba(184, 184, 184, 0.6)',
content = '請勿外傳',
rotate = '30',
zIndex = 1000
} = {}) {
const args = arguments[0];
const canvas = document.createElement('canvas');
canvas.setAttribute('width', width);
canvas.setAttribute('height', height);
const ctx = canvas.getContext("2d");
ctx.textAlign = textAlign;
ctx.textBaseline = textBaseline;
ctx.font = font;
ctx.fillStyle = fillStyle;
ctx.rotate(Math.PI / 180 * rotate);
ctx.fillText(content, parseFloat(width) / 2, parseFloat(height) / 2);
const base64Url = canvas.toDataURL();
const __wm = document.querySelector('.__wm');
const watermarkDiv = __wm || document.createElement("div");
const styleStr = `
position:absolute;
top:0;
left:0;
width:100%;
height:100%;
z-index:${zIndex};
pointer-events:none;
background-repeat:repeat;
background-image:url('${base64Url}')`;
watermarkDiv.setAttribute('style', styleStr);
watermarkDiv.classList.add('__wm');
if (!__wm) {
container.style.position = 'relative';
container.insertBefore(watermarkDiv, container.firstChild);
}
const MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
if (MutationObserver) {
let mo = new MutationObserver(function () {
const __wm = document.querySelector('.__wm');
// 只在__wm元素變動才重新調用 __canvasWM
if ((__wm && __wm.getAttribute('style') !== styleStr) || !__wm) {
// 避免一直觸發
mo.disconnect();
mo = null;
__canvasWM(JSON.parse(JSON.stringify(args)));
}
});
mo.observe(container, {
attributes: true,
subtree: true,
childList: true
})
}
}
if (typeof module != 'undefined' && module.exports) { //CMD
module.exports = __canvasWM;
} else if (typeof define == 'function' && define.amd) { // AMD
define(function () {
return __canvasWM;
});
} else {
window.__canvasWM = __canvasWM;
}
})();
// 調用
__canvasWM({
content: 'QQMusicFE'
});
通過SVG生成水印
SVG:可縮放矢量圖形(英語:Scalable Vector Graphics,SVG)是一種基於可擴展標記語言(XML),用於描述二維矢量圖形的圖形格式。維基百科
SVG瀏覽器兼容性
圖片
概述Canvas,SVG有更好的瀏覽器兼容性,使用SVG生成水印的方式與Canvas的方式類似,只是base64Url的生成方式換成SVG。具體如下:
(function () {
// svg 實現 watermark
function __svgWM({
container = document.body,
content = '請勿外傳',
width = '300px',
height = '200px',
opacity = '0.2',
fontSize = '20px',
zIndex = 1000
} = {}) {
const args = arguments[0];
const svgStr = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${width}">
<text x="50%" y="50%" dy="12px"
text-anchor="middle"
stroke="#000000"
stroke-width="1"
stroke-opacity="${opacity}"
fill="none"
transform="rotate(-45, 120 120)"
style="font-size: ${fontSize};">
${content}
</text>
</svg>`;
const base64Url = `data:image/svg+xml;base64,${window.btoa(unescape(encodeURIComponent(svgStr)))}`;
const __wm = document.querySelector('.__wm');
const watermarkDiv = __wm || document.createElement("div");
// ...
// 與 canvas 的一致
// ...
})();
__svgWM({
content: 'QQMusicFE'
})
通過NodeJS生成水印
我們同樣可以通過NodeJS來生成網頁水印(出於性能考慮更好的方式是利用用戶客戶端來生成)。前端發一個請求,參數帶上水印內容,後臺返回圖片內容。
具體實現(Koa2環境):安裝gm以及相關環境,詳情看gm文檔
ctx.type = 'image/png';設置響應爲圖片類型
生成圖片過程是異步的,所以需要包裝一層Promise,這樣才能爲通過async / await方式爲ctx.body賦值
const fs = require('fs')
const gm = require('gm');
const imageMagick = gm.subClass({
imageMagick: true
});
const router = require('koa-router')();
router.get('/wm', async (ctx, next) => {
const {
text
} = ctx.query;
ctx.type = 'image/png';
ctx.status = 200;
ctx.body = await ((() => {
return new Promise((resolve, reject) => {
imageMagick(200, 100, "rgba(255,255,255,0)")
.fontSize(40)
.drawText(10, 50, text)
.write(require('path').join(__dirname, `./${text}.png`), function (err) {
if (err) {
reject(err);
} else {
resolve(fs.readFileSync(require('path').join(__dirname, `./${text}.png`)))
}
});
})
})());
});
如果只是簡單的水印展示,建議在瀏覽器生成,性能更好
圖片水印生成解決方案
除了給網頁加上水印之外,有時候我們需要給圖片也加上水印,這樣在用戶保存圖片後,帶上了水印來源信息,既可以保護版權,水印的其他信息也可以防止泄密。
通過畫布給圖片加水印
實現如下:
(function() {
function __picWM({
url = '',
textAlign = 'center',
textBaseline = 'middle',
font = "20px Microsoft Yahei",
fillStyle = 'rgba(184, 184, 184, 0.8)',
content = '請勿外傳',
cb = null,
textX = 100,
textY = 30
} = {}) {
const img = new Image();
img.src = url;
img.crossOrigin = 'anonymous';
img.onload = function() {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
ctx.textAlign = textAlign;
ctx.textBaseline = textBaseline;
ctx.font = font;
ctx.fillStyle = fillStyle;
ctx.fillText(content, img.width - textX, img.height - textY);
const base64Url = canvas.toDataURL();
cb && cb(base64Url);
}
}
if (typeof module != 'undefined' && module.exports) { //CMD
module.exports = __picWM;
} else if (typeof define == 'function' && define.amd) { // AMD
define(function () {
return __picWM;
});
} else {
window.__picWM = __picWM;
}
})();
// 調用
__picWM({
url: 'http://localhost:3000/imgs/google.png',
content: 'QQMusicFE',
cb: (base64Url) => {
document.querySelector('img').src = base64Url
},
});
效果如下:
帆布給圖片生成水印
圖片
通過NodeJS批量爲圖片加水印
我們同樣可以通過gm這個庫來給圖片加上水印
function picWM(path, text) {
imageMagick(path)
.drawText(10, 50, text)
.write(require('path').join(__dirname, `./${text}.png`), function (err) {
if (err) {
console.log(err);
}
});
}
如果需要批處理圖片,只需要遍歷相關文件即可。
如果只是簡單的水印展示,建議在瀏覽器生成,性能更好
拓展 隱水印
前段時間阿里憑截圖查到了月餅事件的泄密者,其實就是用了隱水印這其實很大程度不是前端的範疇了,但是我們也應該瞭解.AlloyTeam團隊寫過一篇。不能說的祕密-前端也能玩的圖片隱寫術,通過畫布給圖片加上了“隱水印”,針對用戶保存的圖片,是可以輕鬆還原裏面隱含的內容,但是對於截圖或處理過的照片卻無能爲力,不過對於一些機密圖片文件展示,是可以偷偷用上該技術的。
使用加密後的水印內容
前端生成的水印也可以,別人也可以用同樣的方式生成,可能會有“嫁禍於人”(可能這是多慮的),我們還是要有更安全的解決方法。水印內容可以包含多種編碼後的信息,包括用戶名,用戶ID,時間等。例如我們只是想保存用戶唯一的用戶ID,需要把用戶ID放在下面的md5方法,就可以生成唯一標識。 ,但可以通過雙向遍歷所有用戶的方式進行後續。這樣就可以防止水印造假也可以進入真正水印的信息。
// MD5加密庫 utility
const utils = require('utility')
// 加鹽MD5
exports.md5 = function (content) {
const salt = 'microzz_asd!@#IdSDAS~~';
return utils.md5(utils.md5(content + salt));
}
總結
安全問題不能大意,對於一些比較敏感的內容,我們可以通過組合使用上述的水印方案,這樣才能最大程度地給瀏覽者警示的作用,減少泄密的情況,甚至泄密了,也有可能追蹤到泄密者。