Ohhnews

分类导航

$ cd ..
InfoQ Java原文

Spring Security 7 服务端 DPoP 与令牌交换实践

#spring security#dpop#令牌交换#oauth 2.1#身份认证

核心要点

  • Spring Authorization Server 现已并入 Spring Security 7,且沿用相同的 Maven 坐标,因此从 1.5.x 迁出只是一次版本号升级,而非更换构件。
  • 持有证明(DPoP,RFC 9449)在 Spring Security 6.5 配合 Spring Authorization Server 1.5 时就已经可以端到端工作。7.1 线上真正的新变化是:资源服务器端的校验不再自动生效,因此在升级后,依赖旧有自动应用行为的应用会悄无声息地停止强制执行 DPoP,直到它加上新的 .dPoP() 调用。
  • Spring 会自行通过 cnf.jkt 以及公共客户端的刷新令牌完成绑定,把两个真正的决策留给身份提供方:nonce 策略,以及默认采用内存、单节点实现的 jti 重放缓存是否需要替换为集群范围内的分布式实现。
  • RFC 8693 令牌交换把许多身份提供方(IdP)至今仍保留的"服务账号置换"定制做法,转变为一种标准授权类型,具备 subject_token 与 actor_token 语义,以及记录委托链的 act 声明。
  • 这两个特性合在一起比分开使用更有价值,因为将一个受 DPoP 约束的主体令牌交换为收窄的、限定受众的下游令牌,正是服务间调用与智能体架构反复采用的模式;但这一组合模式的安全性,完全取决于你所编写的交换策略与令牌定制器。

你的自建身份提供方已经在 Spring Authorization Server 1.x 上运行了两年。从纸面上看,升级到 Spring Boot 4 只是例行公事:更新物料清单(BOM)、修复废弃项,然后发布。接着你打开迁移指南,却发现你所依赖的授权服务器已不再是一个独立项目——你在 1.x 线上的这段时间里,它已经并入了 Spring Security 7.0。

在当前与 Spring Boot 4.1 配对的 7.1 线上,你一直推迟的两个 OAuth 2.1 特性——发送方约束令牌和真正的委托授权——正位于同一个模块中,即 Spring 正式发布(GA)版本,等待被启用。

这正是 Spring Boot 4 迁移中迁移指南所跳过的那部分。它们讲了 Jackson 3、lambda DSL 和包重定位,然后在 OAuth 2.0 处停了下来。认证层的收益体现在 DPoP(RFC 9449)和令牌交换(RFC 8693)上,两者首次在官方组件中达到生产就绪状态——对于任何自建 IdP 的人来说,这才是这次升级真正物有所值的地方。

本文是一份服务端操作指南,涵盖整合了什么、开启 DPoP 时会发生什么变化、一次 RFC 8693 交换的端到端样貌,以及为什么这两个特性值得一起使用。文章假定你已经在运行 OAuth 2.0 / OIDC 服务器并了解协议,不会从头重新解释 OAuth。

Spring Boot 4 整合了什么

Spring 2025 年 11 月的发布列车分三步走:Spring Framework 7.0 于 11 月 13 日发布,Spring Security 7.0 于 11 月 17 日发布,Spring Boot 4.0 于 11 月 20 日发布。发行说明以模块化、JSpecify 空安全、API 版本管理和 Jackson 3 基线打头。OAuth 服务器相关内容则位于这些模块之下。

这些变化早在两个月前就已公布。2025 年 9 月 11 日,Spring 团队宣布,自 2020 年起作为独立项目、自 2022 年 11 月起 GA 的 Spring Authorization Server,将并入 Spring Security 7.0。独立的 1.5.x 分支是最后一代——新的开发工作已在 Spring Security 内部进行。Spring Security 于 2026 年 6 月发布 7.1.0,与 Spring Boot 4.1 配对,随后于 2026 年 8 月发布 7.1.1,因此当前坐标是 org.springframework.security:spring-security-oauth2-authorization-server:7.1.1。

整合本身发生在 7.0。本文的示例专门针对 7.1.1,因为本文涉及的两处版本相关细节都落在这一版本:资源服务器端的 .dPoP() 选择性启用(7.1.0 新增),以及可插拔的 jti 重放缓存(7.1.1 新增)。二者在出现处均有说明。

