摘要

  • 每把 WebDAV 锁都有一个由服务器生成的唯一令牌。令牌出现在 If 请求字段中,只能证明请求提交了这个标识,不能证明发送者身份、写权限或锁仍然存活。
  • 一次有效修改必须依次通过多道独立判断:认证主体、授权具体操作、解析所有受影响资源、提交全部必要令牌、计算条件与 ETag、核对锁创建者或特许覆盖者,并在提交时重验当前状态。
  • 深度无限的集合锁、共享锁、COPY/MOVE 和由服务器最终决定的超时,使令牌不可能成为随内容移动的租约。它是有限的协调证据,而不是资源控制权。

同一请求可能跨过六道不同的门

假设编辑器刚刚成功刷新了一把锁。响应中的 DAV:lockdiscovery 显示新的剩余时间,客户端随后发送 PUT,并把令牌放进 If。服务器确实找到这把锁,条件列表也为真,但最终拒绝写入。

如果运维面板只显示“token matched”,拒绝就像服务器自相矛盾。可在 RFC 4918 的模型中,匹配从来不是终点。修改被锁资源时,服务器除了检查有效令牌是否提交,还必须确认已认证主体与锁创建者相符。即使主体是创建者,持有锁也不会自动授予完整写权限;正常的认证和权限机制仍要执行。

这意味着下面几种情形都可能同时正确:

  • 会话更换了账户,旧令牌仍在客户端内存中;
  • 管理员撤销了内容写权限,却没有立即删除协调状态;
  • 请求能改属性,不能改正文;
  • 原 URL 可写,但 MOVE 的目标集合不允许新增成员;
  • 锁仍存在,ETag 条件已经因另一笔合法修改而过期。

令牌回答的是“请求知道哪把锁”。身份系统回答“谁在请求”。策略回答“他能做什么”。命名空间回答“究竟会改到哪里”。存储提交路径回答“此刻的状态能否兑现”。把这些问题交给一个字符串,等于在没有生命周期、受众限制和撤销语义的情况下,偷偷创造了一套持有即授权的能力系统。

唯一性很强,主张却很窄

WebDAV 为 HTTP 增加远程创作所需的属性、集合、命名空间操作和碰撞避免。写锁首先解决丢失更新:一个作者不知情地覆盖另一个作者的修改。它并不定义文件所有权,也不判断一次编辑是否符合业务流程。

每把锁恰有一个由服务器生成的唯一令牌。客户端不得从令牌结构推断意义。RFC 4918 要求令牌 URI 在所有资源与全部时间内保持唯一,使服务器不会把一把锁误认成另一把。新建锁成功时,服务器在 Lock-Token 响应字段和响应正文中都返回该值。

唯一性保障的是指代,不是授权。服务器还可以通过 DAV:lockdiscovery 暴露活动锁信息,因此协议明确不能依靠令牌的隐蔽性来授予写权。一个调试输出、属性查询或支持工单如果能看见令牌,不应因此变成取得控制权的通道。

RFC 4918 推荐 UUID URN,也保留永久注册的 opaquelocktoken URI scheme,并允许其他满足唯一性的 URI。RFC 9562 建议在需要不可猜测性时使用密码学安全的伪随机数生成器。随机性可以减少猜中,无法判断主体是否拥有 DAV:write-content、DAV:bind 或管理解锁权。

应当长期分开六个概念:

  1. 唯一性——避免两把锁混淆;
  2. 不可猜测性——降低旁观者推测令牌的概率;
  3. 知情——请求提交了哪个值;
  4. 认证——把请求绑定到哪个主体;
  5. 授权——该主体可执行哪些动作;
  6. 执行——当前状态下哪些改变真正写入。

前三项与令牌有关,后三项必须留在各自的权威系统里。即使令牌非常随机,也不能越过这条边界。

锁创建者有特殊关系,但不拥有资源

RFC 4918 给予创建者使用这把锁的特殊地位,同时把这种地位限制在普通权限之内。服务器也可以允许资源所有者、管理员或其他特许主体销毁别人的锁。这里存在三类角色:锁的创建者、资源操作的获权者、处理例外的覆盖者。三者有时重合,但协议不要求它们永远是同一人。

