Skip to content

网关登录

让 Claude Code 通过 OAuth 设备流程、本地批准用户,或 Google 等 OIDC 提供方登录 shunt。

Updated View as Markdown

网关登录为每个 Claude Code 用户提供各自会轮换的 OAuth 会话,而不是分发同一个共享的客户端 token。它是可选启用的:没有 [server.gateway] 时,任何 OAuth 或设备批准路由都不存在。

1. 配置登录界面

创建一个至少 32 字节的签名密钥,以及一份逗号分隔的 email:secret 批准用户列表。两者都放在 shunt 的环境变量里,而不是 shunt.toml 中:

export SHUNT_GATEWAY_JWT_SECRET="$(openssl rand -base64 48)"
export SHUNT_GATEWAY_USERS='alice@example.com:<unique-secret>,bob@example.com:<unique-secret>'

添加 Claude Code 和用户浏览器可以触达的公开 URL,并让 [server.gateway.session] 指向该密钥:

[server.gateway]
public_url = "https://gateway.example.com"
users_env = "SHUNT_GATEWAY_USERS"            # default
trust_forwarded_for = false                   # default
# state_path = "~/.shunt/gateway-sessions.json"  # default; "" = memory-only sessions

[server.gateway.session]
jwt_secret = "${SHUNT_GATEWAY_JWT_SECRET}"
ttl_hours = 1                                 # default

如果 public_url 不是一个纯 HTTPS origin(只有回环地址允许 http)、token TTL 为零、签名密钥短于 32 字节,或者既没有配置有效的用户列表也没有配置有效的外部 IdP,启动会安全失败(fail closed)。静态用户的 secret 中可以包含 :,因为只有第一个冒号用于分隔 email 与其 secret。

已弃用的 jwt_secret_env(env 变量名,默认 SHUNT_GATEWAY_JWT_SECRET)和 token_ttl_seconds(默认 3600)在单独使用时仍完全受支持,token_ttl_seconds 仍是设置小于一小时生命周期的唯一方式。同时设置一个已弃用的键和它对应的 session.* 替代键会导致启动失败(jwt_secret_envsession.jwt_secret,或 token_ttl_secondssession.ttl_hours)。只要已弃用的键被显式设置——无论是在配置文件中,还是通过 SHUNT_* 环境变量 override——shunt 都会记录一次弃用警告,只有当该键本身完全未被设置时才会保持沉默(不设置 jwt_secret_env、只依赖 SHUNT_GATEWAY_JWT_SECRET env 变量来保存 secret 的配置仍然不会触发警告)。完整的优先级规则和密钥轮换步骤见配置参考

改用 Google OIDC

在 Google Cloud 中创建一个 OAuth web client,并使用这个完全一致的授权重定向 URI:

https://gateway.example.com/device/callback

把它的 secret 放进网关的环境变量,然后配置 issuer 和一份必填的允许列表:

export SHUNT_GATEWAY_OIDC_SECRET='<google-client-secret>'
[server.gateway.oidc]
issuer = "https://accounts.google.com"
client_id = "<google-client-id>"
client_secret_env = "SHUNT_GATEWAY_OIDC_SECRET" # default
allowed_domains = ["example.com"]
# allowed_emails = ["contractor@outside.example"]

Google 使用默认的 openid email profile scope。shunt 要求 Google UserInfo 返回 email_verified = true,然后仅当完整 email 或域名(不区分大小写)与允许列表匹配时才放行该用户。

对于 GitHub、SAML 或其他不暴露 shunt 所需标准 OIDC 界面的提供方,请在其前面放一个诸如 Dex 的 OIDC 身份提供方,并在此处配置 Dex 的 issuer。直接对接特定提供方的 OAuth2 集成不在范围内。

issuer 和每个 endpoint 都必须使用 HTTPS。明文 HTTP 仅在 localhost127.0.0.1 上被接受:批准页面的 Content Security Policy 只能点名这两个回环主机,因此位于 [::1] 或其他 127.0.0.0/8 地址上的 IdP 会在启动时被拒绝,而不是之后在浏览器中被拦截。

allowed_domainsallowed_emails 至少需要一个非空条目;缺少它时 shunt 拒绝启动。配置了 [server.gateway.oidc] 后,users_env 变为可选。保留 SHUNT_GATEWAY_USERS 会同时显示 SSO 和密码登录,取消设置则只显示提供方按钮。

所有非回环部署都请使用 HTTPS。默认情况下,/device 忽略 X-Forwarded-ForX-Real-IP,并按 socket 对端限流。如果 shunt 只能通过一个可信的反向代理触达,可以设置 trust_forwarded_for = true,并把该代理配置为先移除客户端提供的转发头部,再写入它自己可信的客户端地址。绝不要在直接暴露的网关上启用这个选项。

2. 下发 Claude Code 托管登录设置

在每台开发者机器上设置这些托管设置:

{
  "forceLoginMethod": "gateway",
  "forceLoginGatewayUrl": "https://gateway.example.com"
}