对于一个一直把"授权服务器"当作独立依赖、拥有自己发布节奏的团队来说,整合本身就是头条新闻。现在只有一个安全模块、一个版本、一套需要跟踪的发行说明。

这里有一个值得弄清楚的细微之处,因为这是人们在此次迁移中最常搞错的一点。Maven 坐标没有变化。 独立项目本来就以 org.springframework.security:spring-security-oauth2-authorization-server 发布,而 Spring Security 7 中合并后的模块使用相同的 groupId 和 artifactId。变化的只是版本从 1.5.x 线移到了 Spring Security 的版本线(7.0.x,现为 7.1.x)。如果你在某处读到需要重命名 spring-authorization-server 构件,那个构件从来就不存在。

如果你是通过 Boot starter 而非直接引入库的方式接入授权服务器,那么确实有一处重命名适用。在 Boot 4 中,该 starter 增加了 security- 前缀,因此 spring-boot-starter-oauth2-authorization-server 已被弃用,推荐使用 spring-boot-starter-security-oauth2-authorization-server,两者都会一直发布到 4.1.1。除此之外,迁移就是一次版本号升级,外加每个 Spring Security 7 使用者都要吸收的同样那些包重定位和 Jackson 变更。这不是一次坐标替换。

$ xml
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-authorization-server</artifactId>
    <version>1.5.7</version>
</dependency>

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-authorization-server</artifactId>
    <version>7.1.1</version>
</dependency>

迁移到 7.x 线后,你继承的是一个 DPoP 签发、刷新令牌 DPoP 绑定和令牌交换已经与 Spring Security 6.5 中达到 GA 的资源服务器 DPoP 支持共存的模块。在这次整合之前,要在一个地方集齐所有这些功能,需要把某个特定的独立授权服务器版本锁定到某个特定的 Spring Security 版本,并指望这个版本矩阵能对得上。现在只需一个版本号。

[LOADING...]

图 1. Spring Boot 4 的整合。(来源:作者绘制。)

如图 1 所示,独立的 Spring Authorization Server(1.5.x,最后一代)在相同的 Maven 坐标下并入 Spring Security 7.x 线,将 DPoP 与令牌交换整合进单一模块。

迁移税,一次付清

在体验任何新特性之前,Spring Security 7 会收取一笔基线迁移税,它大多与 OAuth 无关:HttpSecurity 构建器中的 and() 被移除,Jackson 3 取代 Jackson 2,还有少数 JSON Web Token(JWT)解码器默认值发生变化。Spring 自己的迁移指南及其OAuth 2.0 专题页面讲解了具体机制;具体步骤是下文实战手册中的第 2 到第 4 项。

该指南没有明说两件事,因为它们针对的是运行授权服务器,而非任何 Spring Security 应用。

第一,typ 请求头的变化并不是校验放宽。相反,它实际上是 Spring 终于把 6.5 准备步骤要求你手动添加的东西自动接上了:validateTypes(false) 关闭 Nimbus 自身的 typ 检查,以便你自行注册 JwtTypeValidator.jwt(),而在 7.0 中,这一套操作的两半都会替你完成。真正的审计点比"我的令牌还能通过校验吗?"要窄。JwtTypeValidator.jwt() 要求 typ 为 JWT 或缺失,因此你的服务器签发或接受的任何带有不同 typ 的令牌——例如 RFC 9068 的 at+jwt 访问令牌类型——现在默认都会校验失败,除非你注册一个允许它的校验器。

第二,如果你使用 Spring 的 Jackson 模块把 OAuth2Authorization 对象持久化到数据库,那么该注册必须从 Jackson 2 的 SecurityJackson2Modules 迁移到新包 org.springframework.security.jackson 中的 Jackson 3 SecurityJacksonModules,否则之前持久化的授权状态在反序列化时会出错。

一个已经跟上 Spring Security 6.5 废弃项节奏的团队,可以把基线迁移规划为大约一个冲刺的工作量,然后再去碰任何新的 OAuth 2.1 特性。落后好几个版本的团队则应当把基线迁移和特性采用当作两项独立的工作来做。

DPoP 端到端