RFC 3744 把模糊的“写”进一步拆开。DAV:write-content 控制修改既有资源内容;DAV:write-properties 控制 dead properties;DAV:bind 控制向集合添加成员,包括向尚未映射的 URI 执行会创建资源的 PUT;DAV:unlock 控制非锁所有者执行 UNLOCK。

因此,拥有普通写权限的人不能忽略别人建立的锁;知道令牌的人也不能绕过 ACL。管理员可以恢复可用性,但覆盖操作必须作为例外被授权、归因和审计,而不是伪装成锁创建者的正常行为。

这正对应 Heng Lu 反复强调的“记录不是主权”。锁记录可以准确说明服务器曾建立何种协调关系,却不会因此成为一切权利的来源。实际权力位于能认证、能授权、能解析命名空间、能更改存储状态和能执行恢复的系统。清楚承认这点,不会削弱协议;反而防止协调层越界。

If 同时传条件,也提交令牌

WebDAV 的 If 字段最容易在代理、网关和日志层被错误简化,因为它有两项相互关联却不相同的职责。

第一项是条件表达。一个 state list 内的条件按 AND 组合,多组列表按 OR 形成备选,Not 否定紧随其后的状态令牌或 ETag 条件。未标记列表适用于 Request-URI,tagged list 则适用于明确写出的资源。

第二项是令牌提交。某个锁令牌只要出现在 If 中,就算请求把它提交给服务器。即使包含它的列表不是最终令整体为真的那一组,提交事实仍然成立。协议这样设计,是因为一次方法可能同时影响多个资源,也可能提供多个条件分支。

如果网关把 If 当普通布尔式,只转发“获胜分支”,某个受影响资源需要的令牌就会消失。如果日志只记“token present”,又会把 ETag 不成立误写成前置条件通过。正确的观测必须同时记录:哪些令牌被提交、哪些列表被计算、每项条件针对哪个资源、最终为何失败。

412 与 423 也代表不同门槛。If 条件为假,服务器在授权检查之后返回 412 Precondition Failed。若操作会影响被锁资源,但请求没有提交必要令牌,服务器以 423 Locked 和 lock-token-submitted 前置条件说明缺失。前者是状态断言不成立,后者是锁证据未交齐。它们都不应被面板压成“锁冲突”。

名称看起来更直接的 Lock-Token 字段反而用途更窄:LOCK 新建成功时在响应中返回令牌,UNLOCK 请求用它指出要移除哪把锁。PUT、PROPPATCH、COPY、MOVE、DELETE 等状态变更方法通过 If 提交相关状态令牌,而不是把 Lock-Token 当作通用授权头。

请求写出的 URL 不等于完整影响范围

一把锁有 lock root、scope、type 和 depth。直接锁起始于创建它的根 URL。集合上的 depth-infinity 锁会间接覆盖所有后代,也会覆盖后来加入命名空间的新成员。

因此,请求目标即使只是 /workspace/report.md,实际约束也可能来自 /workspace/ 上层集合。前端只查询文件自己的状态,看不到继承锁;服务器必须根据当前命名空间重建全部适用锁。

LOCK 的 Depth 只能是 0 或 infinity,缺省为 infinity。若服务器尝试锁定整个层级,途中遇到不兼容锁,不能留下部分成功的树。指定范围内必须全成或全败,Multi-Status 可指出阻塞成员。

普通修改也会扩展影响面。DELETE 不只删除成员,还改变父集合的成员关系。COPY 和 MOVE 同时具有源与目标,覆盖目标时还会影响目标原状态。协议要求为所有必须解锁才能完成操作的资源提交必要令牌,而不是只验证 Request-URI 对应的一个值。

MOVE 最能说明令牌不是随内容携带的租约。资源被移动后,源地址上的直接锁不会跟到新 URL。资源可能离开源集合的间接锁范围,又进入目标集合的深度无限锁范围。同一份内容因为命名空间关系变化而受不同锁约束;旧令牌不会获得迁徙权。

