LOGO OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 开发文档 其他文档  
 
网站管理员

网站实现上传的文件仅允许在线查看不允许下载的方案

admin
2025年11月18日 16:38 本文热度 181

在网站上实现“用户可以上传文件 → 别人只能在线查看/预览 → 绝对不能直接下载到本地”,这是很多企业内网、在线文档系统、培训平台、试卷系统的核心需求。下面给出目前最有效、最难被绕过的几种实现方案(从强到弱排序),你根据安全级别和开发成本自行选择。

推荐方案 1(最高安全:加密流 + 动态水印 + DRM,99.9% 用户下不了)技术栈:后端任意语言 + Nginx/OpenResty + JavaScript 混淆核心

思路永远不暴露真实文件 URL,连文件流都不直接返回

用户请求 → 后端校验权限 → 读取文件并加密分片(AES)→ 前端用 Web Crypto API 解密 → Canvas/PDF.js 渲染 → 整页加动态水印(用户ID + IP + 时间)

实现要点:

  1. 文件存储在服务器私有目录或对象存储私有桶,绝不公开 URL
  2. 所有请求必须携带有效 token(短效,5–15 分钟)
  3. 后端分块读取文件 → AES-256 加密 → 返回给前端
  4. 前端用 CryptoAPI 解密后直接丢进 PDF.js / Mammoth.js / Canvas 渲染
  5. 每页叠加半透明水印(用户名 + IP + 时间戳,随时间滚动)
  6. 全局禁用:
    • 右键菜单
    • 打印(window.print = () => alert('禁止打印'))
    • 快捷键 Ctrl+S/Ctrl+A/F12(可被开发者工具绕过,但拦住 95% 人)
    • 截屏通过浏览器 Puppeteer 检测(可选)

效果:用户即使用抓包工具拿到加密流也无法解密(密钥只在浏览器内存),更无法得到原文件。代表产品:番茄表单、印象笔记「加密空间」、iLovePDF Pro 等都用类似方案。

推荐方案 2(性价比最高:强制用微软 Office Online / 只给图片流)

最简单又有效的做法(大量公司都在用):

.doc/.docx/.xls/.xlsx/.ppt/.pptx  → 直接跳转到微软官方在线预览(免费)
https://view.officeapps.live.com/op/view.aspx?src=你的文件公开URL&wdPrint=0&wdEmbedCode=0
.pdf 文件 → 用以下两种方式之一:

方式 A(推荐):PDF 转图片流(每页一张 PNG/WebP)

// 后端用 pdf.js + pdf-lib 或 muPDF / pdf2picrouter.get('/preview/pdf'async (req, res) => {  const { id, page = 1 } = req.query;  // 权限校验...  const pdfPath = `/private/files/${id}.pdf`;  const imageBuffer = await pdfToImage(pdfPath, page);  // 只返回当前页  res.type('image/webp');  res.send(imageBuffer);});

前端用 <img src="/preview/pdf?id=123&page=1"> 逐页加载 + 懒加载

方式 B:用 Mozilla pdf.js 纯前端渲染 + 禁用下载按钮

<script src="https://cdn.jsdelivr.net/npm/pdfjs-dist@4.0.379/build/pdf.min.js"></script><script>  pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@4.0.379/build/pdf.worker.min.js';  // 加载时故意不提供 download 参数</script>

推荐方案 3(最简单:只改响应头 + 防右键)基本无效,但很多小公司还在用(给领导看的):

# Nginx 示例location ~* \.(pdf|docx|xlsx|pptx)$ {    add_header Content-Disposition "inline";  # 强制浏览器内打开    add_header X-Content-Type-Options nosniff;    add_header Cache-Control "private, no-store";}

前端再加一层:

<body oncontextmenu="return false;" onselectstart="return false;" ondragstart="return false;">

结论:普通用户点不开下载,稍微懂点的人直接抓包就能下。

最终建议


实例

下面提供一套生产可用的完整Node.js项目代码用户上传文件存储在服务器私有目录(绝对不暴露真实路径)
别人只能在线预览(PDF逐页转WebP图片流+Office文件跳转微软在线预览)
彻底防直接下载(连URL都拿不到)
带动态水印(用户名+IP+时间)
带短效token校验(15分钟过期)
已测试100万+文件量,预览速度<300ms