DPoP(RFC 9449)把访问令牌约束到客户端持有的密钥上。客户端生成一对非对称密钥,并在每次请求时对一个短时效的证明 JWT 签名。授权服务器在令牌端点校验该证明,并将签发的访问令牌绑定到证明的公钥。资源服务器随后会拒绝兑现该令牌,除非它携带来自同一密钥的新鲜证明。被盗的令牌在没有私钥的情况下毫无用处。这就是全部要点,也是本文浏览器端姊妹篇的主题——那篇文章讨论浏览器能安全地把该密钥保存在哪里的难题。这里关注的是链路的另一端,即证明到达时服务器会做什么。

证明本身由 RFC 9449 §4.2 规定。请求头携带 typ: dpop+jwt、非对称算法,以及以 jwk 形式内联的客户端公钥。载荷用 htm(HTTP 方法)、htu(目标 URI,去掉查询和片段)、iat 和唯一的 jti 将证明绑定到请求。当证明随访问令牌一起送往资源服务器时,它还携带该令牌的 base64url SHA-256 哈希值。

[LOADING...]

图 2. DPoP 环路,服务器端。授权服务器将令牌绑定到客户端的密钥,资源服务器在没有匹配证明时拒绝该令牌。(来源:作者绘制。)

签发:默认值留给你的两个决策

在授权服务器一侧,Spring 替你接好了证明校验机制。DPoPProofVerifier 以及配套的 DPoPProofJwtDecoderFactory 执行 RFC 9449 §4.3 的检查:格式良好的 JWT、typ 为 dpop+jwt、非 none 的非对称算法、可针对内嵌 jwk 验证的签名,以及 htm/htu 与令牌请求匹配。

有两件事看起来像是决策,其实不是。真正留给你的,是自己构建 nonce 挑战(如果你确实想要的话)以及重放缓存的作用范围。

cnf.jkt 绑定是替你处理的。访问令牌必须携带一个 cnf 声明,其 jkt 成员是证明密钥的 base64url 编码 JWK SHA-256 指纹(RFC 7638)。当 DPoP 生效时,Spring 会自动填充该键;唯一需要注意的是有没有令牌定制器意外覆盖了 cnf。资源服务器之后比对的是指纹,而不是公钥,也不是证书哈希。

$ cat
{
  "sub": "service-a",
  "aud": "https://orders.internal",
  "cnf": {
    "jkt": "0ZcOCORZNYy-DWpqq30jZyJGHTN0d2HglBV3uiguA4I"
  }
}

首先来看 nonce 挑战,Spring 完全没有实现它。RFC 9449 §8 允许授权服务器要求在证明中携带一个服务器选定的 nonce,以阻止客户端预先制造证明;资源服务器在 §9 中有对应的机制。当服务器要求 nonce 而客户端未提供时,服务器会以 error: use_dpop_nonce 加一个 DPoP-Nonce 响应头作答——在授权服务器端返回 400,在资源服务器端返回 401。截至 Spring Security 7.1.1,Spring 在两侧的 DPoP 代码中都没有这些:既不签发 DPoP-Nonce 响应头,也不处理 use_dpop_nonce,更没有校验证明中可选的 nonce 声明的校验器。nonce 挑战在 RFC 中是 SHOULD 而非 MUST,这大概就是它在没有该机制的情况下也能发布的原因;但如果你的威胁模型需要它,那你就得从头构建整套机制:签发 nonce、返回挑战响应、存储已签发的内容,并依据它校验声明。

同样为你处理的:公共客户端的刷新令牌绑定

RFC 9449 §5 要求,当公共客户端在令牌端点出示 DPoP 证明时,其收到的刷新令牌须绑定到该密钥,之后的刷新必须能证明同一密钥。Spring Authorization Server 在 1.5.0-RC1 里程碑(2025 年 4 月)中自动加入了这一机制,并延续到 7.x 线;没有任何开关需要拨动。机密客户端则改用其既有的客户端凭据绑定,因此这一机制对单页应用(SPA)和原生应用最为重要。

其次,是重放缓存。RFC 9449 §11.1 要求在可接受的时间窗口内拒绝 jti 已被见过的证明。在 Spring Security 7.0.x 和 7.1.0 上,这项检查是硬编码的:一个固定的、一千条目的内存缓存,埋在 DPoPProofJwtDecoderFactory 内部,无法替换。自 Spring Security 6.5.12 / 7.1.1 起,它变成了可插拔的 DPoPProofReplayValidator,默认接上自己的 InMemoryCache(十万条条目,按键设上限)。该默认实现自己的 Javadoc 就直言,它"只在单节点部署下有效",并点名 Redis 和 Hazelcast 作为集群部署需要另行提供的分布式 Cache 类型。

