当前位置: 首页> 最新文章列表> 如何用 hash_hmac_file() 对上传的文件进行安全校验和防护?

如何用 hash_hmac_file() 对上传的文件进行安全校验和防护?

gitbox 2025-08-27

核心思路(高层)

  1. 能验证文件未被篡改,并且只有知道密钥的一方能生成正确的签名。与单纯哈希(如 SHA256)不同,HMAC 使用密钥防止伪造。

  2. 对上传文件的正确流程通常是:

    • 生成/分发一个共享密钥(或使用服务端私钥);

    • 客户端或发送方对文件计算 HMAC(例如 HMAC-SHA256),并随上传一起发送签名(HTTP header 或 表单字段);

    • 服务端接收文件后用相同密钥重新计算 HMAC,并用时序安全比较hash_equals)比对签名,匹配则通过校验。

  3. 永远通过 HTTPS 传输签名和文件。签名在明文的 HTTP 上会被窃听并复用。


为什么使用 hash_hmac_file()

  • hash_hmac_file($algo, $filename, $key, $raw_output = false) 直接在文件上计算 HMAC,内部会处理流式读文件,不需要把整个文件先读入内存,适合中大文件。

  • 当你需要快速实现服务端验证时,hash_hmac_file() 是简洁且高效的方案。但对于更复杂场景(如分片上传)也可以配合 hash_init() / hash_update() / hash_final() 实现逐块计算。


基本示例:客户端签名 + 服务端验证(单文件)

下面演示一个常见情形:客户端使用共享密钥对文件做 HMAC(客户端示例略),服务端用 hash_hmac_file() 验证 $_FILES 上传。

服务端接收端 verify_upload.php(简洁示例):

<span><span><span class="hljs-meta">&lt;?php</span></span><span>
</span><span><span class="hljs-comment">// verify_upload.php</span></span><span>

</span><span><span class="hljs-comment">// 1) 安全的密钥存放(示例中用环境变量)</span></span><span>
</span><span><span class="hljs-variable">$HMAC_KEY</span></span><span> = </span><span><span class="hljs-title function_ invoke__">getenv</span></span><span>(</span><span><span class="hljs-string">'UPLOAD_HMAC_KEY'</span></span><span>); </span><span><span class="hljs-comment">// 请在部署时通过安全方式设置(环境变量 / Vault)</span></span><span>
</span><span><span class="hljs-keyword">if</span></span><span> (!</span><span><span class="hljs-variable">$HMAC_KEY</span></span><span>) {
    </span><span><span class="hljs-title function_ invoke__">http_response_code</span></span><span>(</span><span><span class="hljs-number">500</span></span><span>);
    </span><span><span class="hljs-keyword">echo</span></span><span> </span><span><span class="hljs-string">"Server misconfiguration."</span></span><span>;
    </span><span><span class="hljs-keyword">exit</span></span><span>;
}

</span><span><span class="hljs-comment">// 2) 假设客户端将签名放在 HTTP Header: X-File-Signature</span></span><span>
</span><span><span class="hljs-variable">$clientSig</span></span><span> = </span><span><span class="hljs-keyword">isset</span></span><span>(</span><span><span class="hljs-variable">$_SERVER</span></span><span>[</span><span><span class="hljs-string">'HTTP_X_FILE_SIGNATURE'</span></span><span>]) ? </span><span><span class="hljs-variable">$_SERVER</span></span><span>[</span><span><span class="hljs-string">'HTTP_X_FILE_SIGNATURE'</span></span><span>] : </span><span><span class="hljs-literal">null</span></span><span>;
</span><span><span class="hljs-keyword">if</span></span><span> (!</span><span><span class="hljs-variable">$clientSig</span></span><span>) {
    </span><span><span class="hljs-title function_ invoke__">http_response_code</span></span><span>(</span><span><span class="hljs-number">400</span></span><span>);
    </span><span><span class="hljs-keyword">echo</span></span><span> </span><span><span class="hljs-string">"Missing signature."</span></span><span>;
    </span><span><span class="hljs-keyword">exit</span></span><span>;
}

</span><span><span class="hljs-keyword">if</span></span><span> (!</span><span><span class="hljs-keyword">isset</span></span><span>(</span><span><span class="hljs-variable">$_FILES</span></span><span>[</span><span><span class="hljs-string">'file'</span></span><span>]) || </span><span><span class="hljs-variable">$_FILES</span></span><span>[</span><span><span class="hljs-string">'file'</span></span><span>][</span><span><span class="hljs-string">'error'</span></span><span>] !== UPLOAD_ERR_OK) {
    </span><span><span class="hljs-title function_ invoke__">http_response_code</span></span><span>(</span><span><span class="hljs-number">400</span></span><span>);
    </span><span><span class="hljs-keyword">echo</span></span><span> </span><span><span class="hljs-string">"Upload failed."</span></span><span>;
    </span><span><span class="hljs-keyword">exit</span></span><span>;
}

