在开发在线文件分享功能时,最容易实现的是上传和生成链接,真正复杂的是控制文件后续的访问行为。
例如,一份 PDF 发给客户后,希望它只能访问 3 次,超过指定日期自动失效,并且分享者能够随时撤回访问权限。
如果直接把对象存储的文件 URL 交给浏览器,即使隐藏下载按钮,也无法解决这些问题。
本文以 Go、Gin、PostgreSQL 和腾讯云对象存储 COS 为例,设计一套受控 PDF 预览方案,重点分析以下工程问题:
以下是可实施的简化方案,不代表某个项目已经采用完全相同的生产部署配置。
常见 PDF 在线预览方式:
const task = pdfjsLib.getDocument({
url: pdfUrl
});
const pdf = await task.promise;
console.log(pdf.numPages);这种方案能正常渲染 PDF,但如果 pdfUrl 是永久有效的文件地址,任何获取该地址的人都可能绕过应用界面直接请求文件。
浏览器需要接收内容才能完成渲染,因此不能依靠隐藏下载按钮提供可靠的内容保护。
受控预览系统真正需要解决的是:访问者每一次获取受保护内容时,服务端是否仍然允许这次操作。
以腾讯云 COS 作为对象存储,后端使用 Go + Gin,关系型数据库保存分享规则及会话状态。
用户上传 PDF
|
v
文件安全处理任务
|
v
生成受控预览资源
|
v
腾讯云 COS 私有 Bucket
|
|
访客打开链接 -> Gin API
|
验证访客身份
|
校验分享策略
|
创建访问会话
|
请求预览数据 Range
|
再次校验访问权限
|
读取 COS 私有对象
|
返回授权的预览数据
|
浏览器查看器渲染这里将分享链接、访问会话、预览文件和原始 PDF 分离。
分享链接只负责定位分享业务,不直接对应 COS 对象地址。
后端需要根据当前身份、文件范围、分享状态及有效期决定是否提供内容。
首先,将 COS Bucket 设置为私有读写。
生产环境建议通过腾讯云 CAM 为服务端配置最小权限访问凭证,并优先使用临时凭证或实例角色,避免将长期 SecretKey 写入代码。
Go SDK 安装:
go get github.com/tencentyun/cos-go-sdk-v5下面使用 COS Go SDK 构建一个私有对象读取客户端。
package storage
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"github.com/tencentyun/cos-go-sdk-v5"
)
func NewCOSClient(
bucketURL, secretID, secretKey string,
) (*cos.Client, error) {
u, err := url.Parse(bucketURL)
if err != nil || u.Scheme != "https" || u.Host == "" {
return nil, errors.New("invalid COS bucket URL")
}
return cos.NewClient(
&cos.BaseURL{BucketURL: u},
&http.Client{
Transport: &cos.AuthorizationTransport{
SecretID: secretID,
SecretKey: secretKey,
},
},
), nil
}
func ReadCOSRange(
ctx context.Context,
client *cos.Client,
objectKey string,
from, to int64,
) (io.ReadCloser, error) {
if from < 0 || to < from ||
to-from+1 > 8*1024*1024 {
return nil, errors.New("invalid range")
}
options := &cos.ObjectGetOptions{
Range: fmt.Sprintf("bytes=%d-%d", from, to),
}
resp, err := client.Object.Get(
ctx, objectKey, options,
)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusPartialContent {
resp.Body.Close()
return nil, errors.New("unexpected COS response")
}
return resp.Body, nil
}上述代码展示了两个关键操作:
其中 8MB 是示例请求上限,可根据实际内存、带宽与预览分片策略调整。
这段代码只是存储访问层。真正对外提供接口时,调用它之前必须完成访客鉴权,且 objectKey 必须由服务端从授权资源映射中取得,不能直接信任客户端提交的文件路径。
示例使用静态传入凭证以便理解 SDK;生产环境应替换为 CAM 角色或临时凭证机制。
PDF.js 为了加载较大的 PDF,可能按需请求文件的不同字节区间。
例如:
Range: bytes=0-1048575对应前 1MiB 数据。
如果只在打开分享页面时验证一次身份,后续 Range 接口没有验证分享状态,就会产生权限绕过问题。
正确的调用链应当是:
Range 请求
|
验证访客身份
|
验证访问会话归属
|
检查分享是否撤回或过期
|
检查文件是否处于分享范围
|
验证 Range 边界和请求大小
|
读取 COS 对象
|
返回内容Gin Handler 可以只负责参数解析和响应输出,将权限检查放到 Application Service 中统一处理。
需要避免一个常见错误:客户端直接传 objectKey,服务器收到后读取对应 COS 对象。
这会引入越权访问风险。正确方式是客户端提交资源标识,服务端从数据库中解析当前已授权的文件版本和对象键。
假设设置最大访问次数为 1。
如果两名访客同时请求,下面的普通应用层判断存在竞争风险:
查询已使用次数
|
used < max ?
|
计数加一两个请求可能同时读取到 0,最终都被允许访问。
应将判断和更新合并为数据库原子操作。
以 PostgreSQL 为例:
UPDATE share_links
SET used_views = used_views + 1
WHERE id = $1
AND status = 'active'
AND (
expires_at IS NULL
OR expires_at > NOW()
)
AND (
max_views = 0
OR used_views < max_views
)
RETURNING used_views;如果没有更新成功的行,则应拒绝新的计数型访问。
不过,单条 SQL 仍然不够。
因为一次 PDF 查看会发送多个 Range 请求,如果每次请求都执行上述更新,可能发生重复扣次。
因此还需要访问会话表:
CREATE TABLE access_sessions (
id UUID PRIMARY KEY,
share_id UUID NOT NULL,
visitor_id TEXT NOT NULL,
counted BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);完整的计数过程应放在事务中:
counted = TRUE。如果已经计数,本次请求不再次消耗次数,但仍必须独立验证当前分享是否有效。
这样可以同时解决并发超额和重复计数两个问题。
另外,需要明确何时计数。可以选择首次成功准备内容时计数,而不是用户仅仅打开分享页面就计数。
具体口径要与产品规则一致。
对于需要更强访问控制的文件,可以考虑将预览内容分段加密后存储。
一种实现思路:
原始 PDF
|
安全处理
|
按固定大小分块
|
AES-GCM 加密
|
生成分块索引
|
上传加密对象至 COS
|
受控接口读取加密分块
|
授权查看器解密并渲染可以为每个加密块设置独立的 Nonce,并将文件版本与块序号纳入附加认证数据(AAD)。
示意公式:
ciphertext[i] =
AES-GCM.Encrypt(
key,
nonce[i],
plaintext[i],
aad[i]
)需要特别注意:
这一方案可以提高存储安全性和受控交付能力,但如果浏览器最终需要解密并显示内容,就无法保证客户端完全无法提取明文。
因此,分段加密不能被等同于绝对防下载或 DRM。
最基础的撤回实现是修改分享状态:
UPDATE share_links
SET status = 'revoked'
WHERE id = $1;后续所有需要重新鉴权的预览请求都应被拒绝。
但这里存在一个重要问题:
如果此前向客户端签发了腾讯云 COS 预签名 URL,那么这条 URL 通常可以在签名有效期内独立访问 COS,不需要重新经过业务服务器鉴权。
所以在权限敏感场景中,单纯使用短时签名 URL 仍然存在撤回延迟窗口。
两个交付方案的区别如下:
方案 | 优点 | 局限 |
|---|---|---|
COS 短时预签名 URL | 减少业务服务器流量压力 | 已签发 URL 在有效期内可能继续使用 |
Gin 服务端代理 COS Range | 可以对后续请求持续鉴权 | 业务服务器承担更多流量和连接开销 |
如果对撤回时效要求较高,可以优先考虑服务端代理,或使用具备独立授权能力的专门内容分发方案。
还要明确:已经传送到客户端的数据,不会因为数据库中的分享状态变化而自动消失。
PDF 主要关注页面预览、Range 读取、内容鉴权和访问次数。
视频则还涉及转码、播放协议、CDN 分发、播放授权和视频流量控制。
如果视频使用腾讯云 VOD(云点播),可以利用其播放凭证、Key 防盗链以及不同级别的视频加密能力。
但腾讯云 VOD 的 Key 防盗链并不等于业务系统的用户授权机制。
业务后端仍然需要判断:
PDF 的对象读取控制和 VOD 的播放控制应该在业务权限层保持一致,在实际内容交付层分别实现。
安全功能不能只通过正常打开文件来验证。
至少应当进行以下集成测试:
测试项目 | 预期 |
|---|---|
不带凭证访问私有 COS 对象 | 拒绝 |
未授权用户读取 PDF Range | 拒绝 |
分享过期后访问内容 | 拒绝 |
分享撤回后发起新的 Range 请求 | 拒绝 |
多个会话竞争最后一次访问额度 | 不超额 |
一个会话重复请求 Range | 不重复计数 |
客户端篡改资源 ID | 不能读取其他文件 |
COS 签名链接已签发后撤回 | 明确验证其有效期窗口 |
网络断开、Range 请求失败 | 不产生错误的计数状态 |
对于并发计数测试,应使用真实数据库进行验证,而不能仅依赖内存 Mock。
对于内容交付,还应检查缓存头部、代理层缓存规则、跨域配置、日志中的敏感参数和对象访问权限。
开发受控文件分享系统,真正复杂的部分不是生成链接,而是整个访问生命周期的权限一致性。
通过 Go + Gin 管理分享策略、使用数据库事务保证访问次数一致性、以腾讯云 COS 私有对象作为文件存储,以及在每次受保护内容请求时重新鉴权,可以建立一套较完整的访问控制模型。
若进一步引入分段加密、动态水印与访问审计,还可以改善敏感文件的受控交付与追溯能力。
但必须明确系统边界:浏览器已经获得的内容无法依靠简单撤回操作被彻底收回,也不能承诺绝对阻止截图或外传。
这也是受控文件分享类系统,包括唯览空间,在技术实现中需要持续解决的问题。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。