☰
Lore 子仓库认证令牌设计解析:ADR-00003 如何在多子仓库场景下权衡 AuthN 令牌粒度
2026/9/25 3:24:21 网站建设 项目流程
  • 版本控制
  • 后端

【免费下载链接】lore

Lore is a next-generation, open source version control system

项目地址:https://gitcode.com/gh_mirrors/lore6/lore
点击查看免费下载

本篇围绕 Lore 的架构决策记录 ADR-00003(docs/developing/decisions/00003-auth-tokens-for-sub-repos.md)展开:当一个 Lore 仓库由多个拥有独立访问控制的子仓库(sub-repository)组成时,认证数据(auth data)应当以什么粒度表示。读完本文,你能理解该决策的三种候选方案及其取舍逻辑,并能在当前代码库中找到与之对应的实现证据——客户端的令牌交换流程、JWT 声明校验,以及服务端"认证与授权分离"的 gRPC 拦截器结构。

背景与问题陈述

Lore 仓库为了控制对受限内容的访问,可以由多个子仓库构成,每个子仓库拥有自己的访问控制(access controls)。在着手设计认证实现细节时,团队必须决定:认证数据如何表示一个仓库及其所有子仓库的访问关系?这正是 ADR-00003 要回答的问题(见文档 "Context and Problem Statement" 一节)。

决策驱动因素(Decision Drivers)有四个:

  • 确保与所有必要的网络传输(network transports)兼容;
  • 最小化客户端复杂度;
  • 最小化服务端复杂度;
  • 最小化授权验证(authorization verification)的开销。

这四个约束共同指向一个核心矛盾:令牌该"大而全",还是"小而多"。

三种候选方案

ADR 完整评估了三种表示认证数据的方式,本文完整继承其分析并展开。

方案一:单个令牌承载整个仓库的全部授权数据

在该模型中,客户端识别出 CLI 操作会涉及的所有仓库,并将"标识用户的令牌"交换为一个 JWT,该 JWT 描述用户对全部仓库的访问权限。ADR 给出了一个关键的容量估算:

  • 每个仓库约需 18 字节授权数据(16 字节仓库 ID + 读、写各 1 字节标志位);
  • 进入 JWT claim 后,base64 编码会使数据膨胀到每仓库 24 字节;
  • 外推到一条命令影响 300 个仓库的退化场景,JWT 体量可进入7KB 量级;
  • 而 HTTP 头部长度的普遍接受上限是8KiB,因此未来存在逼近该限制的现实风险。

该方案的利弊(引自 ADR):

  • 优点:客户端逻辑简单,无需请求或切换多个令牌;
  • 优点:QUIC 服务端逻辑简单,服务端收到单个令牌即可为连接上的所有流授权;
  • 优点:gRPC 服务端逻辑简单,令牌作为每个 gRPC 请求的 metadata 携带并用于授权;
  • 中性:服务端解析数百个仓库授权并高效检索存在一定开销;
  • 缺点:令牌大小可能逼近或超出 HTTP 头部限制。

方案二:每个仓库一个令牌,客户端按需切换(选定方案)

客户端同样先识别出 CLI 操作涉及的所有仓库,但不请求覆盖全部仓库的单一 JWT,而是为每个仓库各请求一个 JWT。发送 QUIC 请求时,客户端会为每个仓库(也就是每个令牌)建立一条连接,但连接内仍可使用多个流(streams)。

该方案的利弊(引自 ADR):

  • 优点:QUIC 服务端逻辑简单,单个令牌即可为连接上的所有流授权;
  • 优点:gRPC 服务端逻辑简单,令牌作为每个 gRPC 请求的 metadata 携带并用于授权;
  • 优点:令牌大小有界(bounded);
  • 优点:服务端授权一个请求时,无需在未定长的仓库授权列表中检索;
  • 缺点:客户端必须根据操作涉及的仓库数量管理多个令牌与多条连接。

方案三:令牌仅标识用户,服务端独立查授权

客户端完全不把令牌交换为仓库级令牌;每个被操作的仓库的授权,由服务端基于令牌所标识的用户独立验证。

  • 优点:令牌大小有界;
  • 优点:客户端逻辑简单;
  • 缺点:服务端必须为每条建立连接执行授权逻辑,而这可能需要与外部服务协调或以其他形式做 IO 来确认"该认证用户被允许做什么"。

决策结果及其后果

ADR 最终选择方案二:"One auth token per repository, client swaps between auth tokens as necessary"。选择理由原文表述为:该方式既能处理认证而不破坏 HTTP 用例,又能最小化技术栈其他地方的复杂度。

决策带来的后果(Consequences):

方向后果
Good不存在生成大到超出标准 HTTP 头部限制的认证令牌的风险
Good服务端围绕连接认证的逻辑无需改动
Good服务端验证认证的开销极小
Bad客户端需要为命令涉及的每个子仓库各换取一个令牌;在 QUIC 场景下还需为每个令牌各建立一条连接

换句话说,Lore 把复杂度有意压到了客户端(多令牌、多连接的管理),以换取服务端在认证热路径上的极简与令牌体量的恒定上界。

当前代码库中的实现证据

以下实现细节来自当前仓库源码,用于印证 ADR 中"令牌作为 gRPC 请求 metadata 携带""QUIC 连接按令牌建立"等设计点如何落地。

客户端:令牌交换与 JWT 声明校验