</span><span><span class="hljs-comment">// 临时上传文件路径</span></span><span>
</span><span><span class="hljs-variable">$tmpPath</span></span><span> = </span><span><span class="hljs-variable">$_FILES</span></span><span>[</span><span><span class="hljs-string">'file'</span></span><span>][</span><span><span class="hljs-string">'tmp_name'</span></span><span>];

</span><span><span class="hljs-comment">// 3) 使用 hash_hmac_file 计算服务器端签名(使用 SHA256)</span></span><span>
</span><span><span class="hljs-variable">$algo</span></span><span> = </span><span><span class="hljs-string">'sha256'</span></span><span>;
</span><span><span class="hljs-variable">$serverSig</span></span><span> = </span><span><span class="hljs-title function_ invoke__">hash_hmac_file</span></span><span>(</span><span><span class="hljs-variable">$algo</span></span><span>, </span><span><span class="hljs-variable">$tmpPath</span></span><span>, </span><span><span class="hljs-variable">$HMAC_KEY</span></span><span>);

</span><span><span class="hljs-comment">// 4) 用 hash_equals 做时序安全比较,避免泄露信息</span></span><span>
</span><span><span class="hljs-keyword">if</span></span><span> (!</span><span><span class="hljs-title function_ invoke__">hash_equals</span></span><span>(</span><span><span class="hljs-variable">$serverSig</span></span><span>, </span><span><span class="hljs-variable">$clientSig</span></span><span>)) {
    </span><span><span class="hljs-title function_ invoke__">http_response_code</span></span><span>(</span><span><span class="hljs-number">403</span></span><span>);
    </span><span><span class="hljs-keyword">echo</span></span><span> </span><span><span class="hljs-string">"Signature mismatch. File may be tampered."</span></span><span>;
    </span><span><span class="hljs-comment">// 可以记录审计日志:来源 IP、文件名、时间等</span></span><span>
    </span><span><span class="hljs-keyword">exit</span></span><span>;
}

</span><span><span class="hljs-comment">// 5) 签名校验通过,安全地将文件移动到最终目录并设置权限</span></span><span>
</span><span><span class="hljs-variable">$dest</span></span><span> = </span><span><span class="hljs-keyword">__DIR__</span></span><span> . </span><span><span class="hljs-string">'/uploads/'</span></span><span> . </span><span><span class="hljs-title function_ invoke__">basename</span></span><span>(</span><span><span class="hljs-variable">$_FILES</span></span><span>[</span><span><span class="hljs-string">'file'</span></span><span>][</span><span><span class="hljs-string">'name'</span></span><span>]);
</span><span><span class="hljs-keyword">if</span></span><span> (!</span><span><span class="hljs-title function_ invoke__">move_uploaded_file</span></span><span>(</span><span><span class="hljs-variable">$tmpPath</span></span><span>, </span><span><span class="hljs-variable">$dest</span></span><span>)) {
    </span><span><span class="hljs-title function_ invoke__">http_response_code</span></span><span>(</span><span><span class="hljs-number">500</span></span><span>);
    </span><span><span class="hljs-keyword">echo</span></span><span> </span><span><span class="hljs-string">"Failed to store file."</span></span><span>;
    </span><span><span class="hljs-keyword">exit</span></span><span>;
}

</span><span><span class="hljs-comment">// 可选:保存签名/元数据到数据库以便后续校验</span></span><span>
</span><span><span class="hljs-comment">// $db-&gt;insert('uploads', ['name'=&gt;..., 'sig'=&gt;$serverSig, ...]);</span></span><span>

</span><span><span class="hljs-title function_ invoke__">http_response_code</span></span><span>(</span><span><span class="hljs-number">200</span></span><span>);
</span><span><span class="hljs-keyword">echo</span></span><span> </span><span><span class="hljs-string">"Upload verified and stored."</span></span><span>;
</span></span>

客户端必须将签名值(例如十六进制字符串)随请求一起发送。示例 HMAC 生成(PHP 客户端或命令行演示):

