📚 系列導覽:本篇是「Cloudflare 反爬蟲機制」系列第 3 篇,對應總覽篇六層偵測中的第 3 層(HTTP/2 連線層)。
# 筆記 主題 01 反爬蟲偵測總覽 六層偵測全景、請求時序地圖 02 TLS 指紋:JA3 與 JA4 ClientHello 為何洩漏身分、兩代演算法 03 HTTP/2 指紋與 Akamai Fingerprint(本篇) 連線層參數指紋、三方交叉驗證 04 Managed Challenge 與 Bot Score 主動挑戰機制、PAT、Cookie 綁定
目錄
- 什麼是 HTTP/2 指紋?
- 基礎補課:HTTP/1.1 與 HTTP/2 的差異
- 為什麼 HTTP/2 比 HTTP/1.1 更會洩漏身分
- 指紋來源 1:SETTINGS frame
- 指紋來源 2:WINDOW_UPDATE
- 指紋來源 3:PRIORITY 與依賴樹
- 指紋來源 4:Pseudo-header 順序
- Akamai Fingerprint 格式組成
- 三方交叉驗證:最強的偵測手段
- 常見問題
- 總結
什麼是 HTTP/2 指紋?
HTTP/2 指紋是把客戶端在 HTTP/2 連線建立階段送出的協定參數,濃縮成一個可比對的字串,用來識別客戶端使用的是哪一套 HTTP/2 實作。
一句話理解:TLS 指紋看「你怎麼開始加密」,HTTP/2 指紋看「你怎麼開始一個 HTTP/2 連線」。
這兩者是完全獨立的證據,由不同的軟體元件產生。這個獨立性正是它在偵測上的價值所在。
為什麼需要第二份指紋?
只有 TLS 指紋的問題:
- 可自訂 ClientHello 的 TLS 函式庫已經存在,TLS 指紋不再是無法跨越的門檻
- 單一訊號無法交叉驗證,一旦被模仿就完全失效
- TLS 層看不到任何 HTTP 語意(走 h2 還是 h1、請求怎麼組織)
加入 HTTP/2 指紋之後:
- 多一份需要獨立修補的證據——TLS 函式庫與 HTTP/2 函式庫通常是不同的元件
- 兩份指紋必須「互相吻合」才合理(Chrome 的 TLS 指紋應該配 Chrome 的 h2 指紋)
- 加上
User-Agent的宣稱,形成三方比對
核心特點
HTTP/2 指紋 = SETTINGS + WINDOW_UPDATE + PRIORITY + Pseudo-header 順序
基礎補課:HTTP/1.1 與 HTTP/2 的差異
要理解 HTTP/2 指紋,必須先知道 HTTP/2 多了哪些東西。如果你已熟悉 HTTP/2,可以跳到為什麼更會洩漏身分。
HTTP/1.1:純文字,一次一個
HTTP/1.1 的請求就是可讀的文字:
GET /page HTTP/1.1
Host: example.com
User-Agent: ...
Accept: text/html
它的限制:
- 一個連線同時只能處理一個請求。雖然
keep-alive讓連線可以重複使用,但請求必須排隊,前一個回應沒回來、後一個就不能開始 - 佇列頭部阻塞(head-of-line blocking):排在前面的慢請求會卡住後面所有請求
- 標頭每次完整重傳:同一個連線上的每個請求都重複送一遍
User-Agent、Cookie,非常浪費 - 瀏覽器的因應對策是同時開 6 個 TCP 連線到同一網域,硬撐平行度
HTTP/2:二進位框架,單連線多工
HTTP/2 把通訊改成二進位的框架(frame):
一個 TCP 連線
├─ Stream 1 ─── HEADERS frame ─── DATA frame ───▶ 請求 A
├─ Stream 3 ─── HEADERS frame ─── DATA frame ───▶ 請求 B
├─ Stream 5 ─── HEADERS frame ─── DATA frame ───▶ 請求 C
└─ 連線層 ─── SETTINGS ─── WINDOW_UPDATE ───▶ 連線參數
主要改變:
| 特性 | HTTP/1.1 | HTTP/2 |
|---|---|---|
| 編碼 | 純文字 | 二進位框架 |
| 並行 | 需開多個 TCP 連線 | 單一連線多工(stream) |
| 標頭 | 每次完整重傳 | HPACK 壓縮 |
| 請求行 | GET /page HTTP/1.1 |
拆成四個 pseudo-header |
| 流量控制 | 依賴 TCP | 協定層自己有一套 |
| 優先級 | 無 | 有(原機制已廢棄,見後文) |
| 標頭大小寫 | 不強制 | 強制小寫 |
主要的 frame 類型
| Frame | 用途 | 指紋價值 |
|---|---|---|
SETTINGS |
宣告連線層參數 | 🔴 極高 |
WINDOW_UPDATE |
調整流量控制視窗 | 🔴 高 |
PRIORITY |
宣告 stream 優先級與依賴 | 🔴 高 |
HEADERS |
送出請求或回應的標頭 | 🟡 中(pseudo-header 順序) |
DATA |
送出主體資料 | 低 |
RST_STREAM |
中止單一 stream | 低 |
GOAWAY |
關閉連線 | 低 |
PING |
連線保活與延遲測量 | 低 |
連線的開場
HTTP/2 連線建立後,客戶端要先送一段固定的連線前言(connection preface),是一個magic 字串,接著立刻送出 SETTINGS frame。
TLS 握手完成(ALPN 協商為 h2)
↓
客戶端送出連線前言(固定字串,無指紋價值)
↓
客戶端送出 SETTINGS frame ← 指紋開始
↓
客戶端可能送出 WINDOW_UPDATE ← 指紋
↓
客戶端可能送出 PRIORITY frames ← 指紋
↓
客戶端送出第一個 HEADERS frame ← 這時才是「HTTP 請求」
└─ pseudo-header 順序 ← 指紋
└─ User-Agent 在這裡 ← 宣稱
注意這個順序:SETTINGS 在第一個 HTTP 請求之前就送出去了。這是 HTTP/2 指紋的關鍵——它不需要等你發請求。
為什麼 HTTP/2 比 HTTP/1.1 更會洩漏身分
HTTP/1.1 能洩漏的東西很有限
| 可觀察項 | 辨識力 | 能否輕易偽造 |
|---|---|---|
| 標頭名稱與順序 | 🟡 中 | ✅ 純字串,照抄即可 |
| 標頭大小寫寫法 | 🟢 低 | ✅ 可控制 |
Connection 標頭寫法 |
🟢 低 | ✅ 可控制 |
HTTP/1.1 是純文字協定,所有可觀察的東西都在應用層,都是字串。只要願意,程式碼可以逐字模仿真瀏覽器的請求。
HTTP/2 多出了一整個「連線層」
HTTP/2 引入了應用程式通常不會碰到的協定層設定:
| 可觀察項 | 辨識力 | 能否輕易偽造 |
|---|---|---|
SETTINGS 參數的組合、值、順序 |
🔴 極高 | ❌ 多數函式庫不開放設定 |
WINDOW_UPDATE 增量 |
🔴 高 | ❌ 通常寫死 |
PRIORITY frame 的存在與樹狀結構 |
🔴 高 | ❌ 需深度改造 |
| pseudo-header 順序 | 🔴 高 | ❌ 由 h2 函式庫產生 |
| HPACK 索引策略 | 🟡 中 | ❌ 由 h2 函式庫決定 |
兩個關鍵差異
差異 1:時序更早
SETTINGS 在任何 HTTP 請求之前送出。也就是說,伺服器在還不知道你要請求哪個路徑、也還沒看到你的 User-Agent 時,就已經知道你用的是哪一類 HTTP/2 實作了。
差異 2:控制權在函式庫
這是更本質的差異。以一段典型的程式碼為例,應用程式能設定的通常只有「網址、方法、標頭、主體」。至於 SETTINGS_INITIAL_WINDOW_SIZE 要送多少、要不要送 WINDOW_UPDATE、pseudo-header 用什麼順序排列——這些連 API 都沒有暴露出來,它們寫在 HTTP/2 函式庫的原始碼裡。
應用程式碼可控 函式庫寫死(不可控)
───────────────── ──────────────────
網址 SETTINGS 送哪些、值多少、順序
HTTP 方法 是否送 WINDOW_UPDATE、增量多少
標頭名稱與值 是否送 PRIORITY、樹的形狀
請求主體 pseudo-header 排列順序
HPACK 的索引決策
這個分野與 TLS 指紋的情況完全平行——可控的部分不具決定性,具決定性的部分不可控。
指紋來源 1:SETTINGS frame
SETTINGS 是 HTTP/2 指紋最主要的來源。
有哪些參數
RFC 定義了六個標準參數:
| 編號 | 名稱 | 用途 | 協定預設值 |
|---|---|---|---|
0x1 |
SETTINGS_HEADER_TABLE_SIZE |
HPACK 動態表大小 | 4096 |
0x2 |
SETTINGS_ENABLE_PUSH |
是否接受伺服器推送 | 1(接受) |
0x3 |
SETTINGS_MAX_CONCURRENT_STREAMS |
最多同時幾個 stream | 無限制 |
0x4 |
SETTINGS_INITIAL_WINDOW_SIZE |
每個 stream 的初始流量視窗 | 65535 |
0x5 |
SETTINGS_MAX_FRAME_SIZE |
單一 frame 最大位元組 | 16384 |
0x6 |
SETTINGS_MAX_HEADER_LIST_SIZE |
標頭清單最大位元組 | 無限制 |
三重指紋訊號
同一個 SETTINGS frame 提供三層獨立的辨識訊號:
1. 送出哪些參數(集合)
有的實作只送 2 個,有的送 5 個
「完全不送 SETTINGS」本身也是一種特徵
2. 每個參數的值(數值)
視窗大小、表格大小的具體數字差異很大
這些數字是各實作經過效能調校後寫死的
3. 參數的排列順序(序列)
RFC 未規定順序,各實作固定送出自己的順序
代表性差異(示意)
瀏覽器類型 A(示意)
HEADER_TABLE_SIZE = 65536 ← 遠大於預設的 4096
ENABLE_PUSH = 0 ← 明確拒絕推送
INITIAL_WINDOW_SIZE = 6291456 ← 6 MB,為了大檔案吞吐調校過
MAX_HEADER_LIST_SIZE= 262144
瀏覽器類型 B(示意)
HEADER_TABLE_SIZE = 65536
INITIAL_WINDOW_SIZE = 131072 ← 128 KB,明顯不同的調校取向
MAX_FRAME_SIZE = 16384
某通用 HTTP 函式庫(示意)
INITIAL_WINDOW_SIZE = 65535 ← 直接沿用 RFC 預設值
(只送一個參數)
⚠️ 上述數值為示意,實際值依軟體版本而異,且會隨改版變動。這裡要看的是模式差異,不是背下具體數字。
這裡最值得注意的模式
「沿用協定預設值」本身就是強烈訊號。
真實瀏覽器經過長年的效能調校,幾乎不會使用 RFC 的預設值——它們的視窗大小、表格大小都是為了實際網頁載入情境特別調整過的。相對地,通用 HTTP 函式庫的作者通常沒有這種調校動機,直接用預設值或只調一兩項。
所以看到「只送一兩個參數、且值等於 RFC 預設」時,基本可以判定不是瀏覽器。而這一點不需要任何指紋資料庫就能判斷——這與 JA4 的 a 段可讀性是同一種思路。
指紋來源 2:WINDOW_UPDATE
流量控制在做什麼
HTTP/2 有自己的流量控制機制,獨立於 TCP。接收方宣告「我還能接收多少位元組」,發送方不能超過這個量。
SETTINGS_INITIAL_WINDOW_SIZE 設定的是每個 stream 的初始視窗。但還有一個連線層級的視窗,它的初始值固定為 65535(RFC 規定,不能用 SETTINGS 改),要調大只能靠送出 WINDOW_UPDATE frame 追加增量。
連線層視窗初始值:65535(協定規定,不可改)
↓
客戶端送 WINDOW_UPDATE,增量 = 15663105(示意)
↓
連線層視窗變成:65535 + 15663105 ≈ 15 MB
指紋價值
這個增量是一個單一的、寫死的數字,辨識力相當高:
| 觀察 | 含義 |
|---|---|
| 送出一個特定的大增量 | 對應某個特定的瀏覽器實作 |
完全不送 WINDOW_UPDATE |
沿用 65535 的預設視窗——多數通用 HTTP 函式庫的行為 |
| 送出不同的增量值 | 對應另一種實作 |
為什麼瀏覽器要調大?因為 64 KB 的連線層視窗在現代網頁(動輒數 MB 的資源)下會頻繁觸發流量控制,嚴重拖慢載入。瀏覽器有強烈動機調大它;而一個只抓 HTML 的爬蟲函式庫沒有這個動機。
同樣的邏輯再次出現:效能調校的痕跡本身就是「這是真瀏覽器」的證據。
指紋來源 3:PRIORITY 與依賴樹
這是四個來源中辨識力最強、也最難模仿的一個。
瀏覽器為什麼需要優先級
一個網頁包含 HTML、CSS、JavaScript、圖片、字型。它們對「首次可見」的重要性差異巨大:
關鍵路徑:HTML → CSS → 阻塞渲染的 JS ← 沒有這些,畫面一片空白
次要: 字型、可見區域的圖片 ← 影響體驗但不阻塞
最低: 可見區域外的圖片、預先載入 ← 可以最後再說
在 HTTP/1.1 時代,瀏覽器靠「開幾個連線、先發哪個請求」來間接控制。HTTP/2 把所有請求塞進單一連線後,就必須有明確的機制告訴伺服器「這些 stream 誰先誰後」。
原始機制:依賴樹
RFC 7540 定義的機制是一棵依賴樹:每個 stream 可以宣告「我依賴哪個 stream」以及「權重」(1–256)。
Stream 0(連線根節點)
│
┌───────────┼───────────┐
Stream 3 Stream 5 Stream 7
權重 256 權重 220 權重 147
(CSS) (JS) (字型)
│
Stream 9
權重 110
(圖片)
關鍵行為:某些瀏覽器在連線一開始、還沒發出任何實際請求時,就先送出一組 PRIORITY frame 把這棵樹的骨架建好,之後的請求再掛到對應節點上。
為什麼辨識力極強
真瀏覽器
✅ 連線初期送出一組 PRIORITY frame
✅ 樹的形狀、stream ID、權重值高度固定
✅ 不同瀏覽器的樹形狀明顯不同(各自的資源排程策略)
通用 HTTP 函式庫
❌ 完全不送 PRIORITY
理由很直接:它不是在載入網頁,沒有多種資源要排程
「有沒有送 PRIORITY」幾乎是一道二元的判別線。而且要偽造不只是「送一個 frame」——必須複製正確的樹狀結構、正確的 stream ID 分配、正確的權重值,並且在後續請求中維持一致的掛載行為。多數 HTTP 函式庫連 API 都沒有。
協定演進的注意事項
RFC 7540 的優先級機制在實務上被認為過於複雜、實作差異大、效果不佳。後續發展:
- RFC 9113(2022,HTTP/2 的修訂版)將原優先級機制標記為已廢棄
- RFC 9218 定義新的「可擴充優先級」(Extensible Priorities),改用
priority標頭欄位與PRIORITY_UPDATEframe
現代瀏覽器已陸續改用新機制。但這不代表這個指紋來源消失了——換成新機制之後,「送不送 priority 標頭、值怎麼設、什麼時候送 PRIORITY_UPDATE」同樣是實作特徵。指紋的載體變了,訊號依然存在。
這是指紋技術的一般規律:只要協定留下實作自由度,就會留下指紋。
指紋來源 4:Pseudo-header 順序
這是最容易理解、卻出乎意料有效的一個。
什麼是 pseudo-header
HTTP/1.1 的請求行是一整行文字:
GET /page HTTP/1.1
Host: example.com
HTTP/2 取消了請求行,把資訊拆成四個特殊標頭,名稱以冒號開頭,稱為 pseudo-header:
| Pseudo-header | 對應 HTTP/1.1 的什麼 |
|---|---|
:method |
請求行的方法(GET) |
:path |
請求行的路徑(/page) |
:scheme |
協定(https) |
:authority |
Host 標頭 |
RFC 沒有規定順序
RFC 要求 pseudo-header 必須排在所有普通標頭之前,但四者之間的相對順序沒有規定。於是每個實作各自選了一個順序並寫死。
用四個字母的首字表示(m = method、a = authority、s = scheme、p = path):
| 順序(示意) | 縮寫 |
|---|---|
:method :authority :scheme :path |
m,a,s,p |
:method :path :authority :scheme |
m,p,a,s |
:method :scheme :path :authority |
m,s,p,a |
:method :path :scheme :authority |
m,p,s,a |
⚠️ 具體哪個順序對應哪個軟體會隨版本變動,這裡的重點是「不同實作用不同順序,且各自固定」。
為什麼這麼有效
- 只有 4 個元素,24 種排列——資訊量看似很小
- 但它完全不需要額外資料:每個 HTTP/2 請求都必然帶著這四個 pseudo-header
- 而且它把主流瀏覽器與主流 HTTP 函式庫分到不同的桶子裡
- 對應用程式碼而言完全不可控:你在程式裡設定標頭的順序,不會影響函式庫排列 pseudo-header 的順序
四個字母,零成本採集,且能把「瀏覽器」與「函式庫」有效分群——這是投報率極高的一個訊號。
附帶訊號:HPACK 與普通標頭順序
HTTP/2 用 HPACK 壓縮標頭。HPACK 有一個 61 項的靜態表(預先定義的常見標頭),加上一個連線期間動態增長的動態表。
雖然標頭被壓縮了,但解壓後的順序完整保留且可觀察。此外還有更細緻的訊號:
- 哪些標頭被放進動態表建立索引、哪些以字面值傳送——這是實作的決策
- 動態表大小的使用策略
真瀏覽器的標頭順序高度固定且有明確規律;手寫的客戶端常常是字典序,或是程式碼裡的插入順序。
Akamai Fingerprint 格式組成
Akamai 在 2017 年的一份 HTTP/2 被動指紋研究中提出了一個標準化的表示格式,後來被廣泛採用,通稱 Akamai fingerprint 或 HTTP/2 fingerprint。
格式
四段,用 | 分隔:
SETTINGS | WINDOW_UPDATE | PRIORITY | PSEUDO_HEADER_ORDER
| 段 | 內容 | 缺失時 |
|---|---|---|
| 第 1 段 | SETTINGS 的 編號:值,多組用 ; 分隔,保持送出順序 |
留空 |
| 第 2 段 | 連線層 WINDOW_UPDATE 的增量 |
00 |
| 第 3 段 | PRIORITY frame,格式 streamId:獨占:依賴的streamId:權重,多組用 , 分隔 |
0 |
| 第 4 段 | pseudo-header 的首字母,用 , 分隔 |
— |
範例對照(示意)
瀏覽器類型(示意)
1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p
└────────────┬───────────────┘ └───┬──┘ │ └──┬──┘
送 4 個 SETTINGS,值皆調校過 調大視窗 │ 瀏覽器順序
└─ 此例未送 PRIORITY
某通用 HTTP 函式庫(示意)
4:65535|0|0|m,p,s,a
└──┬──┘ │ │ └──┬──┘
只送 1 個 │ │ 函式庫順序
值為 RFC │ └─ 不送 PRIORITY
預設值 └─ 不送 WINDOW_UPDATE
怎麼讀這個字串
不必背任何具體值,看四個特徵就能判斷:
1. SETTINGS 段有幾組?
4 組以上且值明顯經過調校 → 像瀏覽器
1-2 組且值等於 RFC 預設 → 像通用函式庫
2. WINDOW_UPDATE 是 00 嗎?
00 → 沿用 64KB 預設,不在意大檔案吞吐 → 像函式庫
大數值 → 為網頁載入調校過 → 像瀏覽器
3. PRIORITY 段是 0 嗎?
0 → 沒有資源排程需求 → 像函式庫
有樹狀結構 → 在載入複雜網頁 → 像瀏覽器
4. 第 4 段的順序,對應哪一類實作?
這四個問題全都不需要查表,這正是這個格式設計得好的地方——它跟 JA4 的 a 段一樣,把可讀性當成一等公民。
三方交叉驗證:最強的偵測手段
這一節是本篇、也是整個系列的結論。
三份獨立證據
到目前為止,我們有三份在不同階段、由不同軟體元件產生的證據:
階段 3:TLS 握手
JA4 指紋 ← 由 TLS 函式庫產生(OpenSSL / BoringSSL / NSS / crypto-tls)
階段 4:HTTP/2 連線初始化
Akamai 指紋 ← 由 HTTP/2 函式庫產生(nghttp2 / h2 / net-http2 / 瀏覽器內建)
階段 5:HTTP 請求
User-Agent ← 由應用程式碼填寫
三者由不同元件負責,彼此之間沒有任何機制保證一致。
矛盾偵測
案例:一個聲稱是現代瀏覽器的請求
User-Agent 說:「我是 Chrome 140 / macOS」
JA4 說:無 ALPN、擴充數量偏少、加密套件雜湊對應 OpenSSL 系
Akamai 指紋 說:只送 1 個 SETTINGS(值為 RFC 預設)、不送 WINDOW_UPDATE、
不送 PRIORITY、pseudo-header 順序對應通用函式庫
↓
三份證據指向三個不同的軟體堆疊 → 高度可信的偽裝判定
為什麼矛盾比單一訊號更有力:
| 情況 | 判斷 | 為什麼 |
|---|---|---|
誠實的 API 客戶端(UA 寫 python-requests) |
🟢 低風險 | 三份證據一致:它確實是 Python |
| 真實瀏覽器 | 🟢 低風險 | 三份證據一致 |
| UA 寫 Chrome,但底層是 Python | 🔴 高風險 | 三份證據矛盾,且矛盾方向顯示刻意偽裝 |
注意第一列:誠實反而更安全。這常常違反直覺,但符合防禦邏輯——偵測系統要找的不是「機器人」(機器人多得是且大部分合法),而是「假裝不是機器人的機器人」。
為什麼難以修補
要讓三方一致,必須讓整條網路堆疊都對應同一個目標軟體:
需要一致的層 修補難度
────────────────── ──────────
TLS 實作(ClientHello) 需可自訂 ClientHello 的函式庫
HTTP/2 實作(連線層參數) 需可自訂 SETTINGS/WINDOW_UPDATE/PRIORITY
HTTP 標頭 照抄即可
JavaScript 執行環境(系列 04) 需真實瀏覽器
這解釋了一個實務上常見的挫折:專門處理 TLS 指紋的工具只解決了三分之一。它讓 JA4 對上了,但如果 HTTP/2 連線層仍由原本的函式庫負責,第二份證據依然矛盾——而且此時矛盾模式更奇特(TLS 像瀏覽器、h2 像函式庫),在統計上反而更顯眼。
真瀏覽器的天然優勢
真實瀏覽器
TLS 層、HTTP/2 層、JS 引擎 → 都是同一個軟體的組成部分
→ 一致性是免費的,不需要任何努力
模仿方
需要讓多個獨立元件同時對應同一個目標
→ 每一層都要維護,且目標軟體每次改版都要跟上
這個不對稱是整套偵測體系的基礎。它不是靠某個聰明的演算法,而是靠架構上的成本差異。
常見問題
問題 1:把 HTTP/2 關掉、改用 HTTP/1.1 會不會比較不容易被偵測?
回答:不會,反而更可疑。
表面邏輯是對的:HTTP/1.1 可觀察的東西少得多。但這忽略了 ALPN。
現代瀏覽器在 TLS 握手時,一律透過 ALPN 擴充宣告支援 h2。而這件事會被記錄在 JA4 的 a 段第 9-10 碼(見系列第 02 篇)。
一個聲稱是 Chrome 的請求,卻走 HTTP/1.1
→ JA4 的 a 段顯示 ALPN 不是 h2
→ 「現代瀏覽器不協商 HTTP/2」本身就是矛盾
→ 你少了 HTTP/2 指紋這份證據,但同時製造了一個新矛盾
淨效果是負的:減少一份證據,換來一個明確的異常訊號。
問題 2:HTTP/1.1 就完全沒有指紋嗎?
回答:有,但弱得多,且都是可偽造的字串層面。
| 可觀察項 | 說明 |
|---|---|
| 標頭順序 | 真瀏覽器順序固定;手寫客戶端常是字典序或插入序 |
| 標頭大小寫 | HTTP/1.1 不強制小寫,各實作寫法有差異(User-Agent vs user-agent) |
| 標頭之間的空白處理 | 冒號後的空格數等細節 |
Connection 標頭 |
送不送、值怎麼寫 |
這些全都在應用層,理論上都能逐字模仿。這正是 HTTP/2 指紋更有價值的原因——它把觀察點推到了應用程式碼碰不到的層次。
問題 3:HTTP/3(QUIC)有指紋嗎?
回答:有,而且結構上是類似的。
HTTP/3 跑在 QUIC 上,QUIC 把傳輸與加密整合在一起。可指紋化的部分包括:
- QUIC transport parameters:類似 HTTP/2 的
SETTINGS,在連線建立時交換,同樣由實作寫死 - QUIC Initial packet 的結構特徵
- HTTP/3 自己的
SETTINGSframe - TLS 1.3 的 ClientHello(QUIC 內嵌 TLS 1.3 握手)——JA4 的第 1 碼
q就是為此設計的
一般規律再次適用:只要協定給實作留下自由度,就會留下指紋。換協定不會消除指紋,只會換一組指紋。
問題 4:常見誤解整理
誤解 1:HTTP/2 指紋只是 TLS 指紋的補充,重要性不高。
正確理解:它是一份獨立的證據,由不同的函式庫產生。獨立性正是它的價值——這使它必須被獨立修補,也使交叉驗證成為可能。
誤解 2:只要把標頭順序調成跟瀏覽器一樣,HTTP/2 指紋就對上了。
正確理解:標頭順序只是附帶訊號。四個主要來源(SETTINGS、WINDOW_UPDATE、PRIORITY、pseudo-header 順序)全部在應用程式碼碰不到的層次,需要函式庫本身支援自訂才能改變。
誤解 3:PRIORITY 機制已被 RFC 廢棄,所以這個指紋來源沒用了。
正確理解:原機制被廢棄,但取代它的可擴充優先級(RFC 9218)同樣有實作自由度。「送不送 priority 標頭、值怎麼設」是新的指紋載體。載體變了,訊號還在。
誤解 4:Akamai fingerprint 是 Akamai 專有的技術,只有 Akamai 用。
正確理解:它是一種表示格式(如何把觀察到的參數寫成字串),由 Akamai 的研究提出並公開,任何人都能計算與使用。它跟具體廠商無關。
總結
核心要點
HTTP/2 指紋 = 四個連線層來源的組合
SETTINGS → 送哪些、值多少、什麼順序 🔴 極高辨識力
WINDOW_UPDATE → 增量多少、送不送 🔴 高
PRIORITY → 送不送、樹的形狀 🔴 高(近二元判別)
Pseudo-header → 四個標頭的排列順序 🔴 高(零成本採集)
Akamai 格式:SETTINGS|WINDOW_UPDATE|PRIORITY|PSEUDO_ORDER
三方交叉驗證(本系列主線):
JA4(TLS 函式庫)
Akamai 指紋(HTTP/2 函式庫) → 三者必須一致才合理
User-Agent(應用程式碼)
理解重點
-
時序比 TLS 稍晚,但仍在請求之前:
SETTINGS在第一個 HTTP 請求之前送出。伺服器在還不知道你要什麼路徑時,就知道你用哪類實作了。 -
效能調校的痕跡就是身分證明:瀏覽器把視窗調到 6 MB、把 HPACK 表調到 64 KB,是因為要載入複雜網頁。一個只抓 HTML 的函式庫沒有這種動機,於是沿用 RFC 預設值——而「沿用預設值」本身就是特徵。
-
獨立性是核心價值:TLS 指紋與 HTTP/2 指紋由不同函式庫產生,沒有任何機制保證兩者一致。這使交叉驗證成為可能,也使模仿必須逐層進行。
-
矛盾比單一訊號更有力:誠實回報自己是 Python 的客戶端風險低;TLS 像 Python 卻聲稱是 Chrome 的客戶端風險高。偵測系統要找的是偽裝,不是機器人。
-
關掉 HTTP/2 不是解法:那會讓 JA4 的 ALPN 欄位露出矛盾,用一份證據換一個異常訊號,淨效果為負。
-
協定演進不會消除指紋:
PRIORITY被廢棄了,新機制照樣有實作自由度。只要有自由度,就有指紋。
快速記憶
| 方面 | 說明 |
|---|---|
| 是什麼 | HTTP/2 連線建立階段的協定參數組合 |
| 什麼時候採集 | 第一個 HTTP 請求之前 |
| 為什麼難偽造 | 四個來源全在 HTTP/2 函式庫內部,應用程式碼無 API 可控 |
| 辨識力最強的單項 | PRIORITY 送不送(近乎二元判別) |
| 投報率最高的單項 | pseudo-header 順序(4 個字母,零成本) |
| 最強用法 | 與 JA4、User-Agent 三方交叉驗證 |
| 最大的誤解 | 以為改標頭順序就能修好 |
學習路徑
1. 建立六層全景 ✓ 系列 01
2. 理解 TLS 層的第一份證據 ✓ 系列 02
3. 理解 HTTP/2 層的第二份獨立證據 ✓ 本篇
4. 理解主動挑戰在檢驗什麼 → 系列 04
延伸閱讀
- 反爬蟲偵測總覽 - 六層偵測全景與請求時序
- TLS 指紋:JA3 與 JA4 - 第一份證據,含 ALPN 與 a 段
- Managed Challenge 與 Bot Score - 第三層防線:瀏覽器執行環境
- HTTP 與 HTTPS - HTTP 協定基礎
- Nginx 基礎 - 反向代理與 HTTP/2 設定
建立日期:2026-09-07 最後更新:2026-09-07