所以,授权引擎若只收到“一个路径、一个 token”,无法准确判断改变两个集合成员关系并可能覆盖目标的操作。完整受影响资源集必须在授权与提交前解析,并在提交时针对当前状态复核。

共享锁不是共享密码

共享锁允许其他兼容共享锁共存,但每次成功 LOCK 仍创建一把独立锁和一个独立令牌。三名协作者不是拿到同一个团队密码,而是分别拥有三条协调关系。

刷新其中一把,不会延长其他锁。UNLOCK 删除指定令牌对应的锁,也不保证资源从此“无锁”;其他共享锁或上层集合的间接锁可能继续生效。

“共享”只说明技术兼容性,不会自动创建共同决策权。如何合并修改、谁负责复核、意见冲突如何裁决、哪个版本最终成立,仍由应用流程决定。WebDAV 不替组织发明多数表决,也不把多个令牌合并成一个集体主体。

因此,管理界面不该只显示 locked/unlocked。它至少要区分创建者、直接或间接关系、根、深度、共享或独占、服务器给出的期限以及每次 UNLOCK 实际移除了哪一把锁。

超时由服务器决定,不是客户端签下的租约

客户端可以在 Timeout 中建议期限,服务器却拥有最终选择权,可以忽略或改变请求。服务器实际返回的期限是一项当前状态,不是客户端单方面生成的保证。

刷新锁使用无正文的 LOCK,并在 If 中放入一枚令牌。服务器忽略刷新请求中的 Depth;成功后重启该锁计时器,不影响其他共享锁,并在响应正文中返回更新的 DAV:lockdiscovery。成功刷新不会再返回新的 Lock-Token 响应字段。

刷新失败时,客户端不得假设已经续上。即便本地计算尚未到期,也不能保证锁仍然存在:特许覆盖、服务器崩溃或状态丢失都可能提前移除。反过来,本地时钟走到零也不能证明服务器恰在那一刻完成删除。

安全客户端把 timeout 当作续约义务与清理提示,而不是硬租约。关键写入前仍要提交令牌、核对服务器状态和资源版本,让提交路径在当前事实下决定。安全服务器则记录实际采用的时限、刷新主体、时钟异常、持久化故障与孤儿锁。

可重建的一次写入

高可信实现应让九个步骤各自留下证据:

  1. 认证主体和凭证上下文;
  2. 授权具体方法及内容、属性、成员关系或管理解锁动作;
  3. 解析请求 URI、目标、父集合和可能被覆盖的资源;
  4. 发现当前适用的直接、间接、独占、共享锁;
  5. 完整解析 tagged/untagged If、ETag、Not 与备选列表;
  6. 确认所有必要令牌都已提交并对应仍然有效的锁;
  7. 核对认证主体与锁创建者,或证明特许覆盖已获授权;
  8. 在提交点重验状态,按方法规定的原子范围执行或拒绝;
  9. 保留能区分认证失败、权限拒绝、条件失败、令牌缺失、锁消失和多资源冲突的审计记录。

日志不必广泛保存原始令牌。带密钥的指纹可以跨事件关联,同时降低从日志复用的风险。主体、方法、完整影响集、锁根与深度、条件结果、权限决定、实际期限、最终版本和响应码才是复盘所需的最小证据。

测试也要跨门设计:甲主体创建、乙主体提交同一令牌;有权限但缺令牌;令牌正确但 ETag 错;深度无限锁自动覆盖新成员;层级锁遇阻后不留半棵树;直接锁资源 MOVE 后旧锁不随行、目标集合锁生效;多个共享锁只刷新或移除其中之一;管理员覆盖操作可归因;直接存储或另一 API 不能静默绕过协调规则。

WebDAV 锁能降低一类丢失更新,却不能处理业务死锁、语义合并、恶意管理员、绕过存储或分布式事务。把这些边界写清,才不会在真正需要证据时发现令牌被赋予了从未拥有的含义。

来源