<span><span><span class="hljs-meta">&lt;?php</span></span><span>
</span><span><span class="hljs-comment">// client_sign.php</span></span><span>
</span><span><span class="hljs-variable">$key</span></span><span> = </span><span><span class="hljs-string">'shared-secret-key'</span></span><span>;
</span><span><span class="hljs-variable">$file</span></span><span> = </span><span><span class="hljs-string">'/path/to/file.bin'</span></span><span>;
</span><span><span class="hljs-variable">$algo</span></span><span> = </span><span><span class="hljs-string">'sha256'</span></span><span>;
</span><span><span class="hljs-variable">$sig</span></span><span> = </span><span><span class="hljs-title function_ invoke__">hash_hmac_file</span></span><span>(</span><span><span class="hljs-variable">$algo</span></span><span>, </span><span><span class="hljs-variable">$file</span></span><span>, </span><span><span class="hljs-variable">$key</span></span><span>);
</span><span><span class="hljs-keyword">echo</span></span><span> </span><span><span class="hljs-variable">$sig</span></span><span>; </span><span><span class="hljs-comment">// 发送到服务器的签名</span></span><span>
</span></span>

在实际 HTTP 上传中,客户端将 X-File-Signature: <sig> header 随 multipart/form-data 上传发送,或作为 form 字段 signature


大文件 / 分片上传 的处理建议

hash_hmac_file() 本身可以处理大文件,但在分片上传(chunked)或流式上传场景中,需要逐块计算 HMAC 或采用 分段 HMAC 再合并 的策略。

方案 A:服务端在接收完合并文件后用 hash_hmac_file() 验证

优点:实现简单;缺点:需要在服务端临时存储/合并完整文件,可能占用 IO 与磁盘。

方案 B:在上传时逐块计算 HMAC(客户端与服务端同步每块)

  • 客户端对每个块计算 HMAC 并发送块签名;服务端在接收每块时验证并追加到文件。最后一个块上传成功即为完整文件已验证。

  • 或者客户端发送每个块的原始数据并在尾部发送完整文件的 HMAC(如果客户端能在本地生成完整签名)。

方案 C:使用流式哈希(服务端)

