Specify CLI 认证机制详解:auth.json 配置、GitHub / Azure DevOps 鉴权与凭证安全
【免费下载链接】spec-kit💫 Toolkit to help you get started with Spec-Driven Development项目地址: https://gitcode.com/GitHub_Trending/sp/spec-kit
本文基于 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中所有githubprovider 条目的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三种动态/静态方案中选择。
【免费下载链接】spec-kit💫 Toolkit to help you get started with Spec-Driven Development项目地址: https://gitcode.com/GitHub_Trending/sp/spec-kit
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考