ByteScope

401

HTTP 401 Unauthorized 怎麼修

4xx 用戶端錯誤IETF 標準RFC 9110

這個請求沒有帶上可用的認證憑證——原因片語寫的是 Unauthorized,但它描述的狀況其實是「沒認證」,而且伺服器有義務用一個標頭告訴你什麼樣的憑證才行。

為什麼會看到 401,以及該怎麼處理

同樣三個數字,你站在請求的哪一邊,它就是不同的問題。直接讀你自己那一塊。

用 curl 重現 401

對著出錯的那個網址跑一次。它只印狀態碼、不印錯誤頁,所以你看到的是伺服器真正說了什麼,而不是瀏覽器畫出來的東西。

curl
curl -sS -o /dev/null -D - -w '\n%{http_code}\n' https://example.com/api/me

`-sS` 會把進度列拿掉但留下錯誤訊息,`-o /dev/null` 把錯誤頁的內容丟掉,`-D -` 把回應標頭印到標準輸出,`-w` 則在最後單獨印一行狀態碼。你要找的那個標頭是 `www-authenticate`:§15.5.2 規定 401 上一定要有它,它的第一個 token 是 scheme,而在 Bearer API 上,`error` 參數會把「沒帶」、「不合法」和「過期」分開。如果那個標頭根本不在,代表伺服器沒照規格回答,那你就只剩它自己的紀錄可以查了。接著把同一行再跑兩次,去切開狀態碼分不出的那兩種情況:一次加 `-H 'authorization: Bearer <token>'`,一次加 `-u wrong:creds`。兩次都 401,代表驗證器什麼都拒絕,問題指向金鑰、issuer 或 audience,不是你的 token;只有送真 token 那次是 401,就指向那個 token 過期或被撤銷了。要注意 `-u` 會讓 curl 主動先送 Basic 憑證,所以那一次拿到 401,完全不能證明伺服器有提供 Basic——只有 `www-authenticate` 標頭能說。如果挑戰指的是代理而不是網站,那要改看 407。

401 實際上在講什麼

RFC 9110 §15.5.2 把 401 定義成:這個請求沒有被執行,因為它缺少對目標資源有效的認證憑證;並且附了一條硬性要求——產生 401 的伺服器「必須」送出一個 `WWW-Authenticate` 標頭欄位,裡面至少帶一個挑戰。那個標頭就是 401 和 403 的全部差別:它存在的理由是「換一組憑證重試有可能會成功」,所以伺服器欠用戶端一句機器讀得懂的話,說清楚是哪一種憑證。一個沒有挑戰的 401 是違反規格的,同時也是最會吃掉你下午時間的那個版本,因為它把唯一寫著「伺服器要什麼」的欄位拿掉了。同一節也涵蓋了一個大家常覺得意外的情況:如果請求「有」帶憑證,那這個 401 的意思是那組憑證被拒絕了授權。所以同一個狀態碼同時蓋住「你什麼都沒送」和「你送了、但被打回票」,而狀態行裡沒有任何東西能分開這兩者——這就是為什麼得靠你自己的紀錄去分。§15.5.2 還交代,用戶端如果重送之後拿回同一個挑戰,應該把回應的內容顯示給使用者看,因為診斷細節通常就寫在那裡。還有兩件事會改變你該去哪裡找。§11.3 的挑戰語法是一個 scheme 名稱後面接參數,其中 `realm` 指的是保護範圍,而 scheme 決定了瀏覽器的行為:一個 `Basic` 挑戰(RFC 7617)會讓瀏覽器在你的應用程式上面疊出它自己的帳號密碼對話框。而在 token API 這邊,RFC 6750 §3.1 直接用狀態碼把情況切開,不留給大家各憑喜好:`invalid_request` 是 400、`invalid_token` 是 401、`insufficient_scope` 是 403。這樣讀的話,一個 OAuth 保護的 API 回 401,代表 token 沒帶、格式壞掉、過期或被撤銷;而一個確實是你的、只是範圍不夠的 token,本來應該回 403 才對。最後,401 不在 RFC 9110 §15.1 列為可依啟發式規則快取的狀態碼裡,所以沒有代理會自作主張把它存起來。

401 最常被搞混的幾個代碼