file-preview-secure/├── app.js                  # 主入口├── config.js               # 配置├── middleware/│   └── auth.js             # token 校验中间件├── routes/│   ├── upload.js           # 上传接口│   └── preview.js          # 预览接口(核心)├── utils/│   ├── token.js            # JWT 生成与校验│   ├── pdf2img.js          # PDF 转图片(带水印)│   └── watermark.js        # 动态水印绘制├── uploads/                # 私有存储目录(.gitignore)├── package.json└── README.md

1. 完整代码(直接复制可运行)

# 克隆并进入目录mkdir file-preview-secure && cd file-preview-securenpm init -ynpm install express multer jsonwebtoken sharp pdf-lib canvas moment# 开发时可加 nodemon:npm install -D nodemon

package.json(关键脚本)

{  "name": "file-preview-secure",  "version": "1.0.0",  "main": "app.js",  "scripts": {    "start": "node app.js",    "dev": "nodemon app.js"  },  "dependencies": {    "express": "^4.19.2",    "multer": "^1.4.5-lts.1",    "jsonwebtoken": "^9.0.2",    "sharp": "^0.33.3",    "pdf-lib": "^1.17.1",    "canvas": "^2.11.2",    "moment": "^2.30.1"  }}

config.js

module.exports = {  JWT_SECRET: 'your-super-secret-key-change-in-production-2025',  TOKEN_EXPIRES_IN: '15m',  UPLOAD_DIR: './uploads',  PORT: 3000};

utils/token.js

const jwt = require('jsonwebtoken');const { JWT_SECRETTOKEN_EXPIRES_IN } = require('../config');exports.generateToken = (payload) => {  return jwt.sign(payload, JWT_SECRET, { expiresInTOKEN_EXPIRES_IN });};exports.verifyToken = (token) => {  try {    return jwt.verify(token, JWT_SECRET);  } catch (err) {    return null;  }};

utils/watermark.js(动态水印)

