# 认证 · 令牌刷新为什么这么难

> grok login 背后是一套比想象中复杂得多的鉴权：OIDC/OAuth2/device-code/外部命令四条路，令牌刷新的单点变更与双花防护，以及一个依赖倒置的 seam。这一章看它为什么难、又怎么做对——实现如今已拆成独立的 xai-grok-login crate。

- 作者：David（道雾轩）
- 专栏：Grok Build 源码解读（https://daiw.org/manual/grok-build.md）
- 最后更新：2026-09-15
- 原文：https://daiw.org/manual/grok-build/auth-config
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

# 认证 · 令牌刷新为什么这么难

`grok login` 看着简单，背后是这个仓库里最讲究的子系统之一。为什么难？因为**令牌会过期、要刷新、多进程还会抢着刷**，一步错就把用户踢下线。

## 一个依赖倒置的 seam

`xai-grok-auth` 是个**接缝** crate：数据收集器(`xai-file-utils`)需要「刷新感知的令牌解析」来驱动 HTTP，但又不该反向依赖 shell 的类型。于是抽两个 trait:`HttpAuth`(盖鉴权头)、`AuthCredentialProvider`(`snapshot()` + `refresh_after_unauthorized()`)。`AuthRetryMiddleware` 就靠它做 **401 自动刷新重试**。如今 sampler、otel、telemetry、tools、workspace 等十个 crate 都依赖这层接缝。**用一个 seam crate 打断循环依赖**，是大型 Rust 工程常见手法。

写作之后，trait 背后的实现也搬了家：登录与刷新整块从 `xai-grok-shell::auth` 拆成了独立 crate **`xai-grok-login`**(`AuthManager`、OIDC、device code、外部命令、凭据存储都在这里)，shell 再以 `xai_grok_shell::auth` 重新导出，老路径照常解析。

## 四条登录路

`AuthMode`(`xai-grok-login/src/model.rs`):`Oidc`(企业 SSO)、`ApiKey`、`External`(外部命令)、`WebLogin`(旧版，已弃，只为读旧 `auth.json`，缓存里遇到会迁到 OIDC)。登录流程(`flow.rs`)按序尝试：缓存 → 静默刷新 → **外部鉴权命令**(`sh -c`,stdout=token/stderr=UX)与 devbox 自动迁移 → 交互式(企业 OIDC 或 xAI OAuth2)。`/login` 这类强制重登只是跳过缓存，并不清掉它。

外部命令现在明确分两种契约：设了 `GROK_AUTH_EXPIRED=1` 是**无人值守的静默刷新**(stdin 关闭、stderr 被吞，几秒就超时，拿不到就交回登录流程)；不设则是有人在场的登录(stderr 直达用户，上限 300 秒)。

交互式 OAuth 用 **PKCE**，还**同时跑两条路竞速**：Path A 起本地 loopback HTTP server 收回调，Path B 让你从浏览器**粘贴**——因为远程 VM 上回调回不到 CLI，粘贴是刚需。CI/远程还有 **device code 流**(RFC 8628，轮询换 token)。

## 刷新：单点变更 + 双花防护

令牌刷新是命门。`AuthManager` 把**所有变更**收在一个 `refresh_chain`(落盘与判定只在 `apply_refresh_outcome` 一处发生)，配一把 async `refresh_lock`。为什么这么严？

<Callout type="warn">
  **RT 双花会触发 IdP 令牌族撤销**——两个进程/线程拿同一个 refresh token 各刷一次，IdP 认为令牌被盗，**把整族令牌全撤了**，用户直接掉线。所以刷新必须：① 全局串行(进程内 mutex + 跨进程文件锁)；② 拿到锁先看磁盘：兄弟进程已经轮换过，就直接收养它的新 token(`try_adopt_disk_token`)，绝不拿旧 RT 再去换(否则 `invalid_grant`)；③ 文件锁拿到后还要校验 inode，防止锁文件被并发删掉重建后两边各持一把。还有**系统睡眠**这一关：刷新跨过一次挂起，可能丢掉轮换后的新 RT、磁盘上只剩被撤销的旧 RT。现在的 sleep gate(`manager/sleep_gate.rs`)在睡眠将至时推迟还没开始的刷新，**绝不中途丢弃**进行中的交换，并暂扣系统的睡眠确认，直到它跑完或超时；闸门的时限用 `DualClock`(单调 + 墙钟)计——挂起时单调时钟暂停、墙钟照走，两者之差才量得出睡了多久。
</Callout>

这些坑单看每一个都隐蔽，合起来就是「为什么令牌刷新这么难」的答案。

## 接回采样器

鉴权最终要喂给 [Part 1 的采样器](https://daiw.org/manual/grok-build/custom-models)——那个 `bearer_resolver` 就是 `GrokAuthCredentials`(现在也在 `xai-grok-login`):deployment key → 裸 `Bearer`(两者都有时它优先);user token → `Bearer` + `X-XAI-Token-Auth`(头名和值都用 `obfstr` 混淆)。凭据的 `snapshot().token` **必须和实际发的 bearer 一致**，否则 401 归因的前缀对不上。

秘密卫生贯穿始终：嵌入字面量 `obfstr` 混淆、`Debug` 只露 token 末 12 位、`auth.json` 权限 `0o600` 且写后 `fsync`(文件坏了先备份成 `auth.json.corrupt.<毫秒>` 再重写)。

## 和《从 LLM 到 Coding Agent》对照

教学最小实现里鉴权就是「读个 API key 塞进头」。grok 的增量是一整套**生产级鉴权**：四条登录路、PKCE 竞速、device flow、以及令牌刷新那一堆「多进程 + 挂起 + RT 撤销」的血泪防护。这些代码不产生智能，却决定用户会不会莫名其妙掉线。下一章看它收集(与不收集)哪些遥测。

---

上一篇：[CLI 与启动](https://daiw.org/manual/grok-build/cli-surface) · 下一篇：[遥测与用量](https://daiw.org/manual/grok-build/telemetry-usage)
