云计算百科
云计算领域专业知识百科平台

临时下载链接怎么设计:有效期、撤销与权限范围

给「言序轻填表单」做导出时,我想解决一个小问题:用户在小程序里收完报名或问卷,想把数据放到电脑整理,能不能直接打开一个链接下载?

现在的做法是:在小程序里生成一个两小时有效的链接,复制到电脑打开,不需要登录网页账号,就能导出当前项目的 Excel、CSV 和附件。

这类链接真正需要设计的是三件事:凭什么允许下载、什么时候失效、最多能访问哪些数据。 下面结合项目的 Go、PostgreSQL 和网页端实现来讲。

在这里插入图片描述

1. 把链接当成一份临时权限

这里使用不透明随机凭证:服务端生成 token,数据库保存它对应的资源与有效期,持有人带着 token 发起下载。

这份凭证允许访问的内容先明确下来:

可以做什么不包含什么
读取所绑定表单的导出说明 进入用户工作台
导出该表单的 Excel、CSV 编辑、删除或发布表单
下载该表单对应答卷的附件包 切换到其他表单或取得账号登录态

需要留意:这里授予的是这个项目的完整导出能力,不是只看标题,也不是脱敏预览。任何拿到有效链接的人都可以使用,所以生成时要把这一点说清楚。

当前链接在有效期内可以重复使用,方便分别下载不同格式。它不是一次性下载码,也没有冻结某一时刻的数据快照;文件是在下载请求中查询、生成的。

2. 随机 token 只返回给用户,数据库保存哈希

项目使用 32 字节安全随机数,再编码成带前缀的字符串。下面把生成和哈希整理在一起,使用 Go 标准库 crypto/rand、crypto/sha256 和 encoding/hex:

func issueExportToken() (string, string, error) {
raw := make([]byte, 32)
if _, err := rand.Read(raw); err != nil {
return "", "", err
}

token := "export_" + hex.EncodeToString(raw)
sum := sha256.Sum256([]byte(token))
return token, hex.EncodeToString(sum[:]), nil
}

32 字节提供 256 位随机量。前缀只用于识别格式,不增加随机性。crypto/rand 提供密码学安全随机数,而可预测的编号或时间戳不能代替它。Go 官方说明

数据库只存哈希。下载时对收到的 token 做同样的哈希,再查记录,不需要从哈希还原原文。

这里处理的是高熵随机凭证,不是人设置的密码,不能把这个示例直接当成用户密码存储方案。

3. 两小时是否有效,每次都由后端判断

临时记录的关键字段如下,沿用了项目中的约束:

CREATE TABLE form_temporary_export_links (
form_id TEXT PRIMARY KEY REFERENCES forms(id) ON DELETE CASCADE,
token_hash TEXT NOT NULL UNIQUE,
owner_sub TEXT NOT NULL,
created_by TEXT NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

form_id 绑定具体项目,owner_sub 记录其授权归属,created_by 保留生成者身份用于审计。这几个字段由生成接口的服务端授权结果确定。

生成期限后,网页可以显示“有效至几点”,但是否允许下载,仍由每次 API 请求检查:

if !time.Now().UTC().Before(link.ExpiresAt) {
s.writeError(w, http.StatusGone, "临时导出链接已过期,请重新生成")
return
}

这也覆盖了“当前时间恰好等于到期时间”的情况。

不能只在打开网页时验证一次。用户可能在页面上停留很久,过期后才点击下载,所以元信息接口、CSV、Excel 和附件包接口都要走同一套凭证校验。

过期拒绝也不依赖定时清理任务。即使数据库中的记录还没删除,期限校验仍然应当生效。

4. 重新生成,就是替换当前有效凭证

我的设计是一张表单只保留一份当前临时导出记录。用户重新生成时,覆盖旧哈希和期限:

INSERT INTO form_temporary_export_links
(form_id, token_hash, owner_sub, created_by, expires_at)
SELECT $1, $2, $3, $4, $5
FROM forms
WHERE id = $1 AND owner_sub = $3
ON CONFLICT (form_id) DO UPDATE SET
token_hash = EXCLUDED.token_hash,
owner_sub = EXCLUDED.owner_sub,
created_by = EXCLUDED.created_by,
expires_at = EXCLUDED.expires_at,
created_at = NOW()
RETURNING form_id, expires_at;

生成入口先校验登录及当前表单的访问/导出授权;SQL 还要求表单和服务端解析出的归属匹配。

这里靠 form_id 主键约束和 ON CONFLICT DO UPDATE 完成原子替换,不在应用层拆成“先查有没有、再决定插入还是修改”。PostgreSQL 对这个操作提供原子的插入或更新结果。官方说明

旧链接下次请求时,它的哈希已经查不到记录,所以失效。

并发生成也要接受这个规则:两次生成都可能成功返回,但只有最终保留在数据库中的那份凭证有效。前端的“生成中”状态能减少同一页面重复点击,不能代替数据库约束,也挡不住另一台设备同时生成。

撤销的边界在哪里

替换提交后,后续重新校验旧凭证的请求会被拒绝。但如果一个导出请求已经读到了旧记录、通过了校验,轮换不会自动取消它,更不能撤回已经下载到电脑的文件。

所以这里的“立即失效”,指新校验时不再认可旧凭证,不代表把已传输的数据收回来。

5. 下载目标从凭证记录取,不能让客户端另选

有效 token 只说明用户持有一份能力,还必须把它限定在正确资源上。

下载处理器先查询凭证记录,拿到绑定的 FormID 和 OwnerSub,再调用既有导出逻辑:

r.SetPathValue("id", link.FormID)
s.exportResponseFileForOwner(
w, r, format,
link.OwnerSub,
link.CreatedBy+":temporary-export-link",
)

这样,客户端即使额外传了另一个 formId,也不能改变凭证授权的目标。导出逻辑还会按这对归属与表单 ID 重新取表单;附件读取继续限定在该项目与答卷范围内。

元信息接口只返回标题、到期时间和可用格式,真正的数据由对应下载接口返回。

还有一个容易漏掉的区别:生成者的账号权限和已签发的临时凭证,是两个授权环节。仅让生成者退出登录,不会自动撤销这份 token。如果业务要求撤掉协作者权限时,其已签发链接也同时失效,就要额外建立撤销联动。

6. token 放在 URL 的 # 后面,仍然需要保护

当前链接的形式是:

https://forms.example.test/temporary-export#token=<opaque-token>

fragment 不会作为首次页面请求的一部分发给服务器。浏览器里的 JavaScript 读取它,再通过请求头主动提交给 API。RFC 3986 的 fragment 定义

const token = new URLSearchParams(location.hash.slice(1)).get('token');

const response = await fetch('/api/temporary-export', {
headers: { 'X-Export-Token': token || '' },
});

这样能减少凭证直接进入页面请求 URL 和相关访问日志的机会,但并没有让凭证从浏览器中消失。完整地址仍可能出现在历史记录、剪贴板或截图中,同页脚本也可以读取它;服务端还要避免记录完整的 X-Export-Token。

当前页面设置了 no-referrer,并标记为 noindex, nofollow。这些措施用于减少额外传播,下载权限仍然由后端判断。

临时元信息和下载响应使用:

Cache-Control: private, no-store

这里需要的是禁止保存响应的 no-store。no-cache 允许缓存保存内容,只要求复用前验证,含义不同。缓存指令说明

7. 回归测试要覆盖完整请求链

只验证“生成后能下载”还不够。我会按下面这些条件检查行为:

条件预期行为
未登录或没有目标项目授权 不能生成链接
缺少凭证、凭证格式错误 拒绝元信息和下载请求
凭证有效 可读取说明、下载允许的格式
元信息请求成功,下载前已过期 下载接口仍然拒绝
时间恰好等于到期时间 拒绝
重新生成链接 旧凭证失效,新凭证有效
携带另一个项目 ID 不能改变凭证绑定的项目
两个请求并发生成 一张表单最终只有一份当前记录

项目已有临时链接的创建授权、轮换、过期和 CSV 下载集成测试。并发写入、附件范围等场景还应该结合隔离数据库和导出链路继续验证,不能用前端按钮的状态代替后端测试。

这个功能用在我开发的 言序轻填表单 中:小程序里收集数据,需要电脑整理时,再生成限时导出入口。

电脑端:言序轻填表单。

赞(0)
未经允许不得转载:网硕互联帮助中心 » 临时下载链接怎么设计:有效期、撤销与权限范围
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!