首页 / 资讯中心 / 文章详情

Harvester 中 go-containerregistry transport 包深入解析:容器镜像仓库 Token 与 OAuth2 认证的透明实现

Harvester 中 go-containerregistry transport 包深入解析:容器镜像仓库 Token 与 OAuth2 认证的透明实现 ★ FEATURED ARTICLE
云原生虚拟化存储后端高可用【免费下载链接】harvesterOpen source hyperconverged infrastructure (HCI) software项目地址https://gitcode.com/gh_mirrors/ha/harvester点击查看免费下载导读transport是 go-containerregistry 库中一个极其精巧的包它以net/http.RoundTripper的形式把 OCI Distribution 协议里最棘手的部分——仓库认证Token 认证与 OAuth2 认证——封装成一行代码即可启用的透明能力。在 Harvester 仓库中该包以github.com/google/go-containerregistry v0.20.2go.mod的形式被 vendored 于 vendor/github.com/google/go-containerregistry/pkg/v1/remote/transport作为remote包等镜像交互高层能力的认证底座。读完本文你将掌握该包的握手原理、Basic/Bearer 两种认证路径、scope 声明、Token 刷新与错误处理机制并拿到一个可直接运行的自定义 Registry 客户端示例。一、背景为什么认证是 OCI Distribution 协议中最难的部分OCI Distribution 规范registry API本身并不复杂无非是GET /v2/、GET /v2/name/tags/list、GET /v2/name/manifests/ref等一组有限端点的 REST 接口。但正如原文档指出的正确地实现认证才是真正困难的部分——它涉及通过WWW-Authenticate响应头解析认证挑战challenge区分Basic与Bearer两种挑战类型与 token 服务端进行 Token 交换Token Authentication支持 OAuth2 的 refresh token 流程OAuth2 Authentication处理 token 过期、scope 不足时的重新协商。transport包的价值就在于它实现了 transport.New /NewWithContext返回一个已经配置好认证能力的http.RoundTripper让上层调用者完全不用关心上述细节。包的职责说明也直接写在了 doc.go 中Package transport provides facilities for setting up an authenticated http.RoundTripper given an Authenticator and base RoundTripper.二、设计动机Raison dêtre为什么不用现成的 registry 客户端原文档花了不少篇幅解释该包存在的必要性核心观点是尽可能减少下游依赖docker/distribution客户端其缓存层blob digest 到 descriptor 的映射依赖prometheus/client_golang做命中率统计使用者会被迫拉入体积庞大的 Prometheus 依赖树containerd/containerd的 remotes/docker会连带引入 grpc、protobuf、logruscontainers/image底层同样使用docker/distribution客户端。而transport自身的依赖树非常克制它只依赖authn包而authn又只依赖 docker 的 config 文件解析与处理包——这几乎是任何要与仓库交互的程序都几乎必然需要的。减少依赖带来的收益包括下载更快、编译更快、更不容易陷入依赖地狱。需要注意的是README 中的三张依赖图docker.dot.svg等并不存在于当前仓库的 vendor 目录中本文不再引用。三、核心握手机制transport.New的完整流程transport.New现已标记 Deprecated建议使用NewWithContext的核心逻辑写在 transport.go整个握手分为四步Ping 探测用传入的底层RoundTripper对仓库执行Ping获取认证挑战信息根据挑战结果分派返回200→ 无需认证直接使用原 transport返回401 Basic挑战 → 构造basicTransport每次请求直接附带认证头返回401 Bearer挑战 → 构造bearerTransport并在初始化时立即执行一次 token 刷新refresh以播种 tokenUser-Agent 包装若底层 transport 不是userAgentTransport则套一层NewUserAgentScheme 选择根据 Ping 是否走 HTTPpr.Insecure决定使用https还是http套一层schemeTransport。此外还有一个值得注意的细节如果传入的 transport 已经是*Wrapper类型NewWithContext会直接原样返回不再做任何包装——这是给自带完整认证/重试逻辑的高级使用者预留的显式退出通道详见下文第四节。3.1 Ping探测认证挑战与协议协商ping.go 中的Ping会向仓库发起GET /v2/请求并返回一个Challenge结构Scheme挑战类型如bearer、basicParameters挑战头中的键值对例如Bearer servicegcr.io,realmhttps://auth.gcr.io/v36/tokenz中的realm与serviceInsecure是否通过 HTTP 完成的 Ping。对GET /v2/的响应处理ping.go遵循如下规则200 OK→ 无需认证返回空挑战401 Unauthorized→ 解析WWW-Authenticate挑战头若同时存在多个挑战如Negotiate和Basic并存通过pickFromMultipleChallenges优先挑选可处理的basic/bearerping.go避免随机选中无法识别的 scheme 导致unrecognized challenge错误其他状态码 → 通过CheckError包装成结构化错误返回。协议协商方面Ping默认只尝试https仅当仓库是通过name.NewInsecureRegistry显式标记为不安全时才会追加http并采用并行竞速策略先发 https 请求300msfallbackDelay后若未返回则同时发起 http 请求谁先成功用谁ping.go。这一设计借鉴了 Go 标准库net/dial.go中 Happy Eyeballs 的实现思路保证了本地 HTTP 仓库如测试用 registry也能被自动发现。3.2 scope声明你对仓库的访问权限scope.go 预定义了标准 scope 常量const ( PullScope string pull PushScope string push,pull // For now DELETE is PUSH, which is the read/write ACL. DeleteScope string PushScope CatalogScope string catalog )PullScopepull拉取镜像PushScopepush,pull推拉DeleteScope当前实现与 Push 一致删除走读写 ACLCatalogScopecatalog列出仓库目录catalog 接口。在实际使用时通常通过repo.Scope(transport.PullScope)为某个具体 Repository 生成完整 scope 字符串形如repository:google-containers/pause:pull再传给transport.New。这些 scope 会在后续每一次 token 交换/刷新中作为请求参数发送给 token 服务端。四、两种认证路径Basic 与 Bearer根据 Ping 得到的挑战类型transport.New会选择构造basicTransport或bearerTransport。4.1 basicTransport请求头直接携带凭据basic.go 的RoundTrip逻辑非常直接只要目标 host 与请求匹配就按优先级设置Authorization头若AuthConfig.RegistryToken非空 → 发送Bearer token否则若存在 username/password → 以Basic base64(user:pass)发送否则若存在预编码的Auth→ 直接以Basic auth发送。一个值得注意的安全细节由于http.Client的重定向处理发生在RoundTripper抽象层之上为避免把凭据转发到重定向目标这里仅在in.Host bt.target || in.URL.Host bt.target时才设置认证头basic.go。4.2 bearerTransportToken 交换、刷新与 scope 重新协商Bearer 认证是主流仓库Docker Hub、GCR 等采用的方案其核心在 bearer.go。关键行为1请求时附带 Bearer tokenRoundTrip在确认目标 host 匹配后设置Authorization: Bearer tokenbearer.go。2遇到挑战自动重新协商如果响应中再次出现WWW-Authenticate挑战通常是 token 过期或 scope 不足它会从挑战参数中提取缺失的scope去重后追加到已有 scopes 列表并把新 scope 放在最前面——因为有些仓库在一次 token 交换中只看第一个 scope 参数bearer.go关闭旧响应体执行refresh重新换取 token然后重发一次请求。3两种 token 换取方式refresh与Refresh见 bearer.go。refresh负责维护内部状态。若AuthConfig.RegistryToken已存在则直接复用否则调用Refresh换取新 token兼容某些仓库只返回access_token而不用token字段的差异access_token会被回填到token见 #54 相关处理若 OAuth2 流程返回了refresh_token则会用authn.FromConfig把它升级为新的凭据源供后续刷新使用。Refresh负责真正发请求。如果凭据中存在IdentityToken则走 OAuth2 流程POST 到 realm携带grant_typerefresh_token、refresh_token、scope、service、client_id并在 token 服务端返回 404 时优雅降级到 Basic GET 流程注释明确说明并非所有 token 服务端都实现了 OAuth2否则走标准 Token 流程GET 到 realm把 scopes 与 service 放入 query 参数用 Basic 认证携带凭据。两种流程都会把响应体 JSON 反序列化为Token结构type Token struct { Token string json:token AccessToken string json:access_token,omitempty RefreshToken string json:refresh_token ExpiresIn int json:expires_in }见 bearer.go。若响应中没有 token 也没有 access_token则报错no token in bearer response。4凭据脱敏token 交换请求与响应都可能包含凭据因此refreshOauth/refreshBasic都会通过redact.NewContext标记上下文确保日志系统不会输出敏感内容bearer.go。4.3 凭据从哪里来authn 包的支撑transport本身不负责获取凭据它接收一个authn.Authenticator。在 Harvester vendored 的 vendor/github.com/google/go-containerregistry/pkg/authn 中最常用的入口是DefaultKeychainkeychain.go其语义与 docker 完全一致读取~/.docker/config.json或%USERPROFILE%\.docker\config.json或DOCKER_CONFIG环境变量指定的位置未找到时回退到 Podman 的${XDG_RUNTIME_DIR}/containers/auth.json支持credHelpers配置的 credential helper如docker-credential-gcr多 keychain 可通过NewMultiKeychain按序组合全部找不到时退化为Anonymous匿名凭据。AuthConfig中凭据形态决定了认证路径存在Username/Password或Auth走 Token 流程存在IdentityTokencredential helper 返回Username为字面量token时产生则走 OAuth2 流程见 authn/README.md。五、错误处理CheckError 与结构化 Error原文档特别介绍了 CheckError当响应状态码不在期望集合中时它会把响应体解析为符合 Distribution 规范的结构化错误。Error结构error.go包含Errors []Diagnostic规范定义的错误列表每个Diagnostic由Code、Message、Detail组成error.goStatusCodeHTTP 状态码Request失败的请求用于错误信息中打印脱敏 URLrawBody无法解析为 JSON 时的原始响应体。代码中完整枚举了规范定义的错误码error.go例如BLOB_UNKNOWN、MANIFEST_UNKNOWN、NAME_UNKNOWN、UNAUTHORIZED、DENIED、TOOMANYREQUESTS等。此外Error.Error()会智能拼接无错误列表时给出unexpected status code code textHEAD 请求会特别提示HEAD 响应无 body请用 GET 获取详情单条错误直接输出多条错误以;连接Error.Temporary()用于判断错误是否暂时性可重试依据是temporaryErrorCodes如TOOMANYREQUESTS、UNKNOWN、UNAVAILABLE、BLOB_UPLOAD_INVALID与temporaryStatusCodes408、500、502、503、504两张表error.go。六、增强组件重试、User-Agent、调试日志与 scheme 选择transport包还附带多个可独立使用的RoundTripper包装器这些在remote包中被组合使用6.1 retryTransport对暂时性错误重试retry.go 提供NewRetry(inner, opts...)默认退避策略为初始 100ms、倍率 3.0、抖动 0.1、最多 3 步retry.go默认判定谓词为retry.IsTemporary。可通过WithRetryBackoff、WithRetryPredicate、WithRetryStatusCodes三个函数式选项定制。6.2 userAgentTransport统一 User-Agentuseragent.go 的NewUserAgent会把调用方提供的字符串与库自身版本拼接例如调用方传crane/v0.1.4且库构建于 v0.1.4 时最终得到User-Agent: crane/v0.1.4 go-containerregistry/v0.1.4。库版本号可通过-ldflags-X .../transport.Version$TAG注入否则从debug.ReadBuildInfo自动探测useragent.go。6.3 logTransport调试日志logger.go 的NewLogger会把请求与响应完整 dump 到pkg/logs.Debug通过httputil.DumpRequestOut/DumpResponse并做两处脱敏Authorization头替换为redacted被redact上下文标记的请求/响应体如 token 响应以[body redacted: ...]输出。6.4 schemeTransport按 Ping 结果修正协议schemer.go 只对目标 host 即仓库本身的请求改写 URL schemehttps 或 http避免误伤单独的 token 服务端或 blob 存储域。6.5 remote 包中的组合顺序与 Wrapper 退出通道remote包在 options.go 中展示了上述组件的标准装配顺序由内向外用户提供的底层 transport默认DefaultTransport若 debug 日志开启 →transport.NewLoggertransport.NewRetry默认谓词 可配置状态码若设置了 userAgent →transport.NewUserAgent放在最外层防止 transport.New 双重包装最终交给transport.New完成认证包装。同时WithTransport若收到*transport.Wrapper类型的 transport则跳过全部自动包装options.go把重试、日志、User-Agent 的完全控制权交给调用者。七、实战示例手动编写一个列出镜像 tags的注册表客户端原文档给出了一个完整的可运行示例它不借助remote包的高层封装而是直接用transport构造授权客户端。以下为完整代码保持原文仅补充注释package main import ( io net/http os github.com/google/go-containerregistry/pkg/authn github.com/google/go-containerregistry/pkg/name github.com/google/go-containerregistry/pkg/v1/remote/transport ) func main() { repo, err : name.NewRepository(gcr.io/google-containers/pause) if err ! nil { panic(err) } // Fetch credentials based on your docker config file, which is $HOME/.docker/config.json or $DOCKER_CONFIG. auth, err : authn.DefaultKeychain.Resolve(repo.Registry) if err ! nil { panic(err) } // Construct an http.Client that is authorized to pull from gcr.io/google-containers/pause. scopes : []string{repo.Scope(transport.PullScope)} t, err : transport.New(repo.Registry, auth, http.DefaultTransport, scopes) if err ! nil { panic(err) } client : http.Client{Transport: t} // Make the actual request. resp, err : client.Get(https://gcr.io/v2/google-containers/pause/tags/list) if err ! nil { panic(err) } // Assert that we get a 200, otherwise attempt to parse body as a structured error. if err : transport.CheckError(resp, http.StatusOK); err ! nil { panic(err) } // Write the response to stdout. if _, err : io.Copy(os.Stdout, resp.Body); err ! nil { panic(err) } }这段代码的完整流程正好串联起本文所述的全部机制name.NewRepository解析仓库引用并生成 scopeauthn.DefaultKeychain.Resolve从 docker 配置文件解析凭据对应第四节 4.3transport.New内部完成 Ping → 挑战解析 → basic/bearer transport 选择 → token 预取对应第三节client.Get发出带认证的实际请求bearerTransport.RoundTrip负责在 token 失效时自动重新协商对应第四节 4.2transport.CheckError把非 200 响应解析为结构化错误对应第五节。变体与扩展若目标仓库是私有仓库把 scope 换成PushScope即可获得写权限若仓库走 HTTP本地测试 registry用name.NewInsecureRegistry构造即可让Ping自动协商 http 协议若希望输出调试信息可给底层 transport 套transport.NewLogger并把pkg/logs.Debug的日志开启。八、在 Harvester 项目中的定位与适用场景在 Harvester 仓库中transport是作为 go-containerregistry v0.20.2 的间接依赖被 vendored 的见 go.mod 中// indirect标记以及 vendor/modules.txt 中对其全部子包authn、name、v1/remote、v1/remote/transport的记录。这意味着凡是仓库内涉及与 OCI 镜像仓库进行认证交互的代码路径例如拉取/校验镜像、解析镜像元数据等其底层都运行着本包描述的握手与认证逻辑。对于需要在remote高层 API 之外直接操作仓库的场景——例如处理 schema 1 镜像、调用remote未封装的端点、或需要自定义 scope 与重试策略——transport就是被设计为直接使用的接口。它同时也是理解 go-containerregistry 全库认证体系的最小知识单元理解了transport也就理解了authn如何供料、remote如何组装。九、小结transport包以约十个源文件实现了 OCI Distribution 认证的完整闭环握手Ping→ 挑战分派 →basicTransport/bearerTransport两种认证流Token 交换与 OAuth2 refresh_token含 404 降级与access_token兼容自愈能力token 过期/scope 不足时自动重新协商并重发请求结构化错误CheckError 完整错误码枚举 暂时性错误判定可组合包装器retry、useragent、logger、scheme 选择与Wrapper退出通道。配合 vendor/github.com/google/go-containerregistry/pkg/authn 的凭据解析它构成了 go-containerregistry 乃至 Harvester 等下游项目所有镜像仓库交互的认证基石。后续若需进一步研究可继续阅读 vendor/github.com/google/go-containerregistry/pkg/v1/remote 目录下的options.go装配顺序、puller.go/pusher.go高层调用点以及 vendor/github.com/google/go-containerregistry/pkg/authn/keychain.go凭据解析实现。赞分享云原生虚拟化存储后端高可用【免费下载链接】harvesterOpen source hyperconverged infrastructure (HCI) software项目地址https://gitcode.com/gh_mirrors/ha/harvester点击查看免费下载相关推荐go-containerregistry transport 包解析为容器镜像仓库透明实现 Token 与 OAuth2 认证go containerregistry transport 包解析为容器镜像仓库透明实现 Token 与 OAuth2 认证 导读 在 OCI Distri云原生CLI应用安全linuxkit 镜像仓库认证客户端核心go-containerregistry transport 包的 Token/OAuth2 透明认证实现linuxkit 镜像仓库认证客户端核心go containerregistry transport 包的 Token/OAuth2 透明认证实现 本文聚焦操作系统云原生容器运行时深入 go-containerregistry transportk3d 依赖中的镜像仓库透明认证层Token 与 OAuth2深入 go containerregistry transportk3d 依赖中的镜像仓库透明认证层Token 与 OAuth2 本文以 k3d 仓库 v云原生容器编排创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
阅读完成 · 觉得有帮助?
咨询建站