托管设置的位置因平台而异:

  • macOS:/Library/Application Support/ClaudeCode/managed-settings.json
  • Linux 与 Windows (WSL):/etc/claude-code/managed-settings.json
  • Windows 原生:C:\Program Files\ClaudeCode\managed-settings.json

该 URL 必须与 public_url 相同。Claude Code 从 shunt 的发现文档中读取 OAuth 端点路径。签发的 bearer 门控 /v1/models,以及那些所选提供方会注入服务端凭据的推理请求;透传提供方保持开放。

3. 登录

启动 Claude Code 并运行 /login。CLI 会显示一个设备码,并打开网关的 /device 页面。在该页面上:

  1. 确认显示的设备码。
  2. 选择 SSO 按钮(Google 为 Sign in with Google,其他提供方为 Sign in with SSO),然后完成提供方登录。如果同时配置了静态用户,输入 email 与 secret 并选择 Approve device 的方式依然可用。
  3. 出现成功页面后返回 Claude Code。

预填设备码绝不会自动批准它。密码批准的 POST 受同源保护:网关信任浏览器的 Sec-Fetch-Site: same-origin Fetch Metadata 信号,因为页面的 Referrer-Policy: no-referrer 使浏览器在提交自己的表单时也发送 Origin: null。因此,经过会剥离 Sec-Fetch-* 头的反向代理时,所有批准都会被 “This request came from another site and was blocked.” 拒绝。而外部 callback 则用一次性、十分钟有效的 OAuth state 与 PKCE 绑定这次跨站重定向;提供方的错误绝不会被回显到页面上。

改用终端登录

要以按用户的身份接入网关,forceLoginMethod: "gateway" 并不是唯一途径。shunt 也提供了同一套 device flow 的客户端一侧,用户可以在终端登录,同时让 Claude Code 不进入已登录的网关会话:

shunt gateway login https://gateway.example.com   # 同样在浏览器里完成 /device 批准
shunt gateway claude                              # 启动已连接网关的 Claude Code

shunt gateway login 会把签发的会话保存到 ~/.shunt/gateway/session.json(仅属主可读写;可用 SHUNT_GATEWAY_SESSION_FILE 覆盖)。随后 shunt gateway claude 会在启动 Claude Code 时附带一个内联 --settings 文档,提供网关 base URL 和指向 shunt gateway tokenapiKeyHelper,且只作用于这一个进程 —— ~/.claude/settings.json 完全不会被修改。shunt gateway logout 删除该会话。完整参数见 CLI 参考

哪些会变、哪些不会:

客户端内 /loginforceLoginMethod shunt gateway login + shunt gateway claude
浏览器批准 需要 需要 —— 同一个 /device 页面,同样的 same-origin 保护
网关侧的按用户身份 有,同一个 device flow 会话
客户端侧的功能取舍 会被套用 不会 —— 凭据经由 apiKeyHelper 到达,客户端保持通常的 first-party 模式
模型别名 网关会话把 opus/sonnet 固定为较旧的 id 与普通会话相同
凭据类型闸门 会触发 同样会触发 —— 提供 apiKeyHelper 本身就是触发条件,与会话闸门相互独立(提示缓存 TTL 默认值、Remote Control、语音听写、发布 artifact)
来自 GET /managed/settings 的按用户策略 会下发 未观测到客户端去请求 —— 需要客户端强制执行策略时请用 forceLoginMethod: "gateway"

token 落在哪个凭据槽位决定了客户端的 provider 模式。上表针对 Claude Code 2.1.234 实测,可能随客户端版本变化。

托管设置与模型策略

登录之后,shunt 通过需认证的 GET /managed/settings 提供该用户解析后的策略。配置有序的 [[server.gateway.policies]] 条目:

[[server.gateway.policies]]
[server.gateway.policies.match]
emails = ["alice@example.com"]
[server.gateway.policies.cli]
availableModels = ["claude-opus-4-8"]
[server.gateway.policies.cli.env]
DISABLE_UPDATES = "1"

[[server.gateway.policies]]
match = {} # catch-all
[server.gateway.policies.cli.permissions]
deny = ["WebFetch"]

所有兜底(catch-all)条目按顺序合并。随后第一个匹配 email 的条目再合并到其之上。对象递归合并,允许列表(allow-list)数组直接替换,而键名中包含 deny 的数组取并集且不产生重复项。配置了策略时总是返回 200;当没有用户专属或兜底设置适用时,若启用了遥测,响应只包含注入的遥测 env,否则为 {}。省略 policies 时返回 404,以便 Claude Code 区分“没有托管策略”这一情形。响应包含稳定的按用户 uuid、设置的 checksum,以及一个包含该 checksum 的 RFC 引号形式 ETag;内容未变化时 If-None-Match 返回 304,并且同样接受 weak、逗号列表、通配符以及旧式未加引号的 validator。

