下载要付费、导出却不能有水印?客户端鉴权与资源交付
做内容付费的同学迟早会遇到一个拧巴的需求:用户没付钱之前想看个大概(最好带个水印防截图),付了钱下载的文件必须干干净净、绝对不能带水印。我在 GeoJSONcn 上就是这么设计的,在线预览免费看,下载付费,但下载的 GeoJSON/PNG 一张水印都没有。
这矛盾吗?技术上一点也不矛盾,关键是把水印和数据分成两层。这篇聊聊思路和实现。
先厘清:水印是"显示层"的事,不是"数据层"的事
很多人踩坑,是因为把水印烤进了数据文件。比如给导出的 PNG 直接 ctx.fillText('水印') 写死,结果付费用户拿到的图也有水印,这就错了。
正确的理解是:
- 屏幕预览:在 DOM/Canvas 上层盖一层水印(半透明文字/图案),用户肉眼能看到"这图有 GeoJSONcn 水印",但不影响他看内容;
- 导出文件:用原始数据重新画一遍,根本不加水印层,导出的就是纯净文件。
两者数据源是同一份,区别只在于画的时候有没有叠水印层。这就是屏幕有水印、导出无水印能同时成立的根本原因。
鉴权放哪:客户端能信吗?
付费下载的鉴权,理论上该在服务端。但纯前端项目(比如我这种静态托管加边缘 Worker)怎么办?
我的做法是客户端轻鉴权加服务端校验兜底:
1. 支付成功(支付宝沙箱回调或前端确认),把订单记进客户端的 localStorage(标记已购此资源); 2. 下载时前端检查这个标记,有就放行导出; 3. 关键资源(比如高精度的原始数据文件)实际托管在需要鉴权的路径,真正的交付由 Worker 侧按订单校验。
function canDownload(resourceId) {
const paid = JSON.parse(localStorage.getItem('paidOrders') || '[]');
return paid.includes(resourceId) || isPassOwner(); // 通行证
}
提醒一句:纯客户端标记只能挡普通用户顺手点,挡不住刻意抓包的人。真金白银的交付必须服务端校验(签名 URL / 鉴权 token)。客户端鉴权是体验优化,不是安全边界。这点别搞混。
价格档位怎么设计
我在 GeoJSONcn 上是两档:
- 按次:比如 ¥1.99 下载单个资源;
- 通行证:比如 ¥9.9 的 24 小时通行证,有效期内随便下。
逻辑就是判断 paidOrders 里有没有这个资源,或者通行证是否在有效期。简单透明,用户不迷糊。
导出"重画"的代码骨架
function exportPng(geojson, withWatermark) {
const cv = document.createElement('canvas');
const ctx = cv.getContext('2d');
drawGeoJSON(ctx, geojson); // 画数据
if (withWatermark) { // 只有预览才 true
ctx.fillStyle = 'rgba(0,0,0,0.15)';
ctx.fillText('GeoJSONcn.com 下载无水印', 20, 30);
}
return cv.toDataURL('image/png');
}
// 预览:exportPng(data, true)
// 下载:exportPng(data, false) ← 干净文件
就这一个 withWatermark 参数,屏幕和文件彻底隔离。
最后
水印是显示层概念,别烤进数据;导出用原始数据重画,天然无水印;客户端鉴权管体验,真交付必须服务端校验;按次加通行证两档最省心。
这套"预览免费看、下载付费、文件纯净"的组合,在卖数据/设计素材/报告类站点上很通用。下一篇聊个更硬核的:几万静态页怎么让百度乖乖收录,canonical 和 sitemap 那些坑。