从破解某设计网站谈前端水印(详细教程)

本文涉及的产品
对象存储 OSS,20GB 3个月
密钥管理服务KMS,1000个密钥,100个凭据,1个月
对象存储 OSS,恶意文件检测 1000次 1年
简介: 最近在写公众号的时候,常常会自己做首图,并且慢慢地发现沉迷于制作首图,感觉扁平化的设计的真好好看。慢慢地萌生了一个做一个属于自己的首图生成器的想法。

前言



最近在写公众号的时候,常常会自己做首图,并且慢慢地发现沉迷于制作首图,感觉扁平化的设计的真好好看。慢慢地萌生了一个做一个属于自己的首图生成器的想法。


image.png


制作呢,当然也不是拍拍脑袋就开始,在开始之前,就去研究了一下某在线设计网站(如果有人不知道的话,可以说一下,这是一个在线制作海报之类的网站 T T 像我们这种内容创作者用的比较多),毕竟人家已经做了很久了,我只是想做个方便个人使用的。毕竟以上用 PS 做着还是有一些废时间,由于组成的元素都很简单,做一个自动化生成的完全可以。


但是研究着研究着,就看到了某在线设计网站的水印,像这种技术支持的网站,最重要的防御措施就是水印了,水印能够很好的保护知识产权。


慢慢地路就走偏了,开始对它的水印感兴趣了。不禁发现之前只是大概知道水印的生成方法,但是从来没有仔细研究过,本文将以以下的路线进行讲解。以下所有代码示例均在


https://github.com/hua1995116/node-demo/tree/master/watermark


image.png


image.png


明水印



水印(watermark)是一种容易识别、被夹于[1]内,能够透过光线穿过从而显现出各种不同阴影的技术。


水印的类型有很多,有一些是整图覆盖在图层上的水印,还有一些是在角落。


image.png


那么这个水印怎么实现呢?熟悉 PS 的朋友,都知道 PS 有个叫做图层的概念。


image.png


网页也是如此。我们可以通过绝对定位,来将水印覆盖到我们的页面之上。


image.png


最终变成了这个样子。


image.png


等等,但是发现少了点什么。直接覆盖上去,就好像是一个蒙层,我都知道这样是无法触发底下图层的事件的,此时就要介绍一个css属性pointer-events


pointer-events CSS 属性指定在什么情况下 (如果有) 某个特定的图形元素可以成为鼠标事件的 target[2]


当它的被设置为 none 的时候,能让元素实体虚化,虽然存在这个元素,但是该元素不会触发鼠标事件。详情可以查看 CSS3 pointer-events:none应用举例及扩展 « 张鑫旭-鑫空间-鑫生活[3]


这下理清了实现原理,等于成功了一半了!


明水印的生成


明水印的生成方式主要可以归为两类,一种是 纯 html 元素(纯div),另一种则为背景图(canvas/svg)。


下面我分别来介绍一下,两种方式。


div实现


我们首先来讲比较简单的 div 生成的方式。就按照我们刚才说的。


// 文本内容
<div class="app">
        <h1>秋风</h1>
        <p>hello</p>
</div>


首先我们来生成一个水印块,就是上面的 一个个秋风的笔记。这里主要有一点就是设置一个透明度(为了让水印看起来不是那么明显,从而不遮挡我们的主要页面),另一个就是一个旋转,如果是正的水平会显得不是那么好看,最后一点就是使用 userSelect 属性,让此时的文字无法被选中。


userSelect


CSS[4] 属性 user-select 控制用户能否选中文本。除了文本框内,它对被载入为 chrome[5] 的内容没有影响。


function cssHelper(el, prototype) {
  for (let i in prototype) {
    el.style[i] = prototype[i]
  }
}
const item = document.createElement('div')
item.innerHTML = '秋风的笔记'
cssHelper(item, {
  position: 'absolute',
  top: `50px`,
  left: `50px`,
  fontSize: `16px`,
  color: '#000',
  lineHeight: 1.5,
  opacity: 0.1,
  transform: `rotate(-15deg)`,
  transformOrigin: '0 0',
  userSelect: 'none',
  whiteSpace: 'nowrap',
  overflow: 'hidden',
})