availableModels 解析为字符串数组时,shunt 还会在 /v1/messages/v1/messages/count_tokens 上对该网关用户强制执行它。比较之前,它会从客户端请求的模型中剥掉一个末尾的 Claude Code 上下文窗口提示([1m][1M]),因此 allowed[1m] 能匹配 allowed 条目。被拒绝的模型会得到 400 invalid_request_error,且不会联系上游。

遥测接收

至少 opt-in 一个 signal 的遥测目标列表会同时做两件事。它通过托管设置下发遥测启用标志和五个 OTEL_* 环境变量值(每个 signal 的 exporter 在有目标 opt-in 该 signal 时为 otlp,否则为 none;OTEL_EXPORTER_OTLP_ENDPOINT 设为你的 public_url),把每个托管客户端的 exporter 指向网关。它还为客户端随后 POST 的入站路由启用 verbatim 中继:POST /v1/metricsPOST /v1/logsPOST /v1/traces —— 只要启用了 [server.gateway] 就会注册,在有目标 opt-in 之前接收后丢弃。策略中的 env 键仍覆盖注入的默认值:

[server.gateway.telemetry]
[[server.gateway.telemetry.forward_to]]
url = "https://collector.example.com"
# metrics = true   # 默认
# logs = false     # 默认
# traces = false   # 默认
# headers = { "x-api-key" = "..." }

url 是与 OTEL_EXPORTER_OTLP_ENDPOINT 同形的 base OTLP 端点。shunt 会去掉末尾的 / 并追加 signal 路径,因此上面的目标会收到 https://collector.example.com/v1/metrics。查询字符串、fragment 或 URL 中嵌入的 user:password 会在启动时被拒绝。

每个目标按 signal opt-in。metrics 默认开启;logs 和 traces 默认关闭 —— Claude Code 的 log record 和 span 可能携带 command line、prompt 和文件路径,把它们发送到主机之外应当是一个明确的选择。在应当接收它们的目标上设置 logs = truetraces = true

入站路由要求与 /managed/settings 相同的网关 bearer;静态 [server.auth] token 无法通过认证。载荷 verbatim 中继 —— 原样转发请求字节,保留入站的 content-typecontent-encoding,并在其上应用目标配置的 headers(配置的键会替换转发值,而不是重复该 header)。客户端的 Authorization 头永远不会转发给 collector,中继也不跟随重定向。

响应始终是立即的 200:中继作为分离任务运行,缓慢或不可达的 collector 不会成为客户端可见的延迟;没有任何目标 opt-in 的 signal 会被接收后丢弃而不是拒绝。超过 32 MiB 入站上限的 body 返回 413。同时在途的中继最多 64 个,超出的载荷不会排队,而是携带警告直接丢弃。

会话行为

access token 是 HS256 JWT,默认有效期一小时。Claude Code 会静默刷新它们。每次刷新都会轮换不透明的 refresh token;在 30 天、每族 64 条 tombstone 的范围内重放一个保留的旧 token,会使该轮换族中当前有效的 token 失效,并让 Claude Code 重新登录。

设备授权与尝试计数器存放在内存中。refresh token 会话在配置热重载后依然存在,并按下文所述默认持久化。签名密钥、用户列表和 OIDC 配置的变更会热生效。过期的授权与闲置的限流条目会被伺机清除;设备授权与限流身份各自上限为 4,096 条。已使用的 refresh token tombstone 保留 30 天,每族上限 64 条;而 30 天未刷新的活动会话会过期。添加或移除 [server.gateway] 表本身需要重启,因为路由注册在启动时即已固定。

refresh 会话默认能在 shunt 重启后存活:shunt 在每次授权或轮换后,把 refresh token 存储写入 state_path(默认 ~/.shunt/gateway-sessions.json,原子写入,仅属主权限(Unix 上为 0600)),并在启动时恢复,因此用户可以继续刷新,而不必重跑浏览器流程。refresh token 以 SHA-256 哈希形式存储 —— 该文件从不包含可用的凭据,只有 token 哈希和已登录的身份。文件缺失或损坏时只会退回到仅内存的行为,无法解析出 home 目录的环境也一样。设置 state_path = "" 可获得仅内存会话,此时重启会清除 refresh 会话,用户在其 access JWT 过期后需要重新登录。无论哪种方式,设备授权都只存在于内存中(登录途中重启只损失那一次尝试),而且这个状态文件不能在并发运行的多个 shunt 进程之间共享。

请注意,refresh 授权使用会话中存储的身份来签发 token,不会重新检查静态用户列表或外部 IdP 允许列表,因此从任一批准来源中移除一个用户并不会终止其已有会话。要立即撤销某个用户,还需删除状态文件(或设置 state_path = "")并重启。

同时配置 [server.auth][server.gateway] 时,两者可以组合:有效的静态客户端 token 或有效的网关 bearer 都能获得访问权限。这支持分阶段迁移而不破坏现有客户端。

后续计划

托管策略、ETag 缓存、遥测环境变量下发、需认证的入站 OTLP 遥测接收与中继,以及服务端模型允许列表的强制执行已在上文描述。

Navigation

Type to search…

↑↓ navigate↵ selectEsc close