PAYATHON 2026

PHP 限时下载链接如何判断访问者是否为邮件收件人?

数据小陈

结论

仅凭邮件中的下载链接,无法可靠判断点击者是否为原收件人,也无法证明链接是否被公开分享。任何拿到相同链接的人,都可以在有效期内发起下载请求。

如果只允许指定收件人下载,应将下载记录绑定到用户账号,并在下载时校验登录身份和过期时间。如果只是统计邮件链接的点击次数,可以记录专属链接的访问情况,但这些记录不能证明访问者的身份。

为什么不能靠请求来源判断

HTTP Referer 不能作为下载授权依据。邮件客户端可能不发送它,网页版邮箱也可能隐藏来源或经过跳转;客户端还可以自行构造这个请求头。因此,缺少 Referer 不代表链接被分享,出现邮箱来源也不代表访问者就是收件人。

给每封邮件分配不同的下载标识,只能确定请求使用了哪条链接。链接被转发后,标识仍然相同。IP 地址和 User-Agent 可以辅助排查异常访问,但同一个用户可能切换网络或设备,不同用户也可能共用出口 IP,因此不能据此确认身份。

把下载权限绑定到用户

现有的 id、filename、expires 可以支持限时链接,但没有约束下载者的身份。可以新增 user_id,记录这条下载记录所属的用户。

下载流程可以按以下顺序处理:

  1. 创建下载记录,保存随机标识、所属用户和过期时间。
  2. 将包含该标识的下载地址发送给用户。
  3. 用户访问链接时,要求登录。
  4. 服务端同时校验链接标识、所属用户和过期时间。
  5. 校验通过后,由服务端读取并输出文件。

新链接的标识应使用不可预测的随机值,例如:

$downloadId = bin2hex(random_bytes(32));

随机标识可以降低链接被猜中的风险,但不能阻止持有人转发链接。

PHP 校验示例

示例假设使用 PDO,已有数据库连接 $pdo,表名为 downloads,并且已新增 user_id 字段。$_SESSION['user_id'] 必须由登录流程设置。示例约定数据库中的 expires 使用 UTC,实际项目应统一写入和比较时的时区。

<?php
session_start();

if (!isset($_SESSION['user_id'])) {
    http_response_code(401);
    exit('请先登录');
}

$id = $_GET['id'] ?? null;
if (!is_string($id) || $id === '') {
    http_response_code(400);
    exit('缺少有效的下载标识');
}

$stmt = $pdo->prepare(
    'SELECT filename
     FROM downloads
     WHERE id = :id
       AND user_id = :user_id
       AND expires > :now'
);

$stmt->execute([
    'id' => $id,
    'user_id' => $_SESSION['user_id'],
    'now' => gmdate('Y-m-d H:i:s'),
]);

$download = $stmt->fetch(PDO::FETCH_ASSOC);
if ($download === false) {
    http_response_code(403);
    exit('链接无效、已过期或当前账号无权下载');
}

// 权限校验通过。
// 根据服务端保存的文件映射定位文件,再输出下载响应。

文件应放在无法通过公开 URL 直接访问的位置,或者由存储服务实施相应的访问控制,否则用户可能绕过 PHP 校验直接下载。输出文件时,还应检查文件路径是否位于允许的目录内,不要把请求参数直接当作文件路径。

一次性链接能否解决分享问题

一次性链接可以限制兑换次数,但不能保证第一次兑换的人就是收件人。如果其他人先拿到链接,仍可能先使用它。

如果需要一次性兑换,应在数据库事务或带条件的原子更新中完成状态检查和占用,避免两个并发请求同时通过。还要定义“已使用”是指开始下载还是成功兑换下载资格;服务端输出文件并不等于用户已经完整收到文件。

邮件安全扫描器也可能自动访问链接,因此不宜让一次普通的 GET 请求直接消耗下载资格。可以先展示页面,等用户登录并确认后再兑换。

没有账号体系时,可以要求用户输入另行发送到原邮箱的验证码,以增加一层对邮箱访问权的验证。但这仍无法阻止收件人主动分享验证码或下载后的文件。

备注:内容仅供参考。