有了一个水印片,我们就可以通过计算屏幕的宽高,以及水印的大小来计算我们需要生成的水印个数。


const waterHeight = 100;
const waterWidth = 180;
const { clientWidth, clientHeight } = document.documentElement || document.body;
const column = Math.ceil(clientWidth / waterWidth);
const rows = Math.ceil(clientHeight / waterHeight);
for (let i = 0; i < column * rows; i++) {
    const wrap = document.createElement('div');
    cssHelper(wrap, Object.create({
        position: 'relative',
        width: `${waterWidth}px`,
        height: `${waterHeight}px`,
        flex: `0 0 ${waterWidth}px`,
        overflow: 'hidden',
    }));
    wrap.appendChild(createItem());
    waterWrapper.appendChild(wrap)
}
document.body.appendChild(waterWrapper)


这样子我们就完美地实现了上面我们给出的思路的样子啦。


image.png


背景图实现


canvas


canvas的实现很简单,主要是利用canvas 绘制一个水印,然后将它转化为 base64 的图片,通过canvas.toDataURL() 来拿到文件流的 url ,关于文件流相关转化可以参考我之前写的文章一文带你层层解锁「文件下载」的奥秘, 然后将获取的 url 填充在一个元素的背景中,然后我们设置背景图片的属性为重复。


.watermark {
    position: fixed;
    top: 0px;
    right: 0px;
    bottom: 0px;
    left: 0px;
    pointer-events: none;
    background-repeat: repeat;
}


function createWaterMark() {
  const angle = -20;
  const txt = '秋风的笔记'
  const canvas = document.createElement('canvas');
  canvas.width = 180;
  canvas.height = 100;
  const ctx = canvas.getContext('2d');
  ctx.clearRect(0, 0, 180, 100);
  ctx.fillStyle = '#000';
  ctx.globalAlpha = 0.1;
  ctx.font = `16px serif`
  ctx.rotate(Math.PI / 180 * angle);
  ctx.fillText(txt, 0, 50);
  return canvas.toDataURL();
}
const watermakr = document.createElement('div');
watermakr.className = 'watermark';
watermakr.style.backgroundImage = `url(${createWaterMark()})`
document.body.appendChild(watermakr);


svg


svg 和 canvas 类似,主要还是生成背景图片。


function createWaterMark() {
  const svgStr =
    `<svg xmlns="http://www.w3.org/2000/svg" width="180px" height="100px">
      <text x="0px" y="30px" dy="16px"
      text-anchor="start"
      stroke="#000"
      stroke-opacity="0.1"
      fill="none"
      transform="rotate(-20)"
      font-weight="100"
      font-size="16"
      >
       秋风的笔记
      </text>
    </svg>`;
  return `data:image/svg+xml;base64,${window.btoa(unescape(encodeURIComponent(svgStr)))}`;
}
const watermakr = document.createElement('div');
watermakr.className = 'watermark';
watermakr.style.backgroundImage = `url(${createWaterMark()})`
document.body.appendChild(watermakr);


明水印的破解一


以上就很快实现了水印的几种方案。但是对于有心之人来说,肯定会想着破解,以上破解也很简单。


打开了Chrome Devtools 找到对应的元素,直接按 delete 即可删除。


image.png


明水印的防御


这样子的水印对于大概知道控制台操作的小白就可以轻松破解,那么有什么办法能防御住这样的操作呢?


答案是肯定的,js 有一个方法叫做 MutationObserver,能够监控元素的改动。


MutationObserver 对现代浏览的兼容性还是不错的,MutationObserver是元素观察器,字面上就可以理解这是用来观察Node(节点)变化的。MutationObserver是在DOM4规范中定义的,它的前身是MutationEvent事件,最低支持版本为 ie9 ,目前已经被弃用。


