前端水印生成方案

先看下效果:


水印效果

思路1:

使用canvas进行生成图片,然后动态生成div填充整个背景,将生成的图片用与 background-image属性上,进行页面填充.
问题:
如果在开发者工具里取消了这个属性,则该水印效果就会消失,所以需要一种方法监视该div,不让其修改,或修改了之后又进行恢复,此处就用到了用来监视 DOM变动的Mutation Observer API .DOM 的任何变动,比如节点的增减、属性的变动、文本内容的变动,这个 API 都可以得到通知。参考MDN:MutationObserver
MutationObserver只能监测到诸如属性改变、增删子结点等,对于自己本身被删除,是没有办法的,那么可以通过监测父结点来达到要求。

代码:


/**
 * 网页加水印
 *
 * @export
 * @param {*} [{
 *   container = document.body,
 *   width = '400px',
 *   height = '300px',
 *   textAlign = 'center',
 *   textBaseline = 'middle',
 *   font = "20px Microsoft Yahei",
 *   fillStyle = 'rgba(230, 230, 230, 0.8)',
 *   content = '保密水印',
 *   rotate = '10',
 *   zIndex = -1000
 * }={}]
 * @returns
 */
export function __waterDocument({
  container = document.body,
  width = '200px',
  height = '150px',
  textAlign = 'center',
  textBaseline = 'middle',
  font = "20px Microsoft Yahei",
  fillStyle = 'rgba(230, 230, 230, 0.8)',
  content = '保密水印',
  rotate = '10',
  zIndex = -1000
} = {}) {
  const args = arguments[0];
  const canvas = document.createElement('canvas');
  canvas.setAttribute('width', width);
  canvas.setAttribute('height', height);
  const ctx = canvas.getContext("2d");
  if (ctx === null) {
    console.error("this browser is not support canvas.");
    return;
  }
  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
    })
  }
}

思路2:

如果浏览器对canvas不支持则可以使用svg,svg浏览器支持面比canvas要大一些
可以使用https://caniuse.com/进行查看
实现原理与canvas几乎一致,只是图片替换成了svg


/**
 *网页加水印 svg 方式
 *
 * @export
 * @param {*} [{
 *   container = document.body,
 *   content = '请勿外传',
 *   width = '300px',
 *   height = '200px',
 *   opacity = '0.2',
 *   fontSize = '20px',
 *   zIndex = 1000
 * }={}]
 */
export function __waterDocumentSvg({
  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");
  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;
        __waterDocumentSvg(JSON.parse(JSON.stringify(args)));
      }
    });
    mo.observe(container, {
      attributes: true,
      subtree: true,
      childList: true
    })
  }
}

通过NodeJS生成水印

身为现代前端开发者,Node.JS也是需要掌握的。我们同样可以通过NodeJS来生成网页水印,前端发一个请求,参数带上水印内容,后台返回图片内容。
具体实现(Koa2环境):

  1. 安装gm以及相关环境,详情看gm文档
  2. ctx.type = 'image/png';设置响应为图片类型
  3. 生成图片过程是异步的,所以需要包装一层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`)))
          }
        });
    })
  })());
});

图片水印生成解决方案

通过canvas给图片加水印
思路:读取图片,在图片加载完之后用canvas填充图片,并填充加密文字,然后将图片转成base64url传值给回调函数

/**
 *图片加水印
 *
 * @export
 * @param {*} [{
 *   url = '',
 *   textAlign = 'center',
 *   textBaseline = 'middle',
 *   font = "20px Microsoft Yahei",
 *   fillStyle = 'rgba(184, 184, 184, 0.8)',
 *   content = '请勿外传',
 *   cb = null,
 *   textX = 100,
 *   textY = 30
 * }={}]
 */
export 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);
  }
}

调用:

__picWM({
        url: 'https://www.baidu.com/img/bd_logo1.png?where=super',
        content: '加密图片',
        cb: (base64Url) => {
          document.getElementById('s_lg_img').src = base64Url
        },
      })

效果:


加密图片

使用加密后的水印内容

前端生成的水印也可以,别人也可以用同样的方式生成,可能会有“嫁祸于人”(可能这是多虑的),我们还是要有更安全的解决方法。水印内容可以包含多种编码后的信息,包括用户名、用户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));
}

参考:前端水印生成方案 --QQ音乐前端团队

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,042评论 25 707
  • 大部分的人都会对听故事感兴趣,当讲故事的人,说的栩栩如生,身临其境,一定会让我们有所共鸣,而如果讲故...
    盆叔阅读 233评论 0 0
  • 我没喝过最烈的酒,但我曾经醉的荒唐。 我没走过最远的路,但我曾经在某个人身后嘶吼追赶。 我...
    独尊刘子耕阅读 219评论 0 0
  • 一、目标 第一时间让我们更明确什么是目标。目标是自己非常想要,不惜一切代价都要去得到或者实现的事物。以九宫格的方式...
    瑜悦人生阅读 703评论 0 0