首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Go + Gin + 腾讯云 COS:实现 PDF 受控预览、访问次数限制和远程撤回

Go + Gin + 腾讯云 COS:实现 PDF 受控预览、访问次数限制和远程撤回

原创
作者头像
唯览空间
发布于 2026-10-30 21:06:05
发布于 2026-10-30 21:06:05
1420
举报

前言

在开发在线文件分享功能时,最容易实现的是上传和生成链接,真正复杂的是控制文件后续的访问行为。

例如,一份 PDF 发给客户后,希望它只能访问 3 次,超过指定日期自动失效,并且分享者能够随时撤回访问权限。

如果直接把对象存储的文件 URL 交给浏览器,即使隐藏下载按钮,也无法解决这些问题。

本文以 Go、Gin、PostgreSQL 和腾讯云对象存储 COS 为例,设计一套受控 PDF 预览方案,重点分析以下工程问题:

  • 文件存储在私有 Bucket 中,如何向访客交付内容?
  • PDF.js 的 Range 请求如何进行持续鉴权?
  • 访问次数如何避免并发超额与重复计数?
  • 分享撤回后,为什么旧访问凭证可能仍然有效?
  • 文件分段加密能解决哪些安全问题,又有哪些局限?

以下是可实施的简化方案,不代表某个项目已经采用完全相同的生产部署配置。

一、为什么隐藏下载按钮并不安全?

常见 PDF 在线预览方式:

代码语言:javascript
复制
const task = pdfjsLib.getDocument({
  url: pdfUrl
});

const pdf = await task.promise;
console.log(pdf.numPages);

这种方案能正常渲染 PDF,但如果 pdfUrl 是永久有效的文件地址,任何获取该地址的人都可能绕过应用界面直接请求文件。

浏览器需要接收内容才能完成渲染,因此不能依靠隐藏下载按钮提供可靠的内容保护。

受控预览系统真正需要解决的是:访问者每一次获取受保护内容时,服务端是否仍然允许这次操作。

二、整体架构

以腾讯云 COS 作为对象存储,后端使用 Go + Gin,关系型数据库保存分享规则及会话状态。

代码语言:javascript
复制
           用户上传 PDF
                 |
                 v
          文件安全处理任务
                 |
                 v
          生成受控预览资源
                 |
                 v
        腾讯云 COS 私有 Bucket
                 |
                 |
访客打开链接 -> Gin API
                 |
            验证访客身份
                 |
            校验分享策略
                 |
            创建访问会话
                 |
          请求预览数据 Range
                 |
          再次校验访问权限
                 |
          读取 COS 私有对象
                 |
          返回授权的预览数据
                 |
          浏览器查看器渲染

这里将分享链接、访问会话、预览文件和原始 PDF 分离。

分享链接只负责定位分享业务,不直接对应 COS 对象地址。

后端需要根据当前身份、文件范围、分享状态及有效期决定是否提供内容。

三、腾讯云 COS 如何进行私有文件读取?

首先,将 COS Bucket 设置为私有读写。

生产环境建议通过腾讯云 CAM 为服务端配置最小权限访问凭证,并优先使用临时凭证或实例角色,避免将长期 SecretKey 写入代码。

Go SDK 安装:

代码语言:javascript
复制
go get github.com/tencentyun/cos-go-sdk-v5

下面使用 COS Go SDK 构建一个私有对象读取客户端。

代码语言:javascript
复制
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
}

上述代码展示了两个关键操作:

  1. 服务端使用受保护的 COS 凭证读取私有对象。
  2. 使用 HTTP Range 仅读取指定范围的数据。

其中 8MB 是示例请求上限,可根据实际内存、带宽与预览分片策略调整。

这段代码只是存储访问层。真正对外提供接口时,调用它之前必须完成访客鉴权,且 objectKey 必须由服务端从授权资源映射中取得,不能直接信任客户端提交的文件路径。

示例使用静态传入凭证以便理解 SDK;生产环境应替换为 CAM 角色或临时凭证机制。

四、为什么要对每一次 Range 请求鉴权?

PDF.js 为了加载较大的 PDF,可能按需请求文件的不同字节区间。

例如:

代码语言:javascript
复制
Range: bytes=0-1048575

对应前 1MiB 数据。

如果只在打开分享页面时验证一次身份,后续 Range 接口没有验证分享状态,就会产生权限绕过问题。

正确的调用链应当是:

代码语言:javascript
复制
Range 请求
    |
验证访客身份
    |
验证访问会话归属
    |
检查分享是否撤回或过期
    |
检查文件是否处于分享范围
    |
验证 Range 边界和请求大小
    |
读取 COS 对象
    |
返回内容

Gin Handler 可以只负责参数解析和响应输出,将权限检查放到 Application Service 中统一处理。

需要避免一个常见错误:客户端直接传 objectKey,服务器收到后读取对应 COS 对象。

这会引入越权访问风险。正确方式是客户端提交资源标识,服务端从数据库中解析当前已授权的文件版本和对象键。

五、访问次数如何做到并发安全?

假设设置最大访问次数为 1。

如果两名访客同时请求,下面的普通应用层判断存在竞争风险:

代码语言:javascript
复制
查询已使用次数
     |
used < max ?
     |
计数加一

两个请求可能同时读取到 0,最终都被允许访问。

应将判断和更新合并为数据库原子操作。

以 PostgreSQL 为例:

代码语言:javascript
复制
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 请求,如果每次请求都执行上述更新,可能发生重复扣次。

因此还需要访问会话表:

代码语言:javascript
复制
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()
);