image.png


在这里我们主要观察的有三点


  • 水印元素本身是否被移除
  • 水印元素属性是否被篡改(display: none ...)
  • 水印元素的子元素是否被移除和篡改 (element生成的方式 )


来通过 MDN 查看该方法的使用示例。


const targetNode = document.getElementById('some-id');
// 观察器的配置(需要观察什么变动)
const config = { attributes: true, childList: true, subtree: true };
// 当观察到变动时执行的回调函数
const callback = function(mutationsList, observer) {
    // Use traditional 'for loops' for IE 11
    for(let mutation of mutationsList) {
        if (mutation.type === 'childList') {
            console.log('A child node has been added or removed.');
        }
        else if (mutation.type === 'attributes') {
            console.log('The ' + mutation.attributeName + ' attribute was modified.');
        }
    }
};
// 创建一个观察器实例并传入回调函数
const observer = new MutationObserver(callback);
// 以上述配置开始观察目标节点
observer.observe(targetNode, config);


MutationObserver主要是监听子元素的改动,因此我们的监听对象为 document.body, 一旦监听到我们的水印元素被删除,或者属性修改,我们就重新生成一个。通过以上示例,加上我们的思路,很快我们就写一个监听删除元素的示例。(监听属性修改也是类似就不一一展示了)


// 观察器的配置(需要观察什么变动)
const config = { attributes: true, childList: true, subtree: true };
// 当观察到变动时执行的回调函数
const callback = function (mutationsList, observer) {
// Use traditional 'for loops' for IE 11
  for (let mutation of mutationsList) {
    mutation.removedNodes.forEach(function (item) {
      if (item === watermakr) {
       document.body.appendChild(watermakr);
      }
    });
  }
};
// 监听元素
const targetNode = document.body;
// 创建一个观察器实例并传入回调函数
const observer = new MutationObserver(callback);
// 以上述配置开始观察目标节点
observer.observe(targetNode, config);


我们打开控制台来检验一下。


image.png


这回完美了,能够完美抵御一些开发小白了。


那么这样就万无一失了吗?显然,道高一尺魔高一丈,毕竟前端的一切都是不安全的。


明水印的破解二


在一个高级前端工程师面前,一切都是纸老虎。接下来我就随便介绍三种破解的方式。


第一种


打开Chrome Devtools,点击设置 - Debugger - Disabled JavaScript .


image.png


然后再打开页面,delete我们的水印元素。


image.png


第二种


复制一个 body 元素,然后将原来 body 元素的删除。


image.png


第三种


打开一个代理工具,例如 charles,将生成水印相关的代码删除。


破解实践


接下来我们实战一下,通过预先分析,我们看到某在线设计网站的内容是以 div 的方式实现的,所以可以利用这种方案。打开 https://www.gaoding.com/design?id=33931419&simple=1  (仅供举例学习)


image.png


打开控制台,Ctrl + F 搜索 watermark 相关字眼。(这一步是作为一个程序员的直觉,基本上你要找什么,搜索对应的英文就可以 ~)


image.png


很快我们就找到了水印图。发现直接删除,没有办法删除水印元素,根据我们刚才学习的,肯定是利用了MutationObserver  方法。我们使用我们的第一个破解方法,将 JavaScript 禁用,再将元素删除。


image.png


水印已经消失了。


但是这样真的就万事大吉了吗?


不知道你有没有听过一种东西,看不见摸不着,但是它却真实存在,他的名字叫做暗水印,我们将时间倒流到 16 年间的月饼门事件,因为有员工将内网站点截图了,但是很快被定位出是谁截图了。


image.png


虽然你将一些可见的水印去除了,但是还会存在一些不可见的保护版权的水印。(这就是防止一些坏人拿去作另外的用途)


暗水印



暗水印是一种肉眼不可见的水印方式,可以保持图片美观的同时,保护你的资源版权。


