ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

IE浏览器PDF禁止下载打印复制另存:四层权限管控与溯源方案

IE浏览器PDF禁止下载打印复制另存:四层权限管控与溯源方案 IE浏览器里打开一份PDF然后要求用户既不能下载、不能打印、不能另存、也不能复制里面的文字这个需求在企业内网、政务办公、教务系统里出现的频率高得离谱。核心关键词就四个IE浏览器、PDF、禁止下载、禁止打印、禁止复制。很多人第一反应是找个前端控件把按钮藏起来就完事了结果上线第一天就被人右键另存或者CtrlP轻松绕过。我在几个内网项目里反复折腾过这套东西踩的坑足够写一本书所以这里把完整思路和能直接抄的实现整理出来。这套方案适合还在维护IE内核系统的前端、后端同学也适合需要给PDF做轻量级内容管控的产品和运维读完你至少能搞清楚哪些禁止是真能落地的哪些只是提高门槛而已。1. 先认清IE环境下PDF管控的真实边界1.1 四个禁止分别属于哪一层别再混为一谈很多人把禁止下载禁止打印禁止另存禁止复制当成一个整体去解决这是第一个大坑。它们其实分属三个完全不同的层面混在一起想方案必然翻车。禁止复制属于内容层。它关心的是PDF里的文字能不能被选中、能不能被提取。这一层可以写进PDF文件本身的权限位里也可以靠前端遮罩层去做相对最容易见效。禁止打印一半在内容层一半在表现层。PDF权限位里有专门禁止打印的标志但同时用户还能通过浏览器打印整个网页所以还得配合CSS的打印样式覆盖和键盘拦截。禁止下载和另存属于传输层和文件层。只要PDF字节流真的到了浏览器理论上它就已经在用户机器上了你拦不住一个铁了心的人。能做的只是让顺手保存变得困难、让保存下来的文件打不开或者没意义。记住一句话前端做的是防线服务端和水印做的是门槛真正有价值的是可追责而不是绝对禁止。把这几层分清楚之后方案就清晰了内容层用PDF权限加密表现层用前端拦截和遮罩传输层用动态token和访问控制最后用水印兜底溯源。四层叠起来才称得上完美解决。1.2 为什么IE这个变量让难度直接翻倍换成现代ChromePDF预览有内置的原生插件方案会轻松很多。但IE的处境很尴尬它没有现代浏览器那种成熟的PDF渲染能力要么依赖外部ActiveX插件要么用JavaScript方案硬渲染。IE的第二个麻烦是JS引擎老。ES6的箭头函数、let、const、Promise在IE11上基本没法直接跑你要么用ES5写要么靠Babel转译要么引入polyfill。很多现成的PDF库写着支持现代浏览器实际在IE里加载直接报错控制台一片红。第三个麻烦是插件依赖。老系统里Adobe Acrobat Reader插件一旦版本不对、被禁用、或者安全策略拦截embed和object标签就渲染不出来页面一片空白。这也是为什么越来越多项目转向pdf.js这类纯JS方案——虽然它慢但至少不依赖外部插件可控性强。所以IE环境下做PDF管控本质是在一个先天不足的环境里用更多的补丁去补浏览器的短板。理解了这一点你才不会对某次系统更新后全崩了感到意外。2. IE里展示PDF的三条路我为什么最后选了混合方案2.1 Adobe Acrobat插件方案能力最强但最不可控早期项目基本都是这条路用object标签把PDF塞进页面靠Adobe的ActiveX控件渲染。object classidclsid:CA8A9780-280D-11CF-A24D-444553540000 width100% height100% idpdfViewer param namesrc value/api/pdf/view?tokenxxx / param nametoolbar value0 / /objecttoolbar设成0可以隐藏Adobe控件自带的工具栏这招在当年确实能挡住一部分用户。它还支持传一些参数来控制打印、保存按钮的显示。但这个方案的问题非常致命。首先它完全依赖用户机器上装了Adobe Acrobat Reader而且版本得匹配。我遇到过用户机器上装的是福昕阅读器classid直接不认页面空白。其次从较新版本开始Adobe为了安全考虑逐步弱化了ActiveX控件的可配置项你能关掉的东西越来越少。最后安全策略稍严一点的内网环境会把这类ActiveX控件直接拦掉。所以这个方案我现在的定位是只在存量老系统里做兼容兜底新项目不碰。2.2 pdf.js兼容版方案可控性高是IE下的主力pdf.js是Mozilla开源的纯JS渲染库它不需要任何外部插件把PDF渲染成canvas画到页面上。这一点对管控极其友好——因为渲染结果是canvas文字不是DOM节点用户根本选不中天然就带上了禁止复制的底色。关键是要选对版本。IE11能跑的是pdf.js比较早的版本比如1.x和2.x的某些版本较新的3.x、4.x用了大量现代语法在IE里会直接报错。我一般锁2.x系列的某个稳定版配合ES5的构建产物使用。加载方式大致是这样// 注意IE下必须用ES5写法不能有箭头函数和Promise var loadingTask pdfjsLib.getDocument({ url: /api/pdf/stream?tokenxxx, // 关闭范围请求IE下兼容性更好 disableRange: true, disableStream: true }); loadingTask.then(function (pdf) { return pdf.getPage(1); }).then(function (page) { var viewport page.getViewport({ scale: 1.2 }); var canvas document.getElementById(pdfCanvas); var context canvas.getContext(2d); canvas.width viewport.width; canvas.height viewport.height; var renderContext { canvasContext: context, viewport: viewport }; return page.render(renderContext).promise; });disableRange和disableStream这两个参数在IE里建议打开。IE对HTTP范围请求和流式响应的处理有点脾气关掉它们虽然会牺牲一点大文件的加载性能但能换来稳定这笔账划算。2.3 三条路对比我的实际选择方案IE兼容性禁止复制难度禁止打印难度依赖我的评价Adobe ActiveX插件依赖本机插件中靠控件参数依赖插件配置高存量兜底新项目不用pdf.js兼容版好纯JS低canvas天然不可选需配合CSS无主力方案iframe直接指向PDF好高浏览器原生渲染几乎无法控无只适合公开文档看表格就明白pdf.js在可控性这一列几乎全面领先。它唯一的代价是渲染性能页面多、图大的PDF在IE里翻页会卡。但这个性能代价换来的是内容层的完全掌控我认为非常值。我的实际做法通常是主渲染用pdf.js把canvas输出到页面外面再套一层前端拦截和服务端token管控。这就是后面几节要展开的核心。3. 内容层把禁止复制、禁止打印写进PDF文件本身3.1 PDF加密字典和权限位到底是什么前端那一层终究是软件层面的约定用户关掉JS就能绕过。真正硬一点的是把限制写进PDF文件本身——PDF标准里支持对文档加密并且可以精细化地控制允许打印允许复制允许修改这些权限。原理不复杂。PDF文件里有一个加密字典记录了加密算法如标准加密的RC4或AES、密钥长度以及一个权限标志位。这个权限位是一个整数它的每一个二进制位代表一项权限第3位允许打印、第5位允许复制、第6位允许修改等等。阅读器打开文件时会读取这个权限位然后决定工具栏上哪些按钮可用。关键点来了这个权限位是由所有者密码保护的。用户用用户密码打开文件只能看无权改变权限只有知道所有者密码的人才能修改权限设置。这就意味着你把打印和复制权限关掉后普通用户即便拿到文件也没法通过正常途径解开。这里必须坦白PDF权限加密是可以被专业工具破解的它防的是普通人不是破解者。所以它必须和水印、服务端管控配合使用单靠它撑不起整套方案。3.2 用iText设置权限的完整实操实际项目里我用得最多的是iText这个Java库。思路是读取原始PDF设置权限位重新输出加密版本。import com.itextpdf.text.pdf.PdfReader; import com.itextpdf.text.pdf.PdfStamper; import com.itextpdf.text.pdf.PdfWriter; import java.io.FileOutputStream; public class PdfPermissionUtil { public static void encrypt(String src, String dest, String userPass, String ownerPass) throws Exception { PdfReader reader new PdfReader(src); PdfStamper stamper new PdfStamper(reader, new FileOutputStream(dest)); // 权限位这里设置成 0 表示不允许打印、复制、修改 // 如果只想禁止打印用 PdfWriter.ALLOW_COPY 即可 int permissions 0; stamper.setEncryption( userPass.getBytes(UTF-8), // 用户口令给查看者 ownerPass.getBytes(UTF-8), // 所有者口令自己留着 permissions, // 权限位 PdfWriter.STANDARD_ENCRYPTION_128 // 128位标准加密 ); stamper.close(); reader.close(); } }几个参数要重点说清楚permissions 0意味着什么都不允许包括打印和复制。如果你其实还想让用户打印只是不想让他复制那就传PdfWriter.ALLOW_PRINTING。这个位是可以组合的用按位或拼起来比如ALLOW_PRINTING | ALLOW_FILL_IN表示允许打印和填写表单。加密位数我一般用128位。40位太弱AES 256位在有些老版本的阅读器上兼容性不如128位标准加密稳考虑到项目里可能还有老阅读器128位是兼容和安全的平衡点。用户口令和所有者口令千万别设成一样。设成一样权限就形同虚设了。我的习惯是用户口令用一串随机字符其实很多场景可以直接留空让文件打开时不弹密码框所有者口令用系统里生成的长随机串存到配置或者数据库里绝不硬编码在代码里。3.3 两个容易被忽略的坑第一个坑设置了权限位但在某些阅读器里打印按钮依然是亮的。这不是你设错了而是部分阅读器对权限位的尊重程度不一。福昕、WPS、Adobe对权限位的处理各有差异。所以不能只靠这一层前端拦截和打印样式覆盖必须同时上。第二个坑加密后文件体积和加载速度会受影响。加密本身会带来一些开销尤其对大文件。如果你的PDF动辄几十兆加密后再丢给IE渲染体验会很差。建议在服务端缓存加密后的结果用文件哈希做key避免每次请求都重新加密一遍。实操心得把原始PDF和加密PDF分开存加密结果按文件ID权限配置缓存。这样改权限配置只需重新加密一次而不是每次请求都跑一遍iText。4. 传输层禁止下载和另存的真防线在服务端4.1 动态token加一次性链接让复制地址失效前端再怎么藏按钮只要PDF的URL是固定的、可直接访问的用户复制一下地址在新窗口打开就能下载。所以第一件事是不要暴露真实的PDF访问路径。我的做法是走一个后端接口前端只拿一个一次性的tokenRestController public class PdfController { GetMapping(/api/pdf/stream) public void stream(RequestParam String token, HttpServletResponse response) throws IOException { // 校验token是否有效、是否过期、是否已使用 PdfToken pt tokenService.validate(token); if (pt null) { response.setStatus(403); return; } // 标记token已使用一次性 tokenService.consume(token); // 按权限要求实时加密或读取缓存 byte[] data pdfService.getEncryptedPdf(pt.getFileId()); response.setContentType(application/pdf); response.setHeader(Content-Disposition, inline; filenamepreview.pdf); response.setHeader(Cache-Control, no-store, no-cache, must-revalidate); response.setHeader(Pragma, no-cache); response.setHeader(Expires, 0); response.getOutputStream().write(data); } }token要满足几个条件有效期短一般5到30分钟、绑定用户会话别人拿到也用不了、一次性用过即失效。这样即使有人把URL抓下来过一会儿再打开就是403。有人会问pdf.js渲染时不就是要请求这个URL吗token一次性用掉后续翻页怎么办这里有个取舍。如果PDF是多页分片加载的token就不能是一次性的改成短时效绑定会话更合适。我一般的策略是token在有效期内可复用但绑定IP或会话ID并在首次请求后再生一个短时续期token。这样既能支持分片加载又能防止URL被长期盗用。4.2 响应头里那几个关键字段的作用Content-Disposition设成inline而不是attachment意思是让浏览器尝试内联展示而不是弹下载框。这招在IE里对直接访问PDF的场景有用能减少点击就下载的尴尬。Cache-Control: no-store这几个缓存的头一定要加。IE的缓存机制特别执着不加这些头用户看过的PDF会留在临时文件夹里从缓存里就能把文件捞出来。加上之后IE每次都得重新向服务器请求配合token校验缓存照就断了。X-Content-Type-Options: nosniff也建议加防止浏览器嗅探内容类型后做出意料之外的处理。4.3 更狠一点把PDF转成图片再返回如果你的场景对禁止复制文字要求极高有一个釜底抽薪的办法在服务端把PDF每一页渲染成图片前端只加载图片。图片是像素没有文字层用户复制不到任何内容右键保存的也只是图片想还原成PDF还得自己拼门槛高得多。代价也很明显图片体积通常比原PDF大尤其是文字型PDF加载和翻页变慢如果用户需要复制文本来做别的事比如引用、检索这个方案直接堵死了。所以它适合只读展示、绝不允许外传的场景不适合需要正常办公的场景。提示图片方案和pdf.js方案可以做成配置项。高密级文件走图片渲染普通文件走pdf.js加权限加密用一套接口按策略分流。5. 表现层在IE页面里把右键、CtrlS、CtrlP挨个堵住5.1 需要拦截的事件清单和对应写法这一层是前端最直观的工作虽然它是软防线但用户感知最强很多场景下用户一看到右键被禁就放弃了。需要覆盖的事件我整理成下面这张清单。事件目的拦截方式contextmenu禁右键菜单返回false并阻止默认行为keydown CtrlS禁另存判断按键并阻止keydown CtrlP禁打印判断按键并阻止keydown CtrlC禁复制判断按键并阻止keydown F12挡调试阻止默认行为selectstart禁选中阻止默认行为dragstart禁拖拽阻止默认行为beforeprint打印前处理用于替换打印内容IE下的写成这样注意全是ES5语法document.oncontextmenu function () { return false; }; document.addEventListener(keydown, function (e) { e e || window.event; var key e.keyCode || e.which; // Ctrl 组合键 if (e.ctrlKey) { // S83, P80, C67, A65 if (key 83 || key 80 || key 67 || key 65) { if (e.preventDefault) { e.preventDefault(); } e.returnValue false; return false; } } // F12 123 if (key 123) { if (e.preventDefault) { e.preventDefault(); } e.returnValue false; return false; } }, false); document.onselectstart function () { return false; }; document.ondragstart function () { return false; };IE的事件模型和现代浏览器不一样addEventListener在IE9以上能用但为了兼容更老的IE得准备attachEvent的降级写法。而且e.preventDefault()在IE里不一定生效得靠e.returnValue false兜底。这两个细节不注意代码在IE里就是白写。5.2 用透明遮罩层把复制路径彻底封死即使idor.js渲染的是canvas用户还是可能用截图工具截图。截图这个真拦不住但我们可以让它截得难受——在canvas上面盖一层绝对定位的透明div遮住整个渲染区域。div classpdf-wrap styleposition:relative; canvas idpdfCanvas/canvas div classshield styleposition:absolute;top:0;left:0; right:0;bottom:0;z-index:10;background:transparent;/div /div这层遮罩把所有鼠标事件都吃掉了用户点不到canvas下面的内容也没法通过长按、拖拽去选择。配合前面的onselectstart复制路径基本堵死。遮罩层还有个进阶用法在遮罩上动态绘制用户名、时间、IP组成的水印。这样即便用户截图外传也能追到是谁泄露的。水印的透明度调到不影响阅读的程度位置可以做成平铺或者斜角。注意遮罩层不能挡住滚动和控制。如果PDF需要翻页、缩放得把操作按钮做成独立的层级放在遮罩之上或者用自定义的翻页控件不要依赖用户的滚轮直接在canvas上操作。5.3 打印样式覆盖让CtrlP打出来是空白或者水印页浏览器打印本质上是重新排版整个页面所以要在CSS层专门针对打印媒体做处理。media print { /* 默认把PDF区域藏起来打印结果是空白 */ .pdf-wrap { display: none !important; } /* 或者只打印一条警告和一个追踪水印 */ .print-warning { display: block !important; font-size: 18px; color: #999; } }如果策略是打印时替换成警告页就在页面里预置一个默认隐藏的.print-warning元素打印媒体里把它显示出来把真正的PDF内容隐藏。这样用户打印出来看到的是本文件禁止打印操作已记录之类的提示实际内容一个字都没打印出来。配合window.onbeforeprint和onafterprint事件还能在打印动作发生时上报日志或者临时改DOMwindow.onbeforeprint function () { // 打印前把内容替换掉 document.getElementById(pdfCanvas).style.visibility hidden; }; window.onafterprint function () { // 打印后恢复 document.getElementById(pdfCanvas).style.visibility visible; };这几个事件在IE里的支持度还算可以但可靠的做法还是CSS媒体查询因为它不依赖JS执行。用户如果禁用了JSbeforeprint就不触发了但CSS媒体查询照样生效。6. 水印与溯源把绝对禁止变成可追责6.1 动态水印的两种实现方式我越来越觉得与其执着于绝对禁止不如把重心放在溯源上。因为只要内容能被人看到就一定能被以某种方式带出去——拍照、截图、抄写这些技术手段都拦不住。所以水印不是可有可无的它是整套方案的最后一环。水印分两种实现思路。一种是渲染进canvas的水印在pdf.js渲染完页面后用canvas的绘图API在顶层再画一层文字。这种水印跟随页面内容用户缩放、翻页都在缺点是如果用户另存canvas水印也一并被带走了反而成了反面证据。另一种是DOM层的水印用绝对定位的div或者CSS的background-image平铺。这种不进入PDF内容流只是视觉上的覆盖截图时会带上。我更推荐这一种因为它实现简单改动灵活而且和遮罩层可以合并成一个元素。function buildWatermark(text) { var wrap document.getElementById(shield); // 用重复的文字平铺 wrap.style.backgroundImage none; var items []; for (var i 0; i 20; i) { items.push(span styledisplay:inline-block;width:200px; height:120px;transform:rotate(-25deg);color:rgba(0,0,0,0.08); font-size:14px;white-space:nowrap; text /span); } wrap.innerHTML items.join(); }水印内容我一般拼上用户名 部门 时间戳 文件编号。时间戳精确到分钟就行太精确了排版难看。透明度控制在0.06到0.12之间既要能看清又不影响阅读。6.2 水印、token、加密怎么配合这三者不是各干各的而是要串成一条链。用户请求预览时服务端根据他的身份生成一个专属token这个token决定了他能拿到的PDF是哪个加密版本、水印内容是什么。PDF加密保证文件本身带着权限限制token保证链接不可盗用水印保证泄露后能追责。一旦发现某个文件被外传你可以通过水印上的用户标识快速定位到泄露源同时吊销该用户的token和访问权限。这套机制比单纯禁止下来更有实际威慑力也更符合真实的管理需求。实操心得水印内容不要从纯前端生成一定要由服务端下发。前端生成的水印可以被用户改JS抹掉服务端下发的内容和用户身份绑定抹不掉身份信息。7. 常见问题与排查技巧实录7.1 高频问题速查表现象可能原因解决方向IE里PDF区一片空白pdf.js版本不兼容IE换2.x的ES5构建版本开启disableStream点开链接直接下载Content-Disposition是attachment改成inline并检查接口返回类型打印按钮还是能点权限位没生效或阅读器不遵守前端加打印样式覆盖和beforeprint拦截右键菜单还是弹出事件绑定时机太晚或浏览器插件干预尽早绑定用capture阶段监听水印被抹掉水印由前端生成改为服务端下发水印文本token被复用token没有绑定会话绑定用户会话ID或IP并设短有效期翻页特别卡pdf.js在IE下渲染性能差预渲染相邻页控制同时渲染的页数复制地址能直接打开真实路径暴露走接口token隐藏物理路径这张表基本覆盖了我这几年遇到的高频问题。看表的时候注意一点大部分问题的根因都在分层没做全而不是某一层没写对。比如打印按钮还是能点很可能是权限位设了但前端没补CSS右键还能弹往往是绑定方式不对或者有插件在干预。7.2 几个只有踩过才知道的坑第一个坑IE的缓存比想象中顽固。有次改完接口返回但用户看到的一直是旧文件排查半天发现是IE把PDF缓存到了临时目录。后来在响应头里加了no-store同时给每次请求的URL带上一个时间戳参数才彻底解决。第二个坑pdf.js的worker在IE里加载失败。pdf.js默认会起一个Web Worker来处理解析但IE对Worker的路径和跨域限制比较怪。稳妥的做法是指定workerSrc到同域路径或者在某些版本里干脆禁用worker用主线程解析。禁用worker会让界面卡一下但至少能出内容。第三个坑权限设置成用户口令非空之后某些场景打不开了。如果PDF是给pdf.js加载的它在加载受密码保护的文件时需要处理密码回调。如果用户口令非空而前端没处理加载就会挂住。所以对于需要前端直接加载的文件用户口令一般设空只靠所有者口令和权限位来限制。第四个坑水印数据从canvas上抹掉后原PDF可以还原。如果水印是画在canvas上的有经验的用户可以通过读取canvas的像素数据把水印层去掉再另存。所以水印一定要放在DOM层和canvas分离并且遮罩和canvas之间不要有可直接导出的关系。第五个坑忘了考虑移动端和现代浏览器的混合访问。内网系统往往还有手机端、或者部分用户用Chrome。你为IE写的这套事件拦截和ES5代码在Chrome里也能跑但pdf.js的版本差异和canvas尺寸计算要重新适配。我的做法是检测浏览器类型IE走老方案现代浏览器换新构建版本共用同一套接口和水印逻辑。8. 这套方案落地后的实际效果和我的一点经验说实话没有任何一套方案能真正实现绝对禁止。我做过压力测试一个懂技术的用户只要他有足够动机和时间总能找到办法把内容弄出去。这套方案能做到的是让普通用户的顺手操作全部失效让有意为之的泄露留下可追踪的痕迹让管理上有据可查。从实际效果看四层叠起来之后日常场景里99%的用户根本走不到第二步。右键弹不出来、CtrlS没反应、打印出来是警告页、另存得到的文件打开是加密的、截图带水印这一连串的不顺畅足以劝退绝大多数顺手保存的行为。如果让我给还在做类似需求的同学一句实在话先把服务端token和水印做扎实这两样投入产出比最高PDF权限加密其次前端事件拦截虽然看着最直观但它是最容易被绕过的别把宝全压在这一层。另外别指望一次做完就一劳永逸阅读器更新、浏览器补丁、pdf.js版本迭代都会带来兼容问题留一套日志和监控出问题时能第一时间定位到是哪一层失效了比事后到处补代码强得多。最后再补一个小技巧判断某个PDF的权限是否生效不用每次都手动测。写个脚本用PDFBox读一遍权限位比对预期值放进构建流程里做自动检查。这个脚本帮我省了几十次手动排查的时间。
返回列表