首页
/ Specify CLI 认证机制详解:auth.json 配置、GitHub / Azure DevOps 鉴权与凭证安全

Specify CLI 认证机制详解:auth.json 配置、GitHub / Azure DevOps 鉴权与凭证安全

2026-09-05 21:50:56作者:丁柯新Fawn

本文基于 spec-kit 仓库的认证参考文档,完整讲解 Specify CLI 的 opt-in(显式启用)认证机制:如何编写 ~/.specify/auth.jsonhosts/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() 注册了 githubazure-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.comgith?b.com 等其他 glob 模式不被支持,会在加载时被拒绝。
provider 内置 provider 键:githubazure-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 的环境变量名。

bearerbasic-pat 方案必须至少设置 tokentoken_env 之一。

2.2 源码中的校验规则

load_auth_config() 对配置的约束比文档表格更具体,了解它们可以避免配置不生效:

  • host 模式白名单_is_valid_host_pattern() 只接受两种形式——精确主机名和 *.suffix。它显式拒绝含 ?[] 的模式,以及 * 出现在其他位置的写法。注释中说明动机:*github.com 会匹配 github.com.evil.com 这类危险形式,因此必须禁用。
  • 归一化:所有 hosts 值在存储前会被 strip().lower()token_envtenant_idclient_idclient_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.pygithub_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__.pypresets/__init__.pycommands/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 的实现:

  1. 主机匹配:每个出站请求先用 find_entries_for_url() 解析 URL 的 hostname,与 auth.json 中各条目的 hosts 模式比对(*. 前缀通配对 hostname.endswith(pattern[1:]) 求值,因此只匹配子域)。畸形 authority(如未闭合的 IPv6 方括号)被视为无主机名,返回空匹配而非抛异常。
  2. 附加凭证:命中后由对应 provider 的 resolve_token() 取 token,auth_headers() 构造 Authorization 头。build_request() 中有一个防绕过细节:extra_headers 里的 Authorization 键会被剥除,认证头最后合并,外部无法覆盖。
  3. 401/403 回退open_url() 遍历所有匹配条目,某一轮收到 401/403 时关闭响应、尝试下一条目;其他错误(404、500、网络错误)立即抛出。
  4. 未认证兜底:所有条目用尽(或无匹配)后,以无认证请求作为最后回退。
  5. 重定向凭证剥离:每次尝试都安装隔离的 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_HOSTSgithub.comapi.github.comraw.githubusercontent.comcodeload.github.com)附加 Bearer 头,非 GitHub 主机一律不加,避免向第三方主机泄漏凭据。从源码结构看,配置驱动的 open_url() / build_request() 是扩展、预设、bundle、workflow 安装时下载资产的主通道(tests/test_authentication.pytests/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-patazure-cliazure-ad 三种动态/静态方案中选择。

登录后查看全文
热门项目推荐
相关项目推荐