暗水印的生成方式有很多,常见的为通过修改RGB 分量值的小量变动、DWT、DCT 和 FFT 等等方法。


通过介绍前端实现 RGB 分量值的小量变动 来揭秘其中的奥秘,主要参考 不能说的秘密——前端也能玩的图片隐写术 | AlloyTeam[7]


我们都知道图片都是有一个个像素点构成的,每个像素点都是由 RGB 三种元素构成。当我们把其中的一个分量修改,人的肉眼是很难看出其中的变化,甚至是像素眼的设计师也很难分辨出。


image.png


你能看出其中的差别吗?根据这个原理,我们就来实践吧。(女孩子可以掌握方法后可以拿以下图片进行试验测试)



首先拿到以上图片,我们先来讲解解码方式,解码其实很简单,我们需要创建一个规律,再通过我们的规律去解码。现在假设的规律为,我们将所有像素的 R 通道的值为奇数的时候我们创建的通道密码,举个简单的例子。


image.png


例如我们把以上当做是一个图形,加入他要和一个中文的 "一" 放进图像,例如我们将 "一" 放入第二行。按照我们的算法,我们的图像会变成这个样子。


image.png


解码的时候,我们拿到所有的奇数像素将它渲染出来,例如这里的 '5779' 是不是正好是一个 "一",下面就转化为实践。


解码过程


首先创建一个 canvas 标签。


<canvas id="canvas" width="256" height="256"></canvas>


var ctx = document.getElementById('canvas').getContext('2d');
var img = new Image();
var originalData;
img.onload = function () {
  // canvas像素信息
  ctx.drawImage(img, 0, 0);
  originalData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
  console.log()
  processData(ctx, originalData)
};
img.src = 'qiufeng-super.png';


我们打印出这个数组,会有一个非常大的数组,一共有 256 * 256 * 4 = 262144 个值。因为每个像素除了 RGB 外还有一个 alpha 通道,也就是我们常用的透明度。


image.png


上面也说了,我们的 R 通道为奇数的时候 ,就我们的解密密码。因此我们只需要所有的像素点的 R 通道为奇数的时候,将它填填充,不为奇数的时候就不填充,很快我们就能得到我们的隐藏图像。


var processData = function (ctx, originalData) {
    var data = originalData.data;
    for (var i = 0; i < data.length; i++) {
        if (i % 4 == 0) {
            // R分量
            if (data[i] % 2 == 0) {
                data[i] = 0;
            } else {
                data[i] = 255;
            }
        } else if (i % 4 == 3) {
            // alpha通道不做处理
            continue;
        } else {
            // 关闭其他分量,不关闭也不影响答案
            data[i] = 0;
        }
    }
    // 将结果绘制到画布
    ctx.putImageData(originalData, 0, 0);
}
processData(ctx, originalData)


解密完会出现类似于以下这个样子。


image.png


那我们如何加密的,那就相反的方式就可以啦。(这里都用了 不能说的秘密——前端也能玩的图片隐写术[8] 中的例子,= = 我也能写出一个例子,但是觉得没必要,别人已经写得很好了,我们只是讲述这个方法,需要代码来举例而已)


编码过程


加密呢,首先我们需要获取加密的图像信息。


var textData;
var ctx = document.getElementById('canvas').getContext('2d');
ctx.font = '30px Microsoft Yahei';
ctx.fillText('秋风的笔记', 60, 130);
textData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height).data;


然后提取加密信息在待加密的图片上进行处理。