如果服务端直接从 PHP 读取上传流(例如使用 PSR-7 或 php://input),可以用 hash_init('sha256', HASH_HMAC, $key) 并在读流时 hash_update(),最后 hash_final()。示例:

<span><span><span class="hljs-meta">&lt;?php</span></span><span>
</span><span><span class="hljs-variable">$key</span></span><span> = </span><span><span class="hljs-title function_ invoke__">getenv</span></span><span>(</span><span><span class="hljs-string">'UPLOAD_HMAC_KEY'</span></span><span>);
</span><span><span class="hljs-variable">$algo</span></span><span> = </span><span><span class="hljs-string">'sha256'</span></span><span>;

</span><span><span class="hljs-comment">// 初始化 HMAC 上下文</span></span><span>
</span><span><span class="hljs-variable">$context</span></span><span> = </span><span><span class="hljs-title function_ invoke__">hash_init</span></span><span>(</span><span><span class="hljs-variable">$algo</span></span><span>, HASH_HMAC, </span><span><span class="hljs-variable">$key</span></span><span>);

</span><span><span class="hljs-comment">// 假设我们从 php://input 或文件流逐块读取</span></span><span>
</span><span><span class="hljs-variable">$stream</span></span><span> = </span><span><span class="hljs-title function_ invoke__">fopen</span></span><span>(</span><span><span class="hljs-string">'php://input'</span></span><span>, </span><span><span class="hljs-string">'rb'</span></span><span>);
</span><span><span class="hljs-keyword">while</span></span><span> (!</span><span><span class="hljs-title function_ invoke__">feof</span></span><span>(</span><span><span class="hljs-variable">$stream</span></span><span>)) {
    </span><span><span class="hljs-variable">$chunk</span></span><span> = </span><span><span class="hljs-title function_ invoke__">fread</span></span><span>(</span><span><span class="hljs-variable">$stream</span></span><span>, </span><span><span class="hljs-number">8192</span></span><span>);
    </span><span><span class="hljs-keyword">if</span></span><span> (</span><span><span class="hljs-variable">$chunk</span></span><span> === </span><span><span class="hljs-literal">false</span></span><span>) </span><span><span class="hljs-keyword">break</span></span><span>;
    </span><span><span class="hljs-title function_ invoke__">hash_update</span></span><span>(</span><span><span class="hljs-variable">$context</span></span><span>, </span><span><span class="hljs-variable">$chunk</span></span><span>);
}
</span><span><span class="hljs-variable">$serverSig</span></span><span> = </span><span><span class="hljs-title function_ invoke__">hash_final</span></span><span>(</span><span><span class="hljs-variable">$context</span></span><span>);
</span><span><span class="hljs-title function_ invoke__">fclose</span></span><span>(</span><span><span class="hljs-variable">$stream</span></span><span>);

</span><span><span class="hljs-comment">// 然后比对 clientSig ...</span></span><span>
</span></span>

防止回放攻击与关联元数据

单纯的 HMAC 只能保证完整性与认证(源自持有密钥的一方),但容易发生“回放”(同一文件 + 签名被重放)。防护方法:

  • 在签名时将文件的时间戳、nonce(随机数)、上传者 ID 等一起签名,例如把这些元数据拼接为 signature = HMAC(key, filename + ":" + file_sha256 + ":" + timestamp + ":" + nonce),并把 timestampnonce 一并发送。

  • 服务端检查 timestamp(例如允许 ±5 分钟)并检查 nonce 是否已使用(需要持久化已用 nonce 一段时间)防止重放。

  • 对于 API:使用短期有效的上传令牌(pre-signed upload token),并在 token 中嵌入 expiration 与签名。服务端在验证文件 HMAC 之前先检查 token 的有效性。

示例签名方案(文件外部签名):

  • 客户端先请求一个短期上传凭证(包含 nonce、expiry,由服务端生成并签名);

  • 客户端上传文件并在 header 中放入凭证与文件 HMAC;

  • 服务端先验证凭证,再验证文件 HMAC。


密钥管理与安全建议

  1. 密钥存放:不得把 HMAC 密钥硬编码在仓库/代码中。使用环境变量、配置文件的受限存储、或专门的密钥管理系统(Vault、云 KMS)。

  2. 最小权限:密钥的用途与权限应最小化,若可能为不同用途分配不同密钥(上传签名 key 与其它服务 key 分开)。

  3. 定期轮换:制定密钥轮换策略(例如每 90 天),并支持老密钥短期验证(在轮换期间同时支持新/旧密钥),以避免验证中断。

  4. 审计:记录签名失败的请求、来源 IP、时间,便于事后检测攻击行为。

  5. 使用 HTTPS/TLS:签名和文件必须通过 TLS 传输,避免中间人窃听签名或窃取密钥。


使用 hash_equals 做比对,避免时序攻击

不要用 ===== 或字符串拼接比较签名;应使用 hash_equals() 做常量时间比较,例如:

<span><span><span class="hljs-keyword">if</span></span><span> (!</span><span><span class="hljs-title function_ invoke__">hash_equals</span></span><span>(</span><span><span class="hljs-variable">$serverSig</span></span><span>, </span><span><span class="hljs-variable">$clientSig</span></span><span>)) {
    </span><span><span class="hljs-comment">// 拒绝</span></span><span>
}
</span></span>

hash_equals() 在长度不等或早期不匹配时也能防止时间差被利用。


处理其他安全问题(文件类型、权限)

HMAC 验证只是完整性与认证的一环,还应当:

  • 验证文件类型(MIME 类型+文件头签名),避免执行恶意脚本。

  • 限制大小、限制上传频率、对用户进行大小与类型白名单控制。

  • 将用户上传目录与 web 根目录分离,设置合适文件权限(不可执行)。

  • 对可执行文件做额外扫描(病毒扫描 / 沙箱分析)在高风险场景下。


进阶场景:服务器端签名(下发预签名 URL)

有些架构希望服务器签发一个“预签名 URL”或令牌给客户端,让客户端直接上传到对象存储(S3、GCS 等)。在这种场景:

  • 服务器在生成预签名 URL 时,可以内嵌 HMAC(或使用云存储自身签名机制),并告知客户端后续上传时需带上特定 header(例如 X-Expected-SHA256)。

  • 当文件上传到云存储后,服务器可以通过回调或后续检索来校验对象的 HMAC 或对象完整性(例如对存储中的文件重新计算 HMAC,或在上传时让客户端提供签名并由云存储回传元数据)。细节依赖于云存储的功能。


常见问题答疑(简短)

问:为什么还需要 HMAC,如果有 HTTPS?
HTTPS 保护传输安全,但不能阻止合法持有密钥者上传恶意文件或在服务器端被篡改后重新上传。HMAC 能保证“具有该密钥的一方在本次上传中生成了签名并且文件内容在传输/存储前后没有被修改”。

问:是否可以只用 hash_file()(无密钥)?
hash_file() 只能校验完整性(是否被篡改),但任何人都能伪造该哈希值。若你不希望公开哈希值,且只有服务器负责计算并比对,hash_file() 也有用处;但若要对上传发起方验证,应该用 HMAC。


总结(操作要点)

  • 使用 hash_hmac_file()(或 hash_init + hash_update)在服务端高效计算文件 HMAC;客户端用相同密钥签名并发送签名(或由服务端签发预签名凭证)。

  • 使用 hash_equals() 做时序安全的比较。

  • 始终通过 HTTPS 传输签名与文件,密钥使用安全存储与轮换策略。

  • 对于大文件/分片上传,使用流式 HMAC 或分片签名方案,并在服务端在最终合并或每片验证时保持一致性。

  • 将 HMAC 作为防篡改与来源验证的一部分,结合文件类型检测、权限控制及病毒扫描,构成完整的防护体系。