完整的计数过程应放在事务中:

  1. 锁定当前访问会话记录。
  2. 检查会话是否已经计数。
  3. 如果尚未计数,执行分享次数的条件更新。
  4. 更新会话 counted = TRUE。
  5. 提交事务。

如果已经计数,本次请求不再次消耗次数,但仍必须独立验证当前分享是否有效。

这样可以同时解决并发超额和重复计数两个问题。

另外,需要明确何时计数。可以选择首次成功准备内容时计数,而不是用户仅仅打开分享页面就计数。

具体口径要与产品规则一致。

六、PDF 分段加密应该怎么设计?

对于需要更强访问控制的文件,可以考虑将预览内容分段加密后存储。

一种实现思路:

代码语言:javascript
复制
原始 PDF
   |
安全处理
   |
按固定大小分块
   |
AES-GCM 加密
   |
生成分块索引
   |
上传加密对象至 COS
   |
受控接口读取加密分块
   |
授权查看器解密并渲染

可以为每个加密块设置独立的 Nonce,并将文件版本与块序号纳入附加认证数据(AAD)。

示意公式:

代码语言:javascript
复制
ciphertext[i] =
    AES-GCM.Encrypt(
        key,
        nonce[i],
        plaintext[i],
        aad[i]
    )

需要特别注意:

  • 同一密钥下不得重复使用 GCM Nonce。
  • 加密块索引必须进行完整性校验。
  • 服务端必须限制分块请求范围。
  • 加密密钥应由受控的密钥管理机制保护。
  • 版本切换或撤回不能只依赖前端清除缓存。

这一方案可以提高存储安全性和受控交付能力,但如果浏览器最终需要解密并显示内容,就无法保证客户端完全无法提取明文。

因此,分段加密不能被等同于绝对防下载或 DRM。

七、远程撤回如何实现?

最基础的撤回实现是修改分享状态:

代码语言:javascript
复制
UPDATE share_links
SET status = 'revoked'
WHERE id = $1;

后续所有需要重新鉴权的预览请求都应被拒绝。

但这里存在一个重要问题:

如果此前向客户端签发了腾讯云 COS 预签名 URL,那么这条 URL 通常可以在签名有效期内独立访问 COS,不需要重新经过业务服务器鉴权。

所以在权限敏感场景中,单纯使用短时签名 URL 仍然存在撤回延迟窗口。

两个交付方案的区别如下:

方案

优点

局限

COS 短时预签名 URL

减少业务服务器流量压力

已签发 URL 在有效期内可能继续使用

Gin 服务端代理 COS Range

可以对后续请求持续鉴权

业务服务器承担更多流量和连接开销

如果对撤回时效要求较高,可以优先考虑服务端代理,或使用具备独立授权能力的专门内容分发方案。

还要明确:已经传送到客户端的数据,不会因为数据库中的分享状态变化而自动消失。

八、视频与 PDF 为什么需要不同的处理方式?

PDF 主要关注页面预览、Range 读取、内容鉴权和访问次数。

视频则还涉及转码、播放协议、CDN 分发、播放授权和视频流量控制。

如果视频使用腾讯云 VOD(云点播),可以利用其播放凭证、Key 防盗链以及不同级别的视频加密能力。

但腾讯云 VOD 的 Key 防盗链并不等于业务系统的用户授权机制。

业务后端仍然需要判断:

  • 当前用户是否具有观看权限。
  • 当前课程或分享是否有效。
  • 播放会话是否超过访问限制。
  • 是否允许刷新新的播放凭证。

PDF 的对象读取控制和 VOD 的播放控制应该在业务权限层保持一致,在实际内容交付层分别实现。

九、如何验证这套方案?

安全功能不能只通过正常打开文件来验证。

至少应当进行以下集成测试:

测试项目

预期

不带凭证访问私有 COS 对象

拒绝

未授权用户读取 PDF Range

拒绝

分享过期后访问内容

拒绝

分享撤回后发起新的 Range 请求

拒绝

多个会话竞争最后一次访问额度

不超额

一个会话重复请求 Range

不重复计数

客户端篡改资源 ID

不能读取其他文件

COS 签名链接已签发后撤回

明确验证其有效期窗口

网络断开、Range 请求失败

不产生错误的计数状态

对于并发计数测试,应使用真实数据库进行验证,而不能仅依赖内存 Mock。

对于内容交付,还应检查缓存头部、代理层缓存规则、跨域配置、日志中的敏感参数和对象访问权限。

总结

开发受控文件分享系统,真正复杂的部分不是生成链接,而是整个访问生命周期的权限一致性。

通过 Go + Gin 管理分享策略、使用数据库事务保证访问次数一致性、以腾讯云 COS 私有对象作为文件存储,以及在每次受保护内容请求时重新鉴权,可以建立一套较完整的访问控制模型。

若进一步引入分段加密、动态水印与访问审计,还可以改善敏感文件的受控交付与追溯能力。

但必须明确系统边界:浏览器已经获得的内容无法依靠简单撤回操作被彻底收回,也不能承诺绝对阻止截图或外传。

这也是受控文件分享类系统,包括唯览空间,在技术实现中需要持续解决的问题。


参考文档

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 前言
    • 一、为什么隐藏下载按钮并不安全?
    • 二、整体架构
    • 三、腾讯云 COS 如何进行私有文件读取?
    • 四、为什么要对每一次 Range 请求鉴权?
    • 五、访问次数如何做到并发安全?
    • 六、PDF 分段加密应该怎么设计?
    • 七、远程撤回如何实现?
    • 八、视频与 PDF 为什么需要不同的处理方式?
    • 九、如何验证这套方案?
    • 总结
    • 参考文档
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档