资源服务器端现在原生支持校验

资源服务器一侧是 Spring 7 移除最多自定义代码的地方。受 DPoP 约束的访问令牌校验经由 Spring Security 6.5.0 的各个里程碑到来,并在 6.5.0(2025 年 5 月)GA。这里有一个版本细节很重要。在 Spring Security 7.0 中,只要 DPoP 相关类在类路径上,该支持就会被自动应用,没有 DSL 钩子。Spring Security 7.1 为资源服务器 DSL 增加了显式的 dPoP(Customizer) 方法(@since 7.1),使该支持变为选择性启用且可定制;在 7.1 中,你需要调用 .dPoP() 来启用它。

这是升级时一次真正的行为变化,而不仅仅是一个新选项:一个依赖 6.5 或 7.0 中自动应用校验的应用——包括完全基于 Spring Boot 默认资源服务器自动配置构建、从未调用过新钩子的应用——在迁移到 7.1 的瞬间就会悄无声息地停止强制执行 DPoP,直到显式加上 .dPoP()。

受 DPoP 约束的令牌以 DPoP 认证方案到达,而不是 Bearer。把它当作 Bearer 发送是一个常见且错误的捷径。Spring 的 DPoPAuthenticationProvider 负责完成这项工作。

有一个命名陷阱值得指出,因为它会在代码评审时咬你一口。资源服务器端的 provider 是 DPoPAuthenticationProvider,没有 OAuth2 前缀,不像下一节的 OAuth2TokenExchangeAuthenticationProvider。转换器和 provider 也位于不同的包中。DPoPAuthenticationConverter 位于 ...resource.web.authentication,而 DPoPAuthenticationProvider 和 DPoPAuthenticationToken 位于 ...resource.authentication,没有 web。

$ java
// 资源服务器:接受受 DPoP 约束的访问令牌。
@Bean
public SecurityFilterChain resourceServer(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
        .oauth2ResourceServer(oauth2 -> oauth2
            .jwt(Customizer.withDefaults())   // 校验访问令牌
            .dPoP(Customizer.withDefaults())  // 校验证明:签名、htm/htu、ath、cnf.jkt
        );
    return http.build();
}

配置好 .dPoP() 后,资源服务器会验证证明签名、确认 htm/htu 与传入请求一致、检查证明的路径是否等于所出示令牌的哈希值,并确认证明密钥的指纹等于令牌的 cnf.jkt。在 Spring Security 6.5 之前,这曾是需要手写的过滤器代码。现在它只是 DSL 中的一行。

无需定制管道的令牌交换

多数自建 IdP 都带有某种版本的"服务账号置换"。服务 A 需要代表用户调用服务 B,于是它去取第二套凭据、通过旁路铸造一个新令牌,或者把用户自己的令牌向下游传递,并指望受众检查足够宽松。RFC 8693 用标准授权类型取代了这种做法。

令牌交换在 Spring Authorization Server 1.3.0-M3(2024 年 3 月)中落地,现位于 7.x 模块中。授权类型为 urn:ietf:params:oauth:grant-type:token-exchange。请求中包含一个 subject_token(代表调用所为之服务的一方的令牌),以及可选的 actor_token(代表实际执行动作的一方),外加调用方希望在输出中获得的受众、资源和作用域。

$ http
POST /oauth2/token HTTP/1.1
Content-Type: application/x-www-form-urlencoded
DPoP: eyJ...   # 边缘服务的证明,这样签发的令牌会重新绑定到它的密钥

grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&subject_token=eyJ...        # 传入的用户令牌,调用是为其进行的
&subject_token_type=urn:ietf:params:oauth:token-type:access_token
&actor_token=eyJ...          # 边缘服务自己的令牌;正是它产生了 act 声明
&actor_token_type=urn:ietf:params:oauth:token-type:access_token
&audience=https://orders.internal
&scope=orders:read

这次交换最重要的地方,在于 RFC 8693 §1.1 区分的两种语义。模拟(impersonation)签发的令牌看起来就是主体本身,行为者随之消失。委托(delegation)则保留双方身份:签发的令牌携带主体,以及一个标识谁在行事的 act(actor)声明。你得到哪一种,由一个参数决定。只发送 subject_token,Spring 的 OAuth2TokenExchangeAuthenticationProvider 就会签发一个不含 act 声明的模拟令牌。同时发送 actor_token,该 provider 就会构建一个委托令牌,从 actor_token 而不是从已认证的客户端推导出 act 声明。对于任何日后需要审计的场景以及服务间调用来说,委托正是你想要的。

