PHP 限时下载链接如何判断访问者是否为邮件收件人?
结论
仅凭邮件中的下载链接,无法可靠判断点击者是否为原收件人,也无法证明链接是否被公开分享。任何拿到相同链接的人,都可以在有效期内发起下载请求。
如果只允许指定收件人下载,应将下载记录绑定到用户账号,并在下载时校验登录身份和过期时间。如果只是统计邮件链接的点击次数,可以记录专属链接的访问情况,但这些记录不能证明访问者的身份。
为什么不能靠请求来源判断
HTTP Referer 不能作为下载授权依据。邮件客户端可能不发送它,网页版邮箱也可能隐藏来源或经过跳转;客户端还可以自行构造这个请求头。因此,缺少 Referer 不代表链接被分享,出现邮箱来源也不代表访问者就是收件人。
给每封邮件分配不同的下载标识,只能确定请求使用了哪条链接。链接被转发后,标识仍然相同。IP 地址和 User-Agent 可以辅助排查异常访问,但同一个用户可能切换网络或设备,不同用户也可能共用出口 IP,因此不能据此确认身份。
把下载权限绑定到用户
现有的 id、filename、expires 可以支持限时链接,但没有约束下载者的身份。可以新增 user_id,记录这条下载记录所属的用户。
下载流程可以按以下顺序处理:
- 创建下载记录,保存随机标识、所属用户和过期时间。
- 将包含该标识的下载地址发送给用户。
- 用户访问链接时,要求登录。
- 服务端同时校验链接标识、所属用户和过期时间。
- 校验通过后,由服务端读取并输出文件。
新链接的标识应使用不可预测的随机值,例如:
$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 请求直接消耗下载资格。可以先展示页面,等用户登录并确认后再兑换。
没有账号体系时,可以要求用户输入另行发送到原邮箱的验证码,以增加一层对邮箱访问权的验证。但这仍无法阻止收件人主动分享验证码或下载后的文件。
备注:内容仅供参考。