客户端认证入口在 lore-revision/src/auth/login.rs。其中exchange_token函数将外部令牌交换为 URC 认证令牌,随后立即执行verify_jwt_usage_for_remote,把解码后的 JWT claims 与目标远程(remote)的域名做匹配,再写入加密令牌存储(token_store::store_user_token)。with_token则根据token_type区分两条路径:lore类型令牌直接校验存储,其余类型走交换流程。交互式登录(interactive)通过clientState+ 轮询(poll_interactive_session,默认 30 次、间隔 5 秒)完成浏览器登录流。

令牌"该发给谁"的约束由 audience 声明决定。lore-credential/src/jwt.rs 中:

  • JWTUserInfo解析iss、sub、aud、exp等标准声明,其中aud被定义为"根域名列表";
  • acceptable_root_domains返回"签发者域名 + audience 中的根域名"合集;
  • domain_in_root_domains强制标签边界匹配而非裸后缀匹配——ADR 关联的威胁模型注释(见 lore-revision/src/auth.rs 顶部 "General Notes for Auth token handling")指出:攻击者可以搭一个把 Epic 受控 URC Auth 服务声明为认证提供方的恶意仓库,诱骗用户把令牌发给自己的服务器;因此"令牌只应给到令牌 audience 字段列出的域名"。domain_in_root_domains通过要求.epicgames.net形式的边界匹配,保证evilepicgames.net这类仿冒域名无法通过校验;
  • verify_jwt_usage_for_remote在域名不匹配时直接拒绝并记录 "forbidding JWT leak"。

这套机制与 ADR 方案二"每个仓库一个有界令牌"相配合:客户端持有的是面向特定仓库域名的令牌,audience 校验则防止令牌被跨域滥用。

服务端:认证拦截器只做认证,授权交给下游

ADR 提到"gRPC 服务端逻辑简单,令牌作为每个 gRPC 请求的 metadata 携带"。当前实现中,lore-server/src/auth/jwt_interceptor.rs 的JWTInterceptor实现了 tonic 的Interceptor:从 metadata 中提取Bearer令牌(extract_bearer_token),用JwtVerifier验证签名(缓存命中走同步热路径,block_in_place兜底),然后把原始令牌(RawToken)与解析后的声明(AuthorizationToken)作为 extensions 插入请求,交给下游处理器。

该文件内的测试用例明确固化了 ADR 隐含的认证/授权两阶段分离:

  • an_authenticated_token_with_no_partition_grant_passes_the_interceptor:即使令牌对某分区(partition,即仓库)没有任何授权(grant),拦截器也放行——"分区决策是下游 authorizer 的事";
  • a_request_with_no_bearer_token_is_unauthenticated:无令牌返回Unauthenticated;
  • a_token_failing_on_its_own_claims_is_refused:声明自证失败的令牌(如过期)被拒绝,且错误细节不外泄(统一返回 "Not allowed",原因只进日志)。

真正的仓库级授权发生在authnz层,见 lore-server/src/authnz/:repository_authorizer.rs、resource_grants_authorizer.rs、global_grants_authorizer.rs等按分区/资源维度检查用户授权。这与 ADR 方案二的"授权无需在未定长列表中检索"一致——每次请求只对应一个分区,授权判断是针对单仓库的。

gRPC 请求如何标注"针对哪个仓库"

方案二要求每个请求/连接对应单一仓库,代码中这一"分区"语义由 metadata 键承载。lore-transport/src/grpc/mod.rs 定义了:

pub const PARTITION_ID_KEY: &str = "lore-partition-bin";

存储客户端在发起请求时将该键的二进制值(16 字节RepositoryId/Context)追加到 metadata 中(见 lore-transport/src/grpc/storage_client.rs 中append_bin(PARTITION_ID_KEY, ...)的用法)。因此 gRPC 路径上的"令牌 + 分区"组合,正是 ADR 所描述的"令牌作为每个 gRPC 请求的 metadata 携带并用于授权"的具体形态:令牌负责认证身份,lore-partition-bin负责划定该次操作的仓库边界,authnz层再做单仓库授权判定。

小结

ADR-00003 的核心权衡可以概括为一句话:用客户端的多令牌/多连接管理成本,换取令牌的恒定体量上界与服务端认证热路径的极简。它否定了"超大 JWT"方案(7KB 逼近 8KiB 头部上限的容量风险)和"服务端逐次查授权"方案(每条连接引入外部 IO)。结合当前源码可以看到,该决策落地为三层结构:客户端侧的令牌交换与 audience 域名校验(lore-revision/src/auth/login.rs、lore-credential/src/jwt.rs),gRPC 请求上的lore-partition-bin分区 metadata(lore-transport/src/grpc/mod.rs),以及服务端认证拦截器与authnz授权层的职责切分(lore-server/src/auth/jwt_interceptor.rs、lore-server/src/authnz/)。研究 Lore 的认证体系时,这条 ADR 是理解"为什么令牌这么小、为什么认证和授权分开、为什么每个请求都带分区"的关键入口。

  • 版本控制
  • 后端

【免费下载链接】lore

Lore is a next-generation, open source version control system

项目地址:https://gitcode.com/gh_mirrors/lore6/lore
点击查看免费下载

相关推荐

上一篇:jQuery Mobile查看产品企业解决方案按钮设计指南
下一篇:Duix.Avatar 完整指南:一段10秒视频做出离线AI数字人

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询