拆解 Claude Code 的登入機制






上一篇寫 cc-switch,提到 Claude Code 會看 ANTHROPIC_AUTH_TOKEN 這個環境變數決定 key 從哪來。但大多數人其實沒設這個變數——你開 Pro/Max 訂閱、跑一次 claude /login 點一下瀏覽器,之後它跑了好幾天還記得你。它怎麼做到的?
如果你也跑 Pro/Max 訂閱、好奇它為什麼幾天不用重登,這篇就是給你的——分析 Claude Code 的訂閱登入:access token(短命的通行證)跟 refresh token(長命的續命卡)各自在做什麼、存在你電腦哪裡、為什麼偶爾它還是會叫你重登一次。
你的登入資訊放在哪裡
三邊都會寫到 ~/.claude/.credentials.json(Windows 路徑是 %USERPROFILE%\.claude\.credentials.json)。Mac 多一層——同一份資料也往系統 Keychain 寫一筆,開 "鑰匙圈存取" app 搜 Claude Code 就能找到。
- macOS:檔案+Keychain 並存——Keychain 靠作業系統加密保管。
- Linux:純檔案,權限
600(只有你自己讀得到)。 - Windows:純檔案,靠 Windows 預設權限限制成只有你能讀。
不管哪一邊,裡面長這樣(欄位名稱,值我抹掉):
{
"claudeAiOauth": {
"accessToken": "sk-ant-oat01-...",
"refreshToken": "sk-ant-ort01-...",
"expiresAt": 1735000000000,
"scopes": ["user:file_upload", "user:inference", "user:mcp_servers", "user:profile", "user:sessions:claude_code"]
}
}
expiresAt 是 Unix 毫秒時間戳。想看人類閱讀格式:
# macOS (BSD date)
date -r $((1735000000000 / 1000))
# Linux (GNU date)
date -d @$((1735000000000 / 1000))
為什麼要分成兩個 token
access token 是真正去打 API 那把鑰匙。每次 Claude Code 送請求,header 裡帶的就是這把。Anthropic 那邊把它設得短命——OAuth 回傳的 expires_in 是 28800 秒,剛好 8 小時。(GitHub 上有人回報更短、一兩小時就被踢,那是 refresh 出包的 bug,不是正常壽命。)短命的好處是:萬一這把外洩,能造成的時間損害有上限。
但短命就有個問題:每幾個小時就叫你重登一次,誰受得了。所以才有 refresh token——它的工作只有一件:拿來去換新的 access token。長命、不直接打 API、靜靜躺在 Keychain 或那個 600 檔裡。
兩個合作的設計就是這樣:真正會在網路上頻繁來回的那把短命;靜靜躺在你電腦上等候差遣的那把長命。 這套是 OAuth 2.0 的標準分工,Google、GitHub、你公司的 SSO 走的都是這套,Claude Code 只是把它套上來。
第一次 /login 那一刻發生了什麼
你跑 claude /login,背後大概是這樣:
- Claude Code 自己先想一串很長的亂數(叫 code verifier),把它 hash 一次得到 code challenge。
- 開系統瀏覽器,把 code challenge 跟一個 redirect URI(通常是
http://localhost:<port>)一起塞進https://claude.ai/oauth/authorize?...。 - 你在瀏覽器登入 Claude 帳號、按授權。
- claude.ai 把一個 authorization code 丟回那個 localhost。Claude Code 在背景開的小 server 接到。
- CLI 拿這個 code+第一步那串原始亂數,去 token endpoint 換成 access token+refresh token+expiresAt 一組三件。
- 寫進 Keychain 或那個 credentials 檔。
中間多一步 "亂數+hash" 叫 PKCE(Proof Key for Code Exchange,唸 pixie),用意是防中間有人偷走 authorization code 也沒用——他沒有最原始那串亂數,換不到 token。Claude Code 是裝在你電腦上的 CLI(OAuth 講的 public client,沒有藏在後端的密鑰可驗身分),這層保護尤其重要。
平常它怎麼幫你續命
每次 Claude Code 啟動或要打 API 前,會先看 expiresAt:
- 還沒到 → 拿 access token 直接打,你沒感覺。
- 到了或快到了 → 拿 refresh token 去 token endpoint 換一組新的(新 access token、新 expiresAt)。換完寫回 Keychain/檔案,繼續跑。
整段過程透明、你看不到。這也是為什麼平常用幾天還在登入狀態。
但 GitHub issues 上有一票場景會讓這層續命失敗:
- 機器等待 (sleep) 很久才醒,refresh token 那邊已經被判定失效。
- 同時開好幾個 Claude Code session,互相搶著換 token,有人拿到舊的就 401。
- 跑在 headless 機器上(CI、server)、refresh 一掛沒辦法 fallback 開瀏覽器補。
- 重開機之後,特別是 Windows 上很常見。
失敗的下場都一樣:401 → 叫你 /login 重來一次。這也是為什麼有 claude setup-token 這個指令——它幫你生一個壽命一年的 OAuth token,塞進環境變數 CLAUDE_CODE_OAUTH_TOKEN,CI/server 跑的時候就不用碰瀏覽器。
環境變數一設,整套都跳過
回到上一篇 cc-switch。我那台 Mac 上 Keychain 裡沒有那筆、~/.claude/.credentials.json 也沒生出來——因為我設了 ANTHROPIC_AUTH_TOKEN 指到 MiMo,整套 OAuth 訂閱機制連碰都沒碰。
它的優先順序大概是這樣,從上往下找,第一個有的就用、後面全部跳過:
- 雲端供應商變數(Bedrock/Vertex/Foundry)
ANTHROPIC_AUTH_TOKEN(會塞進 Bearer header)ANTHROPIC_API_KEY(會塞進 X-Api-Key header)apiKeyHelper腳本(自訂腳本動態回傳 key,例如從公司 vault 拿短命 token)CLAUDE_CODE_OAUTH_TOKEN(setup-token 生的那個)- OAuth 訂閱(也就是這篇主角)
等等——AUTH_TOKEN 跟 API_KEY 都是 key,差在哪?差在 Claude Code 把它塞進哪個 HTTP header、原本要給誰看:
ANTHROPIC_AUTH_TOKEN→Authorization: Bearer <key>。Bearer 是 OAuth/JWT 圈的標準寫法——這篇主角 OAuth 訂閱出去也是塞這個 header,AUTH_TOKEN 等於讓你親自寫第 6 條那個值,直接擋下訂閱那條路。大部分第三方 gateway 也認這個,MiMo 那把tp-...走的就是這條。ANTHROPIC_API_KEY→X-Api-Key: <key>。Anthropic 自家 API 收 key 用這個 header 名稱,第三方 gateway 大多不會去學。Claude Console 申請的sk-ant-api03-...走這條。
設錯邊基本上 401——key 本身是對的,但對方根本沒在看那個 header。兩個同時都設了,AUTH_TOKEN 贏(就是上面這順序)。
所以我切到 MiMo 那一刻、ANTHROPIC_AUTH_TOKEN 一設下去,Claude Code 連我訂閱的 access token 還沒過期都不去看——它走第 2 條路了。把那個變數拿掉,它才會回去走第 6 條、讀 Keychain/credentials 檔。
cc-switch 幫你切設定組,背後就是在第 2、3、5、6 條這幾個 token 來源之間切(第 4 條 apiKeyHelper 是動態腳本,cc-switch 不管那層)。
一句話總結:access token 短命、refresh token 長命,兩個合作下來你感覺從來沒被登出——直到那個 refresh token 也死了的那一天,才會叫你 /login 重來。
Sources:
- 上一篇 Day 153 cc-switch 切引擎:https://dawsonwang.com/day/153
- Claude Code Authentication 官方文件:https://code.claude.com/docs/en/authentication
- 反向工程那條 OAuth flow 的整理:https://akashmohan.com/writings/claude-code-oauth
- refresh 沒被觸發、被迫每天重登的回報:https://github.com/anthropics/claude-code/issues/42904