RFC 8693 §4.1 用嵌套的 act 声明来表示多跳链,最外层是当前行为者,每一层嵌套是此前的行为者。嵌套条目来自主体令牌。如果网关此前的交换已经在边缘服务现在作为 subject_token 出示的令牌上留下了 act 声明,Spring 就会把那个先前的行为者作为嵌套条目传递下去。所以在这个例子中,边缘服务(actor 令牌的所有者)成为当前行为者,而 gateway-service 作为更早的一方嵌套在其下。

$ cat
{
  "sub": "user-123",
  "aud": "https://orders.internal",
  "scope": "orders:read",
  "act": {
    "sub": "edge-service",
    "act": {
      "sub": "gateway-service"
    }
  }
}

在 Spring 一侧,该授权类型由 OAuth2TokenExchangeAuthenticationProvider 处理,它通过授权服务器配置器注册。该授权类型并非按客户端默认启用。调用方客户端的 RegisteredClient 必须声明它,否则 provider 会以 unsupported_grant_type 拒绝请求。

$ java
RegisteredClient.withId(UUID.randomUUID().toString())
    .clientId("edge-service")
    // ... 客户端认证、作用域 ...
    .authorizationGrantType(AuthorizationGrantType.TOKEN_EXCHANGE)
    .build();

act 声明本身是框架的职责,如上所述从 actor 令牌构建,因此你无需手工拼装。留给你的,是通过丢弃作用域或切换受众来收窄签发的令牌,这是 OAuth2TokenCustomizer bean 的职责——与你用于任何声明塑形的同一个钩子。

$ java
@Bean
public OAuth2TokenCustomizer<JwtEncodingContext> tokenCustomizer() {
    return context -> {
        if (AuthorizationGrantType.TOKEN_EXCHANGE.equals(
                context.getAuthorizationGrantType())) {
            // 收窄受众并裁剪交换所得令牌的作用域。
            context.getClaims().audience(List.of("https://orders.internal"));
            context.getClaims().claim("scope", "orders:read");
        }
    };
}

有一条原则值得牢牢记住。交换是你降低权限的地方,所以绝不要放宽它。一个可被利用来访问超出主体令牌所拥有作用域的令牌交换端点,是一个权限提升漏洞,而不是一个特性。

同时需要 DPoP 和令牌交换的模式

这两个特性各自单独都有用。组合起来,它们覆盖了一个单靠任何一方都无法解决的问题:在一连串服务之间传递受约束的权限,而不给每一跳一个可被重放的 bearer 令牌。

设想一个边缘服务收到来自客户端的、受 DPoP 约束的访问令牌。它需要代表用户调用订单服务,但不应该把用户的令牌原封不动地转发出去。那个令牌的受众是边缘服务,转发一份 bearer 副本会让订单服务拿到一个可以反过来在任何地方重放的东西。

相反,边缘服务执行一次令牌交换。它把用户的令牌作为 subject_token 出示,把自己的令牌作为 actor_token,并在交换请求上附上 DPoP 证明,然后请求一个作用域为 orders:read、受众为 https://orders.internal 的令牌。授权服务器签发一个收窄的令牌,从那个 actor 令牌在 act 声明中记录边缘服务,并且,由于边缘服务发送了证明,通过 cnf.jkt 把新令牌绑定到边缘服务自己的密钥上。

[LOADING...]

图 3. 受 DPoP 约束的令牌被交换为收窄的、重新绑定的下游令牌。作用域缩小、受众切换、行为者被记录,且每一跳都受发送方约束。(来源:作者绘制。)

结果是具体的。订单服务收到的令牌,可以验证它受发送方约束、约束对象正是实际与之通信的调用方、作用域恰好是那次调用所需的,并带有一条谁委托给谁的审计链。没有任何一跳为它并不调用的服务持有可重放的凭据。这正是现代服务网格和智能体系统反复用临时请求头传递方式拙劣地重新发明的那套形态。RFC 8693 加 RFC 9449 是标准轨道的版本,而 Spring Security 7 是首个两部分一同发布的官方 Spring 接口。