容易搞混的怎麼分
403401 是「我不知道你是誰」,403 是「我知道,答案還是不行」。可靠的判斷不是看字面而是看標頭:RFC 9110 要求 401 一定要有 `WWW-Authenticate`,因為換一組憑證有可能成功;而 403 什麼都不要求,因為換了也沒用。RFC 6750 §3.1 在 OAuth 上畫了同一條線——token 沒帶或不合法是 `invalid_token`、回 401;token 有效但缺少必要的 scope 是 `insufficient_scope`、回 403。
404一個你很確定存在的資源,對沒帶憑證的呼叫者回 404,是完全合法的:§15.5.4 允許一台不想承認某個被禁資源存在的伺服器改回 404,而私有版本庫和物件儲存正是這樣做的。所以在你把一個 404 當成路由 bug 去查之前,先帶著憑證重試一次——如果它變成 200 或 403,那它從頭到尾就是一個換了裝的存取決定。
400一個伺服器根本解析不了的 `Authorization` 標頭,是格式壞掉的請求,不是被拒絕的身分,而 RFC 6750 §3.1 明講了:`invalid_request` 應該回 400,不是 401。實務上各家框架做法不一,所以在一個需要認證的端點上看到 400,值得先當成「你的標頭形狀不對」來讀——少了 `Bearer ` 前綴、夾了一個換行,或者送了兩個 `Authorization` 標頭——再去假設 token 本身被拒。
407407(Proxy Authentication Required,§15.5.8)是同一個概念往前一跳:要憑證的是代理、不是來源機,而且它用 `Proxy-Authenticate` 挑戰,不是 `WWW-Authenticate`。在公司網路上,一個你解釋不了的 401 常常是一個你沒仔細讀的 407,而破綻是:那個挑戰對你試的每一台主機都出現,不是只出現在你在弄的那個 API 上。
BasicBasic scheme(RFC 7617)是把 `user:password` 做 base64,那是編碼不是加密——任何看得到那個標頭的人都讀得出密碼,所以它只有在 TLS 上才安全。它還有一個大家很少是故意要的介面副作用:瀏覽器收到 `Basic` 挑戰時,會在頁面上疊出它自己的憑證對話框,所以一個要給 JavaScript 呼叫的 API 應該改用 `Bearer` 來挑戰。
原因片語
Unauthorized
類別
4xx 用戶端錯誤
定義出處
RFC 9110
標準
IETF 標準

站上可以搭配的工具

這些全都在你的瀏覽器裡跑,不會上傳任何東西。

相關狀態碼

查 401 的時候,通常也會順手看一下這幾個。

常見問題

怎麼知道這個狀態碼是哪一台伺服器回的?

去讀回應標頭,不要讀那個網頁。curl -sS -o /dev/null -D - https://example.com/path 會只印標頭、不印內容,而 serverviacf-ray 三個加起來,就能指出是哪一層回答的。有 cf-ray 的值代表這個回應是 Cloudflare 處理的,那個值也是他們客服會跟你要的編號。想把邊緣節點整個排除掉,就加上 --resolve example.com:443:203.0.113.10 再打一次:它會照樣送出原本的主機名稱和 SNI,但直接連到你指定的來源位址——答案變了,就代表邊緣節點和來源機講的不是同一件事。

數字後面那句原因片語(像 Not Found)重要嗎?

不重要,而且絕對不要拿它來做判斷。RFC 9112 要求用戶端忽略原因片語,伺服器可以隨意改它,而 HTTP/2 和 HTTP/3 根本沒有這個欄位——所以在 HTTP/1.1 上是 404 Not Found,到了 HTTP/2 就只剩一個 404。這個網站會寫出登記在案的片語,是因為那是大家會拿去搜尋、也是會出現在 HTTP/1.1 紀錄裡的字串,不是因為有任何軟體依賴它。

這個代碼失敗的請求該不該重試?

5xx 和 429 可以重試,4xx 不要——下一次送過去也不會有任何不同。如果回應帶了 Retry-After 就照它做,RFC 9110 定義它就是為了這件事,值可以是秒數也可以是一個時間點。沒有的話,就用帶抖動的指數退避加上一個硬上限,而且只對冪等的方法這樣做:一個被重試的 POST 可能會刷兩次卡。對一台已經在失敗的伺服器丟出重試風暴,是短暫事故變成長時間事故最常見的那條路。

我可以讓自己的伺服器改回別的狀態碼嗎?

可以,唯一要守的規則是那個代碼必須是真的。所有會自動讀你回應的東西——搜尋引擎、監控、快取、用戶端的重試邏輯——都只看那個數字,從來不看那一頁寫了什麼。用 200 回一頁錯誤訊息,會把故障從你自己的警報裡藏起來;用 200 回一頁不存在的內容,會讓錯誤被當成內容收進索引;而對一個只是格式不對的請求回 500,會把值班的人送去堆疊裡錯的那一半。

401 還是卡住嗎?看完整的狀態碼對照表,或是回到上面,讀為你這一端寫的那一塊。