Specify CLI 认证机制详解:auth.json 配置、GitHub / Azure DevOps 鉴权与凭证安全
本文基于 spec-kit 仓库的认证参考文档,完整讲解 Specify CLI 的 opt-in(显式启用)认证机制:如何编写 ~/.specify/auth.json、hosts/provider/auth 等字段的取值与校验规则、GitHub 与 Azure DevOps 各类认证方案(bearer、basic-pat、azure-cli、azure-ad)的配置示例,以及凭证匹配、401/403 回退与重定向凭证剥离的底层实现。读完后你可以为公共 GitHub、GitHub Enterprise Server 或 Azure DevOps 私有目录(catalog)/扩展(extension)/预设(preset)下载正确配置凭据,并理解其安全边界。
1. 设计原则:不配置即不发送凭据
Specify CLI 对目录源 HTTP 请求、扩展下载和版本发布检查均采用**可选认证(opt-in authentication)**模型:
- 只有当你显式创建
~/.specify/auth.json时,CLI 才会向对应主机附加Authorization头; - 该文件不存在时,所有 HTTP 请求均以未认证方式发出;
- 配置文件中“哪些主机 + 哪个 provider + 哪种认证方案”由你声明,provider 类定义“如何认证”(Bearer、Basic-PAT 等)。
这一模型在 src/specify_cli/authentication/init.py 的模块 docstring 中被明确陈述,其内置 provider 注册表 AUTH_REGISTRY 通过 _register_builtins() 注册了 github 与 azure-devops 两个内置 provider。
2. 配置文件结构
认证配置全部集中在用户主目录下的单个 JSON 文件中:
mkdir -p ~/.specify
# 将下文 JSON 写入 ~/.specify/auth.json
chmod 600 ~/.specify/auth.json
最小可用的 GitHub 配置:
{
"providers": [
{
"hosts": ["github.com", "api.github.com", "raw.githubusercontent.com", "codeload.github.com"],
"provider": "github",
"auth": "bearer",
"token_env": "GH_TOKEN"
}
]
}
安全提示:建议将文件权限收紧为仅属主可读写(
chmod 600)。这一点并非只是文档建议——从源码看,config.py 在加载配置时会检查文件权限位,若在 POSIX 系统上该文件对 group/others 可读,会发出UserWarning提醒执行chmod 600,但不会因此失败。
2.1 字段参考
providers 数组中每个条目的通用字段:
| 字段 | 必填 | 说明 |
|---|---|---|
hosts |
是 | 该条目适用的主机名数组。仅支持精确主机名或以 *. 开头的子域通配(如 *.visualstudio.com)。*.visualstudio.com 匹配 foo.visualstudio.com,但不匹配 visualstudio.com 本身。*github.com、gith?b.com 等其他 glob 模式不被支持,会在加载时被拒绝。 |
provider |
是 | 内置 provider 键:github 或 azure-devops。 |
auth |
是 | 认证方案,见下节。 |
token |
否 | 内联 token 值。可能时优先使用 token_env。 |
token_env |
否 | 存放 token 的环境变量名。 |
azure-ad 方案额外要求三个字段:
| 字段 | 必填 | 说明 |
|---|---|---|
tenant_id |
是 | Azure AD 租户 ID。 |
client_id |
是 | 服务主体(service principal)客户端 ID。 |
client_secret_env |
是 | 存放 client secret 的环境变量名。 |
bearer 与 basic-pat 方案必须至少设置 token 或 token_env 之一。
2.2 源码中的校验规则
load_auth_config() 对配置的约束比文档表格更具体,了解它们可以避免配置不生效:
- host 模式白名单:
_is_valid_host_pattern()只接受两种形式——精确主机名和*.suffix。它显式拒绝含?、[、]的模式,以及*出现在其他位置的写法。注释中说明动机:*github.com会匹配github.com.evil.com这类危险形式,因此必须禁用。 - 归一化:所有
hosts值在存储前会被strip().lower();token_env、tenant_id、client_id、client_secret_env等字符串引用字段经_norm()去除首尾空白,防止“验证通过但环境变量查不到”的静默故障。 - provider/scheme 兼容性:未知
provider或该 provider 不支持的auth值会抛出ValueError,错误信息会列出已注册的 provider 或该 provider 支持的方案列表。 - schema 违规的容错策略:文件不存在返回空列表(即全部未认证请求);JSON 结构错误则抛
ValueError,而更高层的 HTTP 辅助函数会捕获它、告警后继续以未认证方式运行(见 http.py 的_load_config(),配置按进程缓存,文件最多读取一次)。
3. Provider 与认证方案
3.1 GitHub(github)
| 方案 | 请求头 | 适用场景 |
|---|---|---|
bearer |
Authorization: Bearer <token> |
PAT、细粒度 PAT(fine-grained PAT)、OAuth token、GitHub App token |
GitHubAuth 类只声明了 key = "github" 与 supported_auth_schemes = ("bearer",),其 auth_headers() 对非 bearer 方案直接抛 ValueError;token 解析则继承基类默认逻辑:优先读 entry.token,否则读 token_env 指定的环境变量,并对值做 strip()。
示例 — 通过环境变量注入 PAT:
{
"hosts": ["github.com", "api.github.com", "raw.githubusercontent.com", "codeload.github.com"],
"provider": "github",
"auth": "bearer",
"token_env": "GH_TOKEN"
}
3.2 GitHub Enterprise Server(GHES)
若目录或扩展托管在自管的 GHES 实例上,只需添加一条 github 条目列出该实例的主机名。同一条目既用于认证 catalog JSON 拉取,也用于私有 release 资产下载——Specify 会识别这些主机为 GitHub Enterprise,并将 release 下载解析到 GHES REST API(/api/v3)。
{
"providers": [
{
"hosts": ["ghes.example.com", "raw.ghes.example.com", "codeload.ghes.example.com"],
"provider": "github",
"auth": "bearer",
"token_env": "GH_ENTERPRISE_TOKEN"
}
]
}
配置要点:
- 必须列出裸 web 主机(如
ghes.example.com),因为 release 下载 URL 就挂在它下面; - 若实例使用子域隔离,还要把 catalog/扩展 URL 实际用到的
raw.、codeload.子域一并列出; *.ghes.example.com通配只匹配子域、不匹配裸主机,因此裸主机必须显式列出。
源码侧可以印证这一机制:http.py 的 github_provider_hosts() 会收集 auth.json 中所有 github provider 条目的 hosts,供 resolve_github_release_asset_api_url() 使用。该函数把浏览器式下载 URL(https://<host>/<owner>/<repo>/releases/download/<tag>/<asset>)解析为 REST API 资产 URL;主机是否按 GHES 处理正是由这份白名单决定——未列入的主机不会被当作 GHES,从而阻止恶意 catalog 诱导向任意主机发起 API 请求。扩展、预设、workflow 的安装路径(如 extensions/__init__.py、presets/__init__.py、commands/bundle/__init__.py)都通过传入 github_provider_hosts() 复用这一逻辑。
3.3 Azure DevOps(azure-devops)
AzureDevOpsAuth 支持四种方案:
| 方案 | 请求头 | 适用场景 |
|---|---|---|
basic-pat |
Authorization: Basic base64(:<PAT>) |
个人访问令牌(PAT) |
bearer |
Authorization: Bearer <token> |
预先获取的 OAuth / Azure AD token |
azure-cli |
Authorization: Bearer <token> |
通过 az account get-access-token 获取 token |
azure-ad |
Authorization: Bearer <token> |
通过 OAuth2 client credentials 流程获取 token |
示例 — 环境变量注入 PAT:
{
"hosts": ["dev.azure.com"],
"provider": "azure-devops",
"auth": "basic-pat",
"token_env": "AZURE_DEVOPS_PAT"
}
示例 — Azure CLI(交互式登录):
{
"hosts": ["dev.azure.com"],
"provider": "azure-devops",
"auth": "azure-cli"
}
要求此前已执行 az login。从源码看,_acquire_via_az_cli() 会以 30 秒超时执行 az account get-access-token --resource 499b84ac-1321-427f-aa17-267ca6975798 --output json 并解析 accessToken 字段;它特意用 shutil.which(尊重 Windows 的 PATHEXT)解析出绝对路径的 az,避免工作目录中一个名为 az.cmd 的恶意文件被当成凭证操作执行——任何失败都会返回 None 并回退到下一个策略,而不是抛出异常。
示例 — Azure AD 服务主体(CI/自动化):
{
"hosts": ["dev.azure.com"],
"provider": "azure-devops",
"auth": "azure-ad",
"tenant_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"client_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"client_secret_env": "AZURE_CLIENT_SECRET"
}
_acquire_via_client_credentials() 会向 https://login.microsoftonline.com/<tenant_id>/oauth2/v2.0/token 发起 POST,请求 client_credentials 授权与 499b84ac-1321-427f-aa17-267ca6975798/.default 作用域。该请求有三层防护:拒绝一切 307/308 重定向(否则 POST 体中的 client_secret 会被原样转发到别处);响应经 read_response_limited 按上限读取,超大响应视为失败;网络故障、JSON 解析失败等一律返回 None 落入回退。
4. 多条目配置
可以同时配置多个条目以覆盖不同主机或组织,条目按数组顺序匹配:
{
"providers": [
{
"hosts": ["github.com", "api.github.com", "raw.githubusercontent.com", "codeload.github.com"],
"provider": "github",
"auth": "bearer",
"token_env": "GH_TOKEN"
},
{
"hosts": ["dev.azure.com"],
"provider": "azure-devops",
"auth": "basic-pat",
"token_env": "AZURE_DEVOPS_PAT"
}
]
}
5. 工作机制与源码级实现
文档描述的运行时行为可概括为五步,下面逐条对照 src/specify_cli/authentication/http.py 的实现:
- 主机匹配:每个出站请求先用 find_entries_for_url() 解析 URL 的 hostname,与
auth.json中各条目的hosts模式比对(*.前缀通配对hostname.endswith(pattern[1:])求值,因此只匹配子域)。畸形 authority(如未闭合的 IPv6 方括号)被视为无主机名,返回空匹配而非抛异常。 - 附加凭证:命中后由对应 provider 的
resolve_token()取 token,auth_headers()构造Authorization头。build_request()中有一个防绕过细节:extra_headers里的Authorization键会被剥除,认证头最后合并,外部无法覆盖。 - 401/403 回退:open_url() 遍历所有匹配条目,某一轮收到 401/403 时关闭响应、尝试下一条目;其他错误(404、500、网络错误)立即抛出。
- 未认证兜底:所有条目用尽(或无匹配)后,以无认证请求作为最后回退。
- 重定向凭证剥离:每次尝试都安装隔离的 opener 并挂上
_StripAuthOnRedirect处理器。重定向时它会:校验目标必须为带主机名的 HTTPS(仅允许环回地址之间使用 HTTP);若新主机不在该条目的声明主机内、或发生 HTTPS→HTTP 降级,则从请求和unredirected_hdrs中同时移除Authorization,防止凭据泄漏到 CDN 或第三方服务;目标 URL 畸形则抛URLError交由上层下载错误处理。
隔离 opener 的动机是代码注释中说明的:open_url() 每次自建 opener,使得进程中若有全局 urllib.request.install_opener 也无法替换掉重定向守卫。
6. 与其他鉴权路径的关系
仓库中另有一条独立的轻量路径:build_github_request() 直接读取 GITHUB_TOKEN / GH_TOKEN 环境变量,但只针对内置的四个 GitHub 官方域名(GITHUB_HOSTS:github.com、api.github.com、raw.githubusercontent.com、codeload.github.com)附加 Bearer 头,非 GitHub 主机一律不加,避免向第三方主机泄漏凭据。从源码结构看,配置驱动的 open_url() / build_request() 是扩展、预设、bundle、workflow 安装时下载资产的主通道(tests/test_authentication.py 及 tests/http_helpers.py 对其行为有测试覆盖),而 build_github_request() 服务于对 GitHub 官方域名的简单请求场景。
7. 快速上手模板
一份预置 GitHub 配置的参考 auth.json:
{
"providers": [
{
"hosts": [
"github.com",
"api.github.com",
"raw.githubusercontent.com",
"codeload.github.com"
],
"provider": "github",
"auth": "bearer",
"token_env": "GH_TOKEN"
}
]
}
启用步骤:
mkdir -p ~/.specify
# 将上方 JSON 复制到 ~/.specify/auth.json
chmod 600 ~/.specify/auth.json
# 使用前确保 GH_TOKEN 已导出
小结
spec-kit 的认证体系围绕单一事实源 ~/.specify/auth.json 展开:以主机白名单控制凭据作用范围,以 provider/scheme 双层键约束认证方式,并在请求链路上实现了 401/403 逐条目回退、未认证兜底、重定向剥离与跨域防泄漏等安全行为。对使用公共 GitHub 的用户,最小配置即是一条 bearer + token_env 条目;对 GHES 用户,额外列出裸主机即可同时打通 catalog 拉取与私有 release 下载;对 Azure DevOps 用户,则可根据交互或 CI 场景在 basic-pat、azure-cli、azure-ad 三种动态/静态方案中选择。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00