能驗證文件未被篡改,並且只有知道密鑰的一方能生成正確的簽名。與單純哈希(如SHA256)不同,HMAC 使用密鑰防止偽造。
對上傳文件的正確流程通常是:
生成/分發一個共享密鑰(或使用服務端私鑰);
客戶端或發送方對文件計算HMAC(例如HMAC-SHA256),並隨上傳一起發送簽名(HTTP header 或表單字段);
服務端接收文件後用相同密鑰重新計算HMAC,並用時序安全比較( hash_equals )比對簽名,匹配則通過校驗。
永遠通過HTTPS 傳輸簽名和文件。簽名在明文的HTTP 上會被竊聽並複用。
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"><?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->insert('uploads', ['name'=>..., 'sig'=>$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"><?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 再合併的策略。
優點:實現簡單;缺點:需要在服務端臨時存儲/合併完整文件,可能佔用IO 與磁盤。
客戶端對每個塊計算HMAC 並發送塊簽名;服務端在接收每塊時驗證並追加到文件。最後一個塊上傳成功即為完整文件已驗證。
或者客戶端發送每個塊的原始數據並在尾部發送完整文件的HMAC(如果客戶端能在本地生成完整簽名)。
如果服務端直接從PHP 讀取上傳流(例如使用PSR-7 或php://input),可以用hash_init('sha256', HASH_HMAC, $key)並在讀流時hash_update() ,最後hash_final() 。示例:
<span><span><span class="hljs-meta"><?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) ,並把timestamp 、 nonce一併發送。
服務端檢查timestamp (例如允許±5 分鐘)並檢查nonce是否已使用(需要持久化已用nonce 一段時間)防止重放。
對於API:使用短期有效的上傳令牌(pre-signed upload token),並在token 中嵌入expiration 與簽名。服務端在驗證文件HMAC 之前先檢查token 的有效性。
示例簽名方案(文件外部簽名):
客戶端先請求一個短期上傳憑證(包含nonce、expiry,由服務端生成並簽名);
客戶端上傳文件並在header 中放入憑證與文件HMAC;
服務端先驗證憑證,再驗證文件HMAC。
密鑰存放:不得把HMAC 密鑰硬編碼在倉庫/代碼中。使用環境變量、配置文件的受限存儲、或專門的密鑰管理系統(Vault、雲KMS)。
最小權限:密鑰的用途與權限應最小化,若可能為不同用途分配不同密鑰(上傳簽名key 與其它服務key 分開)。
定期輪換:制定密鑰輪換策略(例如每90 天),並支持老密鑰短期驗證(在輪換期間同時支持新/舊密鑰),以避免驗證中斷。
審計:記錄簽名失敗的請求、來源IP、時間,便於事後檢測攻擊行為。
使用HTTPS/TLS :簽名和文件必須通過TLS 傳輸,避免中間人竊聽簽名或竊取密鑰。
不要用== 、 ===或字符串拼接比較簽名;應使用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”或令牌給客戶端,讓客戶端直接上傳到對象存儲(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 作為防篡改與來源驗證的一部分,結合文件類型檢測、權限控制及病毒掃描,構成完整的防護體系。