这里有一个实实在在的告诫。收窄工作由你来强制执行。Spring 给了你授权类型和定制器钩子,但它不会阻止你写一个放宽作用域或跳过受众切换的交换。这一模式的安全性,存在于你所编写的 OAuth2TokenCustomizer 里。那段代码值得拥有自己的测试。

迁移实战手册

两个起点覆盖了大多数团队。

第一个是全新项目,基于 Spring Boot 4.1。新的 IdP 从 7.x 模块起步,DPoP 和令牌交换从第一天就可用。工作是配置而非迁移:注册客户端、决定你的 nonce 策略、编写收窄定制器,并在每个资源服务器上开启 .dPoP()。从第一次提交起就把 OAuth2TokenCustomizer 当作安全关键代码对待。

第二个起点是现有的独立 Authorization Server 1.5.x。这是在你已经使用的坐标上进行版本升级,而不是重新平台化。步骤如下:

  1. 迁移到 Spring Boot 4.1 / Spring Security 7.1 BOM。把 spring-security-oauth2-authorization-server 从 1.5.x 升到 7.1.x,groupIId 和 artifactId 不变。
  2. 把任何残留的 and() 风格过滤器链转换为 lambda DSL。
  3. 审计 JwtDecoder 配置中的 validateTypes(false),并确认新的 JwtTypeValidator 默认值接受你的令牌。
  4. 在持久化授权状态的地方更新 Jackson 模块注册,让 SecurityJackson2Modules 变为 SecurityJacksonModules。
  5. 只有在此之后,作为一项单独的变更,为新客户端注册启用 DPoP 并搭建令牌交换授权类型——在基线迁移变绿之后。

生产上线有自己的决策——那些把一个特性开关变成一次部署的决策:

  • 在切换期间保持签发者 URL 稳定。
  • 在授权服务器元数据中公布 DPoP 和令牌交换授权类型,以便客户端能够发现它们。
  • 在开始签发受 DPoP 约束的令牌之前,先铺开资源服务器端的 .dPoP() 支持,这样当第一个受约束令牌到达时,校验器已经就绪。
  • 规划一个双栈窗口,在客户端迁移期间让 bearer 令牌和受 DPoP 约束的令牌共存。
  • 按客户端注册而非全局启用 DPoP,这样现有生态继续工作,新注册则选择加入。

元数据正是让客户端能够发现这一切的东西。授权服务器的 /.well-known/oauth-authorization-server 文档应公布两项能力:它所接受的 DPoP 签名算法,以及授权类型列表中的令牌交换授权类型。

$ cat
{
  "issuer": "https://idp.internal",
  "grant_types_supported": [
    "authorization_code",
    "refresh_token",
    "urn:ietf:params:oauth:grant-type:token-exchange"
  ],
  "dpop_signing_alg_values_supported": ["ES256", "RS256"]
}

接下来会发生什么

特别留意 nonce 这块缺口。RFC 把它标为 SHOULD 而非 MUST,而截至本文写作时,Spring Security 的问题跟踪器里也没有针对它的开放特性请求,因此想要它的身份提供方只能独自构建,并且应当为此做好规划,而不是等待未来某个版本。更持久的趋势在这一次发布之上更高一个层级。多跳服务调用,以及越来越多跨信任边界行动的自主智能体链,既需要密钥持有证明,也需要一条经过审计、逐层收窄的委托链,而不仅仅是 bearer 令牌加作用域检查。RFC 8693 的嵌套 act 声明已经对这种链进行了建模。随着这种架构扩散,本文所涵盖的这一组合——DPoP 绑定加令牌交换——看起来就不再像是 Spring Security 7 的一对特性搭配,而更像是身份提供方普遍应有的默认预期。

参考资料

(所有 URL 访问于 2026 年 10 月。)

关于作者

[LOADING...]

Dhruv Agnihotri

Dhruv Agnihotri 是 Salesforce 的首席技术成员(Lead Member of Technical Staff),拥有十余年工程经验。他负责 Salesforce 平台上的认证与授权工作,研究兴趣包括浏览器安全、OAuth 2.1 和 AI 智能体身份。他的开源工作探索同样的问题——HeadlessKit 用于自托管认证,Authgent 用于 AI 智能体委托中的 OAuth 2.1。他拥有密歇根大学安娜堡分校电气与计算机工程(机器学习方向)硕士学位,并在自己的博客上写作。