var mergeData = function (ctx, newData, color, originalData) {
    var oData = originalData.data;
    var bit, offset;  // offset的作用是找到alpha通道值,这里需要大家自己动动脑筋
    switch (color) {
        case 'R':
            bit = 0;
            offset = 3;
            break;
        case 'G':
            bit = 1;
            offset = 2;
            break;
        case 'B':
            bit = 2;
            offset = 1;
            break;
    }
    for (var i = 0; i < oData.length; i++) {
        if (i % 4 == bit) {
            // 只处理目标通道
            if (newData[i + offset] === 0 && (oData[i] % 2 === 1)) {
                // 没有信息的像素,该通道最低位置0,但不要越界
                if (oData[i] === 255) {
                    oData[i]--;
                } else {
                    oData[i]++;
                }
            } else if (newData[i + offset] !== 0 && (oData[i] % 2 === 0)) {
                // // 有信息的像素,该通道最低位置1,可以想想上面的斑点效果是怎么实现的
                oData[i]++;
            }
        }
    }
    ctx.putImageData(originalData, 0, 0);
}


主要的思路还是我一开始所讲的,在有像素信息的点,将 R 偶数的通道+1。在没有像素点的地方将 R 通道转化成偶数,最后在 img.onload 调用 processData(ctx, originalData)


img.onload = function () {
  // 获取指定区域的canvas像素信息
  ctx.drawImage(img, 0, 0);
  originalData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
  console.log(originalData)
 processData(ctx, originalData)
};


以上方法就是一种比较简单的加密方式。以上代码都放到了仓库 watermark/demo/canvas-dark-watermark.html 路径下,方法都封装好了~。


但是实际过程需要更专业的加密方式,例如利用傅里叶变化公式,来进行频域制定数字盲水印,这里就不详细展开讲了,以后研究完再详细讲~


image.png


破解实践


听完上述的介绍,那么某在线设计网站是不是很有可能使用了暗水印呢?


当然啦,通过我对某在线设计网站的分析,我分析了以下几种情况,我们一一来进行测试。


image.png


我们先通过免费下载的图片来进行分析。打开 https://www.gaoding.com/design?id=13964513159025728&mode=user


image.png


image.png


通过实验(实验主要是去分析他各个场景下触发的请求),发现在下载免费图片的时候,发现它都会去向阿里云发送一个 POST 请求,这熟悉的请求域名以及熟悉的数据封装方式,这不就是 阿里云 OSS 客户端上传方式嘛。这就好办了,我们去查询一下阿里云是否有生成暗水印的相关方式,从而来看看某在线设计网站是否含有暗水印。很快我们就从官方文档搜索到了相关的文档,且对于低 QPS 是免费的。(这就是最好理解的连带效应,例如我们觉得耐克阿迪啥卖运动类服饰,你买了他的鞋子,可能还会想买他的衣服)


image.png


const { RPCClient } = require("@alicloud/pop-core");
var client = new RPCClient({
  endpoint: "http://imm.cn-shenzhen.aliyuncs.com",
  accessKeyId: 'xxx',
  accessKeySecret: 'xxx',
  apiVersion: "2017-09-06",
});
(async () => {
  try {
        var params = {
          Project: "test-project",
          ImageUri: "oss://watermark-shenzheng/source/20201009-182331-fd5a.png",
            TargetUri: "oss://watermark-shenzheng/dist/20201009-182331-fd5a-out.jpg",
            Model: "DWT"
        };
        var result = await client.request("DecodeBlindWatermark", params);
        console.log(result);
      } catch (err) {
        console.log(err);
      }
})()


我们写了一个demo进行了测试。由于阿里云含有多种暗水印加密方式,为啥我使用了 DWT 呢?因为其他几种都需要原图,而我们刚才的测试,他上传只会上传一个文件到 OSS ,因此大致上排除了需要原图的方案。


image.png


但是我们的结果却没有发现任何加密的迹象。


为什么我们会去猜想阿里云的图片暗水印的方式?因为从上传的角度来考虑,我们上传的图片 key 的地址即是我们下载的图片,也就是现在存在两种情况,一就是通过阿里云的盲水印方案,另一种就是上传前进行了水印的植入。现在看来不是阿里云水印的方案,那么只是能是上传前就有了水印。


这个过程就有两种情况,一是生成的过程中加入的水印,前端加入的水印。二是物料图含有水印。


对于第一种情况,我们可以通过 dom-to-image 这个库,在前端直接进行下载,或者使用截图的方式。目前通过直接下载和通过站点内生成,发现元素略有不同。


image.png


第一个为我通过 dom-to-image 的方式下载,第二种为站点内下载,明显大了一些。(有点怀疑他在图片生成中可能做了什么手脚)


但是感觉前端加密的方式比较容易破解,最坏的情况想到了对素材进行了加密,但是这样的话就无从破解了(但是查阅了一些资料,由于某在线设计网站站点素材大多是透明背景的,这种加密效果可能会弱一些,以后牛逼了再来补充)。目前这一块暂时还不清楚,探究止步于此了。


攻击实验


那如果一张图经过暗水印加密,他的抵抗攻击性又是如何呢?


image.png


这是一张通过阿里云 DWT暗水印进行的加密,解密后的样子为"秋风"字样,我们分别来测试一下。


加一些元素


image.png


结果: 识别效果不错


截图


image.png

结果: 识别效果不错

大小变化(缩小)

image.png

结果:识别效果不错

加蒙层

image.png


结果:直接就拉胯了。


可见,暗水印的抵抗攻击性还是蛮强的,是一种比较好的抵御攻击的方式~


最后



以上仅仅为技术交流~ 大家不要在实际的场景盲目使用,商业项目违规使用后果自负 ~ 或者期待一下我接下来想搞的这个个人免费首图生成器~ 喜欢文章的小伙伴可以点个赞哦 ~ 欢迎关注公众号 秋风的笔记 ,学习前端不迷路。


参考



https://imm.console.aliyun.com/cn-shenzhen/project?accounttraceid=1280c6af416744a38e9acf63c4e0878cjdet

https://help.aliyun.com/document_detail/138800.html?spm=a2c4g.11186623.6.656.3bd46bb4oglhEr

https://oss.console.aliyun.com/bucket/oss-cn-shenzhen/watermark-shenzheng/object?path=dist%2F

https://juejin.cn/post/6844903650054111246

https://www.zhihu.com/question/50677827/answer/122388524

https://www.zhihu.com/question/50735753


脚注


[1]

纸: https://zh.wikipedia.org/wiki/

[2]

target: https://developer.mozilla.org/zh-CN/docs/Web/API/event.target

[3]

CSS3 pointer-events:none应用举例及扩展 « 张鑫旭-鑫空间-鑫生活: https://www.zhangxinxu.com/wordpress/2011/12/css3-pointer-events-none-javascript/

[4]

CSS: https://developer.mozilla.org/zh-CN/docs/Web/CSS

[5]

chrome: https://developer.mozilla.org/zh-CN/docs/Glossary/Chrome

[6]

一文带你层层解锁「文件下载」的奥秘: https://juejin.cn/post/6867469476196155400

[7]

不能说的秘密——前端也能玩的图片隐写术 | AlloyTeam: http://www.alloyteam.com/2016/03/image-steganography/#prettyPhoto

[8]

不能说的秘密——前端也能玩的图片隐写术: http://www.alloyteam.com/2016/03/image-steganography/



