新闻详情

新闻详情

首页 / 资讯中心 / 详情

IE浏览器PDF防下载防打印:服务端转图与水印令牌方案

发布时间:2026/10/1 15:27:26来源:尧图网络
IE浏览器PDF防下载防打印:服务端转图与水印令牌方案
IE浏览器里展示PDF同时把下载、打印、另存、复制这几条路全部堵死——这个需求我第一次接的时候以为是前端加点禁右键代码就完事结果整整折腾了三周才敢上线。场景很典型企业内部OA、合同审阅、财务报表、工单附件浏览器还锁在IE11甚至IE8内核的壳里业务方要求用户只能看、不能带走。难点从来不在怎么显示PDF而在怎么让用户拿不到原始文件。下面这套方案是我在两个项目里跑通并且稳定运行了一年多的版本包含服务端渲染、令牌接口、IE前端拦截和终端策略四层代码可以直接抄。1. 先认清防护边界划在哪里1.1 只要屏幕上看得见就一定拿得走先说个不太好听但必须接受的结论任何在浏览器里渲染出来的内容理论上都能被拿走。用户拿手机拍屏你拦不住用采集卡抓屏你也拦不住虚拟机里跑一套再复制走你还是拦不住。所以禁止下载、禁止打印、禁止另存、禁止复制这四个词在工程上真正准确的含义是把顺手就能拿走的成本抬高到需要专门动手、且留下痕迹的程度。这个认知决定了整个架构的取舍。如果业务方的预期是绝对安全、一点都流不出去那这个项目从一开始就不该用浏览器做应该走专用的文档阅读客户端或者干脆只允许在受控终端上看。但如果预期是防止普通员工随手另存一份发出去那浏览器方案完全够用而且成本低得多。我在项目启动会上一定会跟业务方确认三件事谁有权限看、外流后的追责手段是什么、终端环境能不能管控。这三件事的答案直接决定后面投入多少。如果终端能装管控软件那前端拦截做到七成就行如果终端完全不管用户可以用任意浏览器打开那前端拦截基本等于心理安慰全部压力都得压到服务端。1.2 三条技术路线的横向对比在IE里展示PDF业内能走的路其实就三条我把它们的实际表现列一下避免你重复踩坑路线实现方式禁止下载禁止打印禁止复制实际评价浏览器/插件原生渲染Adobe Reader ActiveX 或IE内置PDF处理弱弱弱只能藏工具栏按钮CtrlP照样弹窗前端JS渲染PDF.js 等库在页面里画中中中IE11基本跑不动1.x版本勉强能用但极卡服务端转图片后端把PDF烧录水印后渲染成图强强强推荐IE兼容性最好第一条约十年前是主流。做法是用object标签挂 Adobe 的 AcroPDF 控件然后通过 param 参数把工具栏、侧边栏、书签栏全关掉object idpdfViewer classidclsid:CA8A9780-280D-11CF-A24D-444553540000 width100% height100% param namesrc value/doc/preview?id123 / param nameshowToolbar valuefalse / param nameshowScrollbars valuetrue / param nameshowSidePane valuefalse / param nameshowBookmarks valuefalse / /object看着挺像那么回事工具栏确实没了。但问题是控件的print()和printWithDialog()方法依然可以被调用用户在页面里按 CtrlPIE 会把打印请求转给插件照样打。而且更致命的是——你传给src的那个地址用户在F12里能直接看到复制出来用别的工具打开就是原始PDF。我当时的结论是插件方案只适合防误操作不适合防故意。第二条路PDF.js 在 IE11 上是个灾难。新版本用了大量 ES6 语法和 WorkerIE11 直接报语法错误。能跑的只有 1.x 老版本但渲染一页A4要两三秒翻页时风扇狂转十页以上的文档基本没法用。我实测过放弃了。第三条路是最终选择逻辑很朴素既然浏览器拿到完整PDF文件就一定会泄露那就不给它完整文件。后端把PDF每页渲染成图片图片上烧录好水印一页一页按需下发。前端拿到的永远是位图没有文字层没有文件流CtrlS 存下来的是HTML右键另存的是张图。1.3 转图片方案顺带解决了三个问题选这条路的收益比想象中大。第一文本复制天然被禁掉。PDF里的文字层在渲染成像素的那一刻就没了用户框选半天选不中任何字除非上OCR。第二打印变得可控。打印出来的是一张图如果水印够密打印件反而成了溯源证据。第三IE兼容性最好。IE对img标签的支持是这个星球上最稳的东西比什么canvas、什么WebGL都稳。代价也有。图片比PDF大一份20页的合同转成图片大概3到8MB另外就是图片本身可以被右键另存所以禁止另存这一环必须靠前端拦截加服务端令牌配合后面细说。提示如果你的文档里有需要精确检索的正文内容转图片方案会让搜索失效。这种情况可以考虑双轨——图片用于展示另建一份只含标题和索引的全文检索库检索结果只返回页码不返回正文。2. 服务端渲染把PDF变成带水印的图2.1 渲染参数怎么定算一遍就清楚参数这块最容易拍脑袋我把计算过程写出来你照着调就行。A4纸是 210mm × 297mm换算成英寸是 8.27in × 11.69in。渲染DPI决定像素尺寸96 DPI794 × 1123 px屏幕上够看放大就糊150 DPI1240 × 1754 px显示器上看清晰这个是我的默认值200 DPI1654 × 2339 px为了应对高分屏和用户放大300 DPI2480 × 3508 px只有需要打印的场景才用体积翻三倍输出格式上PNG 无损但体积大一张150 DPI的扫描件能到 2MB 以上JPEG 有损但压得狠质量设 82 时同样的页面大概 200 到 400KB视觉上几乎看不出差别。IE不支持WebP别想着用它省流量。所以一份20页的文档150 DPI JPEG q82总量大约 4 到 8MB。这个量级不能一次性推给浏览器IE会直接卡死或者报内存不足。我的做法是分页懒加载首屏只拉前两页用户滚动到第三页附近再请求后面的。还有并发的问题。IE11 对同一个域名默认只开 6 条并发连接IE8以后都是这个数。如果一页文档你切成20个图片请求加上页面本身和其他资源队列会排得很长用户看到的是图一块一块慢慢冒出来。所以我做了个折中一页就是一张图不做切片但加上请求队列控制同时在服务器侧开启HTTP/1.1的keep-alive让6条连接循环复用实测比不控制队列快将近一倍。带宽估算也得做。假设峰值100人同时看文档每人拉3页每页300KB那就是90MB的瞬时流量。如果办公网出口只有百兆这个峰值能把网络打满。所以我在网关层加了每IP每分钟的请求数限流超了直接返回429让前端显示访问过于频繁请稍后重试不要让后端扛。2.2 PDFBox渲染加烧录水印的完整代码后端我用的是 Java PDFBox 2.0.x因为这个组合稳定、资料多、字体处理也成熟。Maven 依赖dependency groupIdorg.apache.pdfbox/groupId artifactIdpdfbox/artifactId version2.0.29/version /dependency渲染加烧录水印的核心方法import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.rendering.PDFRenderer; import org.apache.pdfbox.rendering.ImageType; import java.awt.*; import java.awt.geom.AffineTransform; import java.awt.image.BufferedImage; /** * 渲染单页并烧录水印 * param doc 已加载的PDF文档 * param pageIndex 页码从0开始 * param markText 水印文本建议包含用户名工号时间 */ public BufferedImage renderWithWatermark(PDDocument doc, int pageIndex, String markText, int dpi) throws Exception { PDFRenderer renderer new PDFRenderer(doc); // 开启降采样缩放时能显著提速画质损失可控 renderer.setSubsamplingAllowed(true); BufferedImage img renderer.renderImageWithDPI(pageIndex, dpi, ImageType.RGB); Graphics2D g img.createGraphics(); g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); // 透明度大约 15%太深会挡内容太浅不起威慑作用 g.setColor(new Color(128, 128, 128, 38)); int fontSize Math.max(14, img.getWidth() / 42); g.setFont(new Font(Microsoft YaHei, Font.PLAIN, fontSize)); AffineTransform origin g.getTransform(); int stepX Math.max(1, img.getWidth() / 3); int stepY Math.max(1, img.getHeight() / 6); // 关键从负坐标开始铺否则旋转后边角会留空 for (int y -img.getHeight(); y img.getHeight() * 2; y stepY) { for (int x -img.getWidth(); x img.getWidth() * 2; x stepX) { g.setTransform(origin); g.translate(x, y); g.rotate(Math.toRadians(-30)); g.drawString(markText, 0, 0); } } g.setTransform(origin); g.dispose(); return img; }这里有个坑必须提醒水印的坐标系陷阱。我第一次写的时候直接g.rotate(Math.toRadians(-30), cx, cy)然后把drawString的坐标算成从 0 开始结果画出来的水印整个飞到了画布外面一张都看不见。原因是旋转之后坐标系也跟着转了你在旋转后的坐标系里画(0,0)实际落点已经偏出去很远。正确写法就是上面那样——保存原始变换每次绘制前setTransform复位再translate到目标位置最后rotate让文字以目标点为原点旋转。另一个坑是中文变方框。服务器如果没有装中文字体drawString画出来的中文全是一个个小方块。解决方案是显式指定字体并且确保服务器装了对应字体。Linux 环境上我一般装fonts-wqy-zenhei或fonts-noto-cjk然后在代码里用Font.createFont(Font.TRUETYPE_FONT, new File(/usr/share/fonts/...))加载成实体字体对象比依赖 logical font 名可靠得多。保存成JPEG时要控制压缩质量import javax.imageio.*; import javax.imageio.stream.ImageOutputStream; import java.io.File; import java.util.Iterator; public void saveJpeg(BufferedImage img, File target, float quality) throws Exception { // 关掉 ImageIO 的磁盘缓存高并发时能省掉大量磁盘IO ImageIO.setUseCache(false); IteratorImageWriter it ImageIO.getImageWritersByFormatName(jpeg); if (!it.hasNext()) throw new IllegalStateException(no jpeg writer); ImageWriter writer it.next(); ImageWriteParam param writer.getDefaultWriteParam(); param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); param.setCompressionQuality(quality); try (ImageOutputStream ios ImageIO.createImageOutputStream(target)) { writer.setOutput(ios); writer.write(null, new IIOImage(img, null, null), param); } finally { writer.dispose(); } }ImageIO.setUseCache(false)这行别省。默认情况下 ImageIO 会用临时文件做缓存并发渲染几十页的时候磁盘IO会突然飙起来我有一回压测时磁盘使用率直接打满页面全部超时排查半天才定位到这里。2.3 Ghostscript 和 pdfium 的备选路线如果你的技术栈不是JavaGhostscript 是最省事的替代方案一条命令渲染整个文档gs -dSAFER -dBATCH -dNOPAUSE \ -sDEVICEjpeg \ -dTextAlphaBits4 -dGraphicsAlphaBits4 \ -r150 -dJPEGQ82 \ -sOutputFile/data/cache/%d.jpg \ /data/source/contract.pdf-r150是DPI-dJPEGQ82是质量输出文件名用%d会自动编号。速度比PDFBox快不少但水印就得用合图层的方式后处理了——先渲染无水印图再用图像库叠水印。这个两步流程会多一点磁盘IO不过Ghostscript本身很快总体还是划算的。还有一个选择是 pdfium它是各种浏览器的PDF渲染内核有无水印的Java绑定和命令行工具。它的字体fallback做得比PDFBox好遇到奇怪字体的PDF不容易出方框缺点是接口相对底层文档也少。我的建议是普通业务文档用PDFBox包含大量特殊字体的设计稿类PDF用pdfium。注意渲染是个CPU密集型操作。一台4核8G的服务器用150 DPI渲染A4页面单页大约150到300毫秒。一份20页的文档首次渲染大概要5秒。这个时间必须放进缓存预热流程不能让用户等——我的做法是文档上传时就异步触发全量渲染把结果落盘用户打开时直接读缓存图。3. 接口层才是真正的禁止下载3.1 一次性令牌与签名URL的设计前端拦截是纸糊的墙真正的门锁在服务端。核心思路是图片地址不可预测、不可复用、不可分享。每个URL都带上四个参数文档ID、页码、过期时间戳、HMAC签名。签名内容把文档ID、页码、过期时间、用户ID、客户端IP全部拼进去用服务端密钥做一次HMAC-SHA256。这样即使有人把URL复制给别人换个IP就打不开过了一分钟也失效。public String buildPageUrl(String docId, int pageNo, String userId, String clientIp) { long exp System.currentTimeMillis() 60_000L; // 60秒有效期 String raw docId | pageNo | userId | clientIp | exp; String sign HmacUtils.hmacSha256Hex(SECRET_KEY, raw); return /doc/page/ pageNo ?d docId u userId e exp s sign; }服务端接收时反向校验GetMapping(/doc/page/{pageNo}) public void page(PathVariable int pageNo, RequestParam(d) String docId, RequestParam(u) String userId, RequestParam(e) long exp, RequestParam(s) String sign, HttpServletRequest req, HttpServletResponse resp) throws IOException { String clientIp IpUtils.clientIp(req); long now System.currentTimeMillis(); if (now exp) { resp.sendError(410, expired); return; } String raw docId | pageNo | userId | clientIp | exp; if (!HmacUtils.hmacSha256Hex(SECRET_KEY, raw).equals(sign)) { resp.sendError(403, forbidden); return; } // 校验会话与文档权限 if (!permService.canView(userId, docId)) { resp.sendError(403, no permission); return; } resp.setContentType(image/jpeg); resp.setHeader(Cache-Control, no-store, no-cache, must-revalidate, max-age0); resp.setHeader(Pragma, no-cache); resp.setHeader(Expires, 0); resp.setHeader(X-Content-Type-Options, nosniff); resp.setHeader(Content-Disposition, inline); File img cacheService.getPageImage(docId, pageNo); if (img null || !img.exists()) { img renderService.renderAndCache(docId, pageNo, userId, clientIp); } try (InputStream in new FileInputStream(img)) { IOUtils.copy(in, resp.getOutputStream()); } auditService.log(userId, docId, pageNo, clientIp); }这段代码里有几个点值得单独说。过期时间给60秒是有讲究的——太短页面加载会失败太长用户就有足够时间把URL抄下来分享。60秒够IE把当前屏的图拉完用户想复制出去再打开基本已经失效。签名绑IP是防分享的关键虽然同一办公网出口NAT后IP可能一样但至少挡住了把链接发到外网。签名绑用户ID保证了链接不能跨账号使用。我见过有些人图省事直接用文档ID和一个固定哈希做URL参数结果用户改个数字就能看别人的文档。这种低级问题在安全评审里一定会被打回别偷这个懒。3.2 响应头与IE的缓存脾气IE的缓存策略跟现代浏览器不太一样它对Cache-Control: no-store的遵守程度在部分版本上很让人头疼。我实测遇到过用户退出登录后再点浏览器的后退按钮图片居然还能从缓存里显示出来。解决办法是三层响应头一起上Cache-Control: no-store, no-cache, must-revalidate, max-age0 Pragma: no-cache Expires: 0Pragma是HTTP/1.0的老东西但IE至今认它。三个头一起写IE11上基本就干净了。另外还有一个骚操作可以顺手加上在图片URL后面追一个每次都会变的随机数参数。因为URL变了IE就不会命中缓存。这个参数不用传到后端后端忽略它就行。var url pageUrl _r Math.random().toString(36).substring(2);3.3 访问审计与异常行为识别这个功能很多项目会漏掉但它才是外流之后能追责的依据。每次图片请求都记一条日志谁、什么时候、看了哪个文档的哪一页、客户端IP是什么。这些数据平时没人看出事的时候价值千金。我在日志基础上加了一个简单的异常检测规则同一个用户在5分钟内拉取超过200页图片或者同一个账号在3个不同IP上打开同一个文档就触发告警。正常看文档不可能触发但如果是脚本批量下载一定会。提示审计日志里千万不要记录文档正文内容只记录行为元数据。一方面是合规要求另一方面日志体积也会失控。4. IE前端这层的拦截怎么写4.1 禁右键、禁选中、禁拖拽的ES5写法IE11 不支持箭头函数、不支持let/const的作用域行为、不支持模板字符串。写这段代码的时候老老实实回到 ES5别用任何新语法否则就是在给自己挖坑。(function () { use strict; function stop(e) { e e || window.event; if (e.preventDefault) { e.preventDefault(); } e.returnValue false; e.cancelBubble true; return false; } // 右键菜单 document.oncontextmenu stop; // 文本选中 document.onselectstart stop; // 拖拽图片 document.ondragstart stop; // 复制剪切粘贴 document.oncopy stop; document.oncut stop; document.onpaste stop; // F1 帮助 document.onhelp stop; window.onhelp stop; })();这里有个重要提醒user-select: none这套CSS在IE10以上才有效IE9及以下只能靠onselectstart拦截。如果你要兼容IE8那就必须两个都上。.noselect { -ms-user-select: none; /* IE10 */ -moz-user-select: none; -webkit-user-select: none; user-select: none; -ms-touch-select: none; }图片上再补两个属性减少被拖拽的概率img src... draggablefalse galleryimgno unselectableon /galleryimgno是IE专属的作用是去掉图片左上角那个保存/打印/邮件的悬浮工具条。这个工具条在IE6到IE9上很烦人鼠标一划过去就冒出来用户点一下就能把图存走。加上这个属性它就消失了。现代浏览器忽略这个属性不影响其他环境。4.2 快捷键拦截与打印拦截键盘拦截要注意一个细节IE的按键事件里keyCode和which在不同版本上取值不一样保险的写法是两个都取哪个有值用哪个。document.onkeydown function (e) { e e || window.event; var code e.keyCode || e.which; var ctrl e.ctrlKey || e.metaKey; if (code 123) { return stop(e); } // F12 开发者工具 if (e.shiftKey code 121) { return stop(e); } // ShiftF10 右键菜单 if (e.altKey code 115) { return stop(e); } // AltF4 if (ctrl) { switch (code) { case 83: // CtrlS 保存 case 80: // CtrlP 打印 case 67: // CtrlC 复制 case 65: // CtrlA 全选 case 85: // CtrlU 查看源码 case 79: // CtrlO 打开 case 78: // CtrlN 新窗口 return stop(e); } } return true; };打印这块要坦白说IE里 CtrlP 的拦截并不百分之百可靠。有些版本上onkeydown能拦住有些版本浏览器会先弹打印对话框事件才传到页面。所以还需要加打印前后的事件黑洞window.onbeforeprint function () { var nodes document.getElementsByClassName(page); for (var i 0; i nodes.length; i) { nodes[i].style.visibility hidden; } }; window.onafterprint function () { var nodes document.getElementsByClassName(page); for (var i 0; i nodes.length; i) { nodes[i].style.visibility visible; } };配合CSS层的打印媒体查询media print { html, body { display: none !important; visibility: hidden !important; } }这样即使用户绕过快捷键拦截打出了打印预览页面上也是一片空白。代价是浪费一张纸但内容确实没打出来。4.3 IE上必须知道的几个兼容坑坑一base64图片的尺寸限制。IE8 时代img srcdata:image/jpeg;base64,...超过 32KB 就不显示IE9 以后放宽了但大字符串拼接在IE11的引擎上依然很慢——它没有现代JS引擎那种字符串优化拼接一个1MB的base64会明显卡顿甚至假死。所以永远不要试图把图片转成base64塞进页面老老实实用URL让浏览器自己去请求。坑二不要用 WebP。IE完全不支持会显示成红叉。统一用JPEG如果对清晰度要求极高且不在乎体积用PNG。坑三X-UA-Compatible必须写。如果用户的IE开启了兼容性视图页面会用老引擎渲染你的CSS和JS可能全部失效。meta http-equivX-UA-Compatible contentIEedge / meta charsetutf-8 /坑四图片懒加载要自己写。IE不支持loadinglazy这个原生属性。我的做法是给每个占位div固定高度监听window.onscroll滚动到可见区域时才设置img.src。注意IE的getBoundingClientRect()返回值是相对视口的配合document.documentElement.scrollTop计算别忘了IE的怪癖模式下scrollTop要从document.body取。坑五内存释放。一次加载几十张大图后即使把img元素从DOM里移除IE也不一定立即释放内存浏览久了会卡。我一般控制同时存在的图片不超过10张超出就主动把img.src置空帮助回收。5. 终端策略和溯源的纵深防御5.1 IE kiosk模式与组策略如果文档查看的场景是在受控的企业内网终端上那可以利用IE自带的能力再加一道锁。IE有个 kiosk 全屏模式iexplore.exe -k https://oa.example.com/view?docId123这个模式下没有菜单栏、没有地址栏、没有工具栏用户唯一能操作的就是页面内容加一个右上角的关闭按钮。F11 不能退出AltF4 可以。这对公共查询终端特别合适——比如办事大厅里那台给群众查资料的机器。再往上就是组策略。Windows域环境下可以在域策略里统一配置IE的菜单项和快捷键行为把打印、另存为这些入口从根上关掉。这不是应用层能做的事需要终端运维配合。做方案评审的时候我会把这条明确写出来让甲方评估可行性能配上就配上配不上就说明风险由前端拦截承担。5.2 明水印和暗水印用途完全不同水印有两个层次很多人会混为一谈。明水印是威慑和溯源。就是我在2.2节代码里烧录的那个内容一般是张三 工号10086 2024-06-12 14:30。用户一打开就看到自己的名字铺满整页心理上就不太敢截图外传因为传出去一眼就知道是谁泄露的。这个必须烧录到像素里不能是前端叠的CSS图层——CSS水印用F12删掉就没了纯属心理安慰。暗水印是事后追责。思路是把用户ID、文档ID编码进像素的最低位肉眼完全看不出来事后拿到外流的图片可以提取出这串信息。用Python实现很简单import numpy as np from PIL import Image def embed_lsb(src_path, bits, out_path): bits: 0/1 组成的列表代表要隐藏的信息 arr np.array(Image.open(src_path).convert(RGB)) flat arr.flatten() n min(len(bits), flat.size) b np.array(bits[:n], dtypenp.uint8) flat[:n] (flat[:n] 0xFE) | b Image.fromarray(flat.reshape(arr.shape)).save(out_path, quality95)但我必须说清楚它的局限LSB水印扛不住JPEG二次压缩。用户如果把图重新存一次JPEG最低位就全乱了。所以暗水印只在一种情况下有用——截屏工具直接保存PNG、或者打印扫描后做原图比对。它是补充手段不是主力。明水印才是真正干活的。真要做好溯源还有一招更粗暴但有效每份文档在像素级别做微小的、不影响阅读的差异比如某几个像素点亮度差1。每个人拿到的图略有不同外流后比对这几处就能定位到人。代价是每份文档都要单独渲染一遍存储成本翻很多倍。只有涉密级别很高的文档才值得这么做。6. 常见问题速查与踩坑实录6.1 高频故障对照表这套方案跑了两年多遇到的问题基本都在这张表里了现象根本原因处理方式IE里图片显示成空框或红叉响应的Content-Type不对必须是image/jpeg配合nosniff图片能拖出幽灵缩略图浏览器默认允许拖拽ondragstart返回false加上draggablefalse退出后后退仍能看到图IE缓存未彻底失效三层缓存头 URL追随机数水印跑到画布外面Graphics2D旋转坐标系先translate再rotate从负坐标开始铺中文水印变成方框服务器缺中文字体装思源黑体用Font.createFont显式加载20页以上文档IE卡死一次性拉取图片过多分页懒加载同时存在的图不超过10张F12里能直接看到图片URL前端拦截本就是浅层令牌60秒有效 绑定用户和IPCtrlP 仍弹出打印窗口IE对keydown拦截不严格配合 kiosk 模式或组策略页面加载特别慢首次渲染没预热上传时异步预渲染落盘缓存高并发时磁盘跑满ImageIO默认使用磁盘缓存ImageIO.setUseCache(false)有的PDF渲染出来是空白文档加密或有权限限制加载时传空密码捕获InvalidPasswordException图片在放大后很模糊DPI设置过低提到200 DPI或按缩放级别准备两套尺寸6.2 几个我实打实踩过的坑第一个坑以为前端拦截就够用了。项目初期我们只做了禁右键、禁打印那套JS自测的时候觉得很完美——右键点不出来、CtrlP不弹窗、框选也选不中。结果上线第二天业务方就收到一份流出去的PDF。用户的操作是在页面空白处右键 → 查看源文件 → 直接找到了PDF的原始地址。那一刻我才彻底明白前端拦截的边界在哪里后面把架构整个改成了服务端转图。第二个坑图片有水印但水印能删。我们最早的版本水印是前端用CSS叠加的一层div看着挺漂亮。后来同事演示了怎么用F12把那个div删掉全程不到五秒。从此所有水印一律走服务端烧录前端做的任何保护我都不再当成安全措施只当成体验优化。第三个坑token有效期设成了24小时。一开始觉得用户可能看得慢给个长点的有效期比较友好。后果是这个URL变成了一个可以随便分享的长期链接谁拿到都能看。改成60秒之后有个副作用需要注意用户如果读一页停下来思考很久再去拉下一页旧的token可能已经过期了。所以我在前端加了一个保活机制页面上每30秒静默请求一次刷新接口重新签发一批新的分页URL并替换img.src。这样用户感觉不到安全边界也守住了。第四个坑IE11的内存。有份80页的招标文件用户直接从头滚到尾IE内存飙到1.2G然后整个页面崩了。后来我加了严格的图片回收逻辑滚动离开视口超过5屏的图片一律把src置空、从DOM里摘掉。用户往上滚回去的时候重新加载多花几百毫秒但页面不会崩。第五个坑字体的问题排查了很久。水印中文在测试服务器上好好的一到生产就是方框。查了半天发现测试机是Windows Server自带微软雅黑生产是CentOS最小化安装一个中文字体都没有。这个坑很典型——所有涉及文字绘制的服务端功能上线前都要在目标环境验证字体别只在开发机上测。第六个坑并发下的连接池。渲染是CPU密集的如果请求线程直接去做渲染20个并发就能把线程池打满后面的请求全部排队超时。我的做法是渲染任务丢到独立的固定大小线程池核数 1请求线程只去缓存里取图取不到就返回一个渲染中的占位图前端每隔一秒重试一次。这个模式很土但特别稳。最后再分享一个小心思我把占位图和真实图的尺寸做成完全一致的都是按DPI算好的固定像素。这样图片从占位换成真图的时候页面不会跳动用户视觉上很平滑。IE对这种布局稳定性其实很敏感如果图片尺寸不固定加载过程中页面会来回抖体验非常差。关于这个方案还能往哪走我个人的想法是在渲染环节加一层动态清晰度控制——首次加载用低DPI快速铺满用户放大某一块的时候再局部渲染高DPI。pdfium有按区域渲染的接口做成瓦片式的按需高清理论上能把首次加载时间从5秒压到1秒以内。这个我还在试等跑通了再单独写一篇。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

Ubuntu 22.04 安装 Miniconda、PyTorch 与 YOLOv8 并完成 CPU 推理 2026/10/1 16:12:42

Ubuntu 22.04 安装 Miniconda、PyTorch 与 YOLOv8 并完成 CPU 推理

Ubuntu 22.04 安装 Miniconda、PyTorch 与 YOLOv8 并完成 CPU 推理本文记录在 VMware Ubuntu 22.04 虚拟机中安装 Miniconda、PyTorch CPU 版和 Ultralytics YOLOv8,并使用 YOLOv8n 对示例图片进行目标检测的完整过程。一、实验环境项目配置操作系统Ubuntu 22.04.5 …

阅读更多 →
9.30blog 2026/10/1 16:12:42

9.30blog

2026年9月24日 14:09 1.printf 占位符的作用是用“”后面的数据或词句替换语句中的占位符(指定格式) 例如:printf(“There are %d apples”,2); 生成There are 2 apples 例2printf(“%s will come tonight\n”,“张三”); 例3printf(“%s say…

阅读更多 →
带父母孩子去阳澄湖吃蟹,湖景包厢到底适不适合一家人坐进去 2026/10/1 16:12:42

带父母孩子去阳澄湖吃蟹,湖景包厢到底适不适合一家人坐进去

先说结论:适合,但前提是你对“湖景包厢”的期待不是一块招牌,而是老人孩子坐下来之后真实的体验。带家人出门吃饭,最容易出问题的往往不是菜好不好吃,而是环境名不副实、桌子挤、上菜慢,老人孩子都别扭。我…

阅读更多 →
(146页PPT)某大型企业基于战略的全面绩效管理体系设计方案(附下载方式) 2026/10/1 16:12:42

(146页PPT)某大型企业基于战略的全面绩效管理体系设计方案(附下载方式)

篇幅所限,本文只提供部分资料内容,完整资料请看下面链接 (146页PPT)某大型企业基于战略的全面绩效管理体系设计方案.pptx_基于物联网的消防监控方案资源-CSDN下载 资料解读:《(146页PPT)某大型…

阅读更多 →
Designer Skills五大集合全景图:33个插件覆盖研究到交付,找到最适合你的安装路径 2026/10/1 16:12:42

Designer Skills五大集合全景图:33个插件覆盖研究到交付,找到最适合你的安装路径

Designer Skills五大集合全景图:33个插件覆盖研究到交付,找到最适合你的安装路径 【免费下载链接】designer-skills Designer Skills Collection: agentic skills, commands, and plugins for design — from research to systems, UI, interaction, and…

阅读更多 →
Unity热更新安全加固:从AssetBundle清单签名到本地缓存防篡改 2026/10/1 16:12:35

Unity热更新安全加固:从AssetBundle清单签名到本地缓存防篡改

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