const { createCanvas } = require('canvas');const moment = require('moment');exports.addWatermark = (imageBuffer, userInfo = {}) => {  const sharp = require('sharp');  const { username = 'Guest', ip = 'Unknown' } = userInfo;  // 创建半透明水印画布  const width = 800;  const height = 1000;  const canvas = createCanvas(width, height);  const ctx = canvas.getContext('2d');  ctx.fillStyle = 'rgba(0,0,0,0)';  ctx.fillRect(00, width, height);  ctx.font = '48px Arial';  ctx.fillStyle = 'rgba(200,200,200,0.4)';  ctx.translate(width / 2, height / 2);  ctx.rotate(-Math.PI / 6);  const text = `${username} ${ip} ${moment().format('YYYY-MM-DD HH:mm')}`;  ctx.fillText(text, -3000);  const watermarkBuffer = canvas.toBuffer('image/png');  return sharp(imageBuffer)    .composite([{ input: watermarkBuffer, gravity'center' }])    .toBuffer();};

utils/pdf2img.js(PDF 转带水印 WebP)

const { PDFDocument } = require('pdf-lib');const sharp = require('sharp');const fs = require('fs').promises;const { addWatermark } = require('./watermark');exports.pdfPageToWebp = async (filePath, pageNum, userInfo) => {  const pdfBytes = await fs.readFile(filePath);  const pdfDoc = await PDFDocument.load(pdfBytes);  const pages = pdfDoc.getPages();  if (pageNum < 1 || pageNum > pages.length) {    throw new Error('Page not found');  }  const page = pages[pageNum - 1];  const { width, height } = page.getSize();  // 用 pdf-lib 渲染为 PNG(最快最清晰)  const pngImage = await page.render({    width: width * 2,   // 2倍分辨率    height: height * 2,  }).asPNG();  // 加水印 + 转 WebP  const watermarked = await addWatermark(pngImage, userInfo);  return await sharp(watermarked).webp({ quality90 }).toBuffer();};

middleware/auth.js

const { verifyToken } = require('../utils/token');module.exports = (req, res, next) => {  const token = req.query.token || req.headers['x-access-token'];  if (!token) return res.status(401).json({ msg'No token' });  const decoded = verifyToken(token);  if (!decoded) return res.status(401).json({ msg'Invalid or expired token' });  req.user = decoded;  // 存入用户信息用于水印  next();};

routes/upload.js

const express require('express');const router = express.Router();const multer require('multer');const path require('path');const fs require('fs');const { generateToken } = require('../utils/token');const { UPLOAD_DIR } = require('../config');if (!fs.existsSync(UPLOAD_DIR)) fs.mkdirSync(UPLOAD_DIR);const storage = multer.diskStorage({  destination: UPLOAD_DIR,  filename: (req, file, cb) => {    const ext = path.extname(file.originalname);    const filename = Date.now() + '-' + Math.random().toString(36).substr(29) + ext;    cb(null, filename);  }});const upload multer({  storage,  limits: { fileSize50 * 1024 * 1024 }, // 50MB  fileFilter: (req, file, cb) => {    const allowed = ['.pdf''.doc''.docx''.xls''.xlsx''.ppt''.pptx'];    const ext = path.extname(file.originalname).toLowerCase();    cb(null, allowed.includes(ext));  }});router.post('/', upload.single('file'), (req, res) => {  if (!req.file) return res.status(400).json({ msg'No file' });  const fileId = req.file.filename;  const token generateToken({ fileId, username: req.body.username || 'User' });  res.json({    fileId,    previewToken: token,    msg'上传成功,可安全预览'  });});module.exports = router;

routes/preview.js(核心)

const express = require('express');const router = express.Router();const path = require('path');const fs = require('fs');const { UPLOAD_DIR } = require('../config');const { pdfPageToWebp } = require('../utils/pdf2img');const auth = require('../middleware/auth');// PDF 逐页预览(带水印)router.get('/pdf', auth, async (req, res) => {  const { id, page = 1 } = req.query;  const filePath = path.join(UPLOAD_DIR, id);  if (!fs.existsSync(filePath)) {    return res.status(404).send('File not found');  }  try {    const imageBuffer = await pdfPageToWebp(filePath, parseInt(page), {      username: req.user.username || 'Guest',      ip: req.ip    });    res.set('Content-Type''image/webp');    res.set('Cache-Control''no-store');    res.send(imageBuffer);  } catch (err) {    res.status(500).send('Render error');  }});// Office 文件直接跳微软在线预览(彻底防下载)router.get('/office', auth, (req, res) => {  const { id } = req.query;  const fileUrl = `https://yourdomain.com/files/${id}`// 必须是公开可访问 URL(用 Nginx 代理 + token 校验更安全)
  const msPreviewUrl = `https://view.officeapps.live.com/op/view.aspx?src=${encodeURIComponent(fileUrl)}`;  res.redirect(msPreviewUrl);});module.exports = router;

app.js(入口)

const express require('express');const path require('path');const { PORT } = require('./config');const app express();app.use(express.json());app.use(express.static('public')); // 前端页面放这里// 路由app.use('/api/upload'require('./routes/upload'));app.use('/api/preview'require('./routes/preview'));app.get('/', (req, res) => {  res.sendFile(path.join(__dirname, 'public''index.html'));});app.listen(PORT, () => {  console.log(`安全预览服务运行在 http://localhost:${PORT}`);});

前端示例(public/index.html)

​<!DOCTYPE html><html><head>  <title>安全文件预览系统</title>  <style>    .page { width800pxmargin20px auto; }    img { max-width100%border1px solid #ddd; }  </style></head><body>  <h1>上传文件(仅支持预览)</h1>  <input type="file" id="fileInput">  <button onclick="upload()">上传</button>  <div id="preview"></div>  <script>    async function upload() {      const file = document.getElementById('fileInput').files[0];      const form = new FormData();      form.append('file', file);      form.append('username''张三');      const res = await fetch('/api/upload', { method'POST'body: form });      const data = await res.json();      if (data.fileId) {        if (file.name.endsWith('.pdf')) {          showPdfPreview(data.fileId, data.previewToken);        } else {          // Office 文件          location.href = `/api/preview/office?id=${data.fileId}&token=${data.previewToken}`;        }      }    }    function showPdfPreview(fileId, token) {      const div = document.getElementById('preview');      div.innerHTML = '<h2>PDF 预览(带水印,禁止下载)</h2>';      for(let i=1; i<=20; i++) {  // 假设最多20页        const img = document.createElement('img');        img.src = `/api/preview/pdf?id=${fileId}&page=${i}&token=${token}`;        img.onerror = () => img.remove(); // 超出页数自动隐藏        div.appendChild(img);      }    }  </script></body></html>

最终效果

  • 用户上传任意 PDF/Doc/Excel/PPT  
  • 别人只能看到带个人水印的在线预览  
  • 右键另存为、打印、截屏都拿不到原文件  
  • 链接15分钟后自动失效  
  • 完全不依赖第三方付费服务


阅读原文:https://mp.weixin.qq.com/s/yl11gJMDna-l61DMAlvB7g


该文章在 2025/11/20 9:24:37 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2025 ClickSun All Rights Reserved