相关文章
|
1月前
|
前端开发 JavaScript 数据可视化
前端实用网站
前端实用网站
|
18天前
|
缓存 监控 前端开发
前端性能优化实战:让你的网站快如闪电的十大秘籍
【9月更文挑战第3天】通过以上十大秘籍的实践,您可以显著提升网站的前端性能,让您的网站在竞争激烈的互联网环境中脱颖而出,为用户带来更加流畅和愉悦的体验。记住,前端性能优化是一个永无止境的过程,只有不断迭代和优化,才能保持网站的竞争力。
|
1月前
|
前端开发 Java Maven
【前端学java】全网最详细的maven安装与IDEA集成教程!
【8月更文挑战第12天】全网最详细的maven安装与IDEA集成教程!
66 2
【前端学java】全网最详细的maven安装与IDEA集成教程!
|
1月前
|
存储 前端开发 搜索推荐
Web前端网站(三)- 记事本
【8月更文挑战第8天】多种颜色搭配的动态粒子背景特效(粒子会随着鼠标的移动进行吸附,好看又好玩),左右摆动的文字特效,并且使用localStorage进行数据的持久化存储,使记事本的内容可以长期的保存在浏览器中,功能包括添加留言、显示留言、删除留言。每一行代码都有详细注释~~~大家可以尽情创作
40 5
Web前端网站(三)- 记事本
|
1月前
|
前端开发 搜索推荐 JavaScript
Web前端网站(四)- 音乐播放器
【8月更文挑战第9天】页面整体色调背景采用柔和渐变的方式呈现,与主页面的“毒药水式”色彩搭配形成了强烈的对比;周边花瓣缓缓飘落到水面之上形成涟漪。整体给人一种温馨、浪漫的感觉,还可以通过中间的3个按钮来控制音乐的切换和播放效果。每一行代码都有详细注释~~~大家可以尽情创作
23 2
Web前端网站(四)- 音乐播放器
|
1月前
|
前端开发 JavaScript
前端网站学习大全
这篇文章提供了前端网站开发学习的资源大全,涵盖了HTML常用标签和CSS常用样式,以及如何使用`<meta>`标签提供页面元信息和`lang`属性定义内容语言等基础知识。
前端网站学习大全
|
20天前
|
API Java 数据库连接
从平凡到卓越:Hibernate Criteria API 让你的数据库查询瞬间高大上,彻底告别复杂SQL!
【8月更文挑战第31天】构建复杂查询是数据库应用开发中的常见需求。Hibernate 的 Criteria API 以其强大和灵活的特点,允许开发者以面向对象的方式构建查询逻辑,同时具备 SQL 的表达力。本文将介绍 Criteria API 的基本用法并通过示例展示其实际应用。此 API 通过 API 构建查询条件而非直接编写查询语句,提高了代码的可读性和安全性。无论是简单的条件过滤还是复杂的分页和连接查询,Criteria API 均能胜任,有助于提升开发效率和应用的健壮性。
37 0
|
20天前
|
iOS开发 Android开发 MacOS
从零到全能开发者:解锁Uno Platform,一键跨越多平台应用开发的神奇之旅,让你的代码飞遍Windows、iOS、Android、macOS及Web,技术小白也能秒变跨平台大神!
【8月更文挑战第31天】从零开始,踏上使用Uno Platform开发跨平台应用的旅程。只需编写一次代码,即可轻松部署到Windows、iOS、macOS、Android及Web(通过WASM)等多个平台。Uno Platform为.NET生态带来前所未有的灵活性和效率,简化跨平台开发。首先确保安装了Visual Studio或VS Code及.NET SDK,然后选择合适的项目模板创建新项目。项目结构类似传统.NET MAUI或WPF项目,包含核心NuGet包。通过简单的按钮示例,你可以快速上手并构建应用。Uno Platform让你的技术探索之旅充满无限可能。
23 0
|
20天前
|
前端开发 数据处理 开发者
解锁Django模板系统终极奥义!揭秘高效前端渲染秘籍,让你的网站秒变炫酷黑科技!
【8月更文挑战第31天】Django作为Python的高级Web框架,内置的模板系统支持动态HTML渲染。本文通过在线书店案例,详细介绍Django模板系统的设置与高效渲染技巧,包括创建模板文件、编写视图函数及URL配置。通过合理使用过滤器、深度查询和模板继承等技巧,提升前端渲染效率和安全性,优化Web应用开发流程。
8 0
|
2月前
|
移动开发 前端开发 HTML5
零基础学前端系列教程 | 和前端谈恋爱的第005天——约会账单
零基础学前端系列教程 | 和前端谈恋爱的第